Kotlin支持面向对象的三大特征:继承、多态、封装。提供了private、protected、internal、public四个访问权限修饰符。
1、Kotlin定义类,语法:
[修饰符] class 类名 [constructor 主构造器]{
零到多个次构造器
零到多个属性
零到多个方法
}
修饰符:可以是public|internal|private(只能出现其中之一)、final|open|abstract(也只能出现其中之一)或者完全省略修饰符。
Kotlin类的定义:类名、类头(泛型声明、主构造器等)和用花括号包围的类体构成。类头和类体都是可选的【空类没有类体,可以省略花括号】,比如class EmptyClass。
2、一个Kotlin类有0~1个主构造器、0~N个次构造器。主构造器是类头的一部分,它跟在类名和泛型声明之后:
class User constructor(name:String){}//如果主构造器没有任何注解或修饰符,则可以省略constructor关键字。
主构造器是在类头使用constructor关键字定义一个无执行体的构造器。虽然主构造器不能定义执行体,但是可以定义多个形参,这些形参可以在属性声明、初始化块中使用。
关于默认构造器:如果没有为非抽象类定义任何的主/次构造器,系统会自动提供一个无参数的主构造器,该构造器默认用public修饰。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。
3、类的属性
[修饰符] var|val 属性名:类型 [= 默认值]
[<getter>]
[<setter>]
修饰符:可以省略,也可以是private|protected|internal|public、final|open|abstract。
public|protected|private只能出现其中之一,final|open|abstract也只能出现其中之一。
对于属性使用protected|internal|public修饰符和不使用访问控制符,效果是一样的。如果使用private修饰该属性,该属性将作为幕后属性使用。
[<getter>]|[<setter>]:用于为该属性编写自定义的getter、setter方法。如果不指定,kotlin会为读写属性提供默认getter、setter方法,为只读属性提供默认的getter方法。
4、顶层函数与类中方法的区别
主要区别在于修饰符:顶层函数不能使用protected、abstract、final修饰符。但类中的方法可以使用private|protected|internal|public、final|open|abstract这些修饰符。
5、构造器的语法:
[修饰符] constructor (形参列表){
//零到多条可执行语句组成的构造器执行体
}
修饰符:可以省略,也可以是private|protected|internal|public其中之一
形参列表:其格式和定义方法的形参列表的格式完全相同
6、方法与函数的关系
Kotlin中的方法与函数其实是统一的,不仅定义函数和方法的语法相同,而且定义在类中的方法依然可独立出来。也就是说,即使我们将方法定义在类里面,这个方法依然可以转换为函数。
class Person {
var age: Int = 0
constructor() {
this.age = 10
}
fun showAge() {
println(age)
}
}
fun main(args: Array<String>) {
var show = Person::showAge//将showAge方法独立成函数,调用showAge方法的调用者将作为第一个参数传入
var p = Person()
show(p)
}
7、解构与componentN方法
Kotlin允许将一个对象的N个属性“解构”给多个变量,写法如下:
var(name,pass) = user
Kotlin是怎么知道把哪两个属性赋值给name、pass变量的?其实Kotlin会将上面的赋值代码转换为如下两行:
var name = user.component1()
var pass = user.component2()
如果希望将对象解构给多个变量,那么必须为该对象的类定义componentN方法,程序希望解构多少个变量就定义多少个componentN方法,且该方法使用operator修饰。
class User(name: String, pass: String, age: Int) {
var name = name
var age = age
var pass = pass
operator fun component1(): String {
return this.name
}
operator fun component2(): String {
return this.pass
}
operator fun component3(): Int {
return this.age
}
}
fun main(args: Array<String>) {
val user = User("tom", "123", 20)
var (name, pass: kotlin.String) = user
}
如果只希望解构后面两个,可以使用_作为占位符
var (_,pass,age) = user
还记得遍历Map时候的一个语法吗?
for((key,value) in map){
...}
其原理是一样的:
operator fun<k,v> Map.Entry<k,v>.component1() = getKey()
operator fun<k,v> Map.Entry<k,v>.component2() = getValue()
即 var (key,value = Map.Entry对象)
Lambda表达式中解构:如果Lambda表达式的参数是支持解构的类型(比如Map.Entry),即可通过将它们放在括号中引入多个新参数来代替单个参数,例如如下的两种写法是一样的:
map.mapValues(entry->"${entry.value}")
使用解构:
map.mapValues((key,value)->"${value}")
注意:Lambda表达式的多个参数是不需要使用圆括号的,只要看到在Lambda表达式的形参列表中出现圆括号,那就是使用了解构。
8、数据类
数据类专门用于封装数据。
数据类除了使用data修饰外,还要满足:
-
主构造器至少需要一个参数
-
主构造器的所有参数需要有val或var声明为属性
-
不能用abstract、open、sealed修饰,也不能成为内部类
-
在kotlin1.1之前,数据类只能实现接口,现在数据类也可以继承其他类了
data class person(var name: String, var pass: String, val age: Int)
定义数据类之后,系统会自动为我们生成如下方法:
-
equals/hashcode
-
toString
-
componentN
-
copy方法,完成对象的复制
由于数据类支持解构,因此可以完成函数中一次返回多个值。
9、属性和字段
Kotlin的属性相当于Java字段(field - 幕后字段)再加上getter和setter(只读的属性没有setter方法),而且开发者不需要自己实现getter和setter方法。
Kotlin使用val定义只读属性,使用var定义读写属性,系统会为只读属性生成getter方法,会为读写属性生成getter和setter方法。
注意:在定义Kotlin的普通属性时,需要程序员显式的指定初始值:要么在定义时指定初始值,要么在构造器中指定初始值。
在Kotlin中定义一个属性,就相当于定义了一个Java类private修饰的字段,以及public final 修饰的getter和setter方法。
需要指出的是,虽然Kotlin为属性生成了getter和setter方法,但由于源程序中并未真正定义这些getter和setter方法,因此Kotlin程序不允许直接调用getter和setter方法。但是如果是Java程序,由于各属性对应的field都用了private修饰,因此只能用getter和setter方法访问属性。简单的概括就是:Kotlin访问只能使用点语法访问属性,而Java程序则只能使用getter和setter方法访问属性。
10、自定义getter(get(){})和setter(set(value){})方法
可以在get(){} 、set(value){}中添加自己的逻辑;
11、幕后字段
在Kotlin中定义一个普通属性时,Kotlin就会为该属性生成一个field、getter和setter方法。kotlin为该属性所生成的field就被成为幕后字段。
注意:如果类的属性有幕后字段,则必须进行显式的初始化;如果类的属性没有幕后字段,则Kotlin不允许为该属性指定初始值。
生成幕后字段的条件:
-
对于只读属性,必须重写setter方法。对于读写属性,必须重写getter和setter方法。否则,就总会生成幕后字段;
或
-
重写getter、setter方法时,使用field显式引用了幕后字段,则也会生成幕后字段;
12、幕后属性
用private修饰的属性就是幕后属性,Kotlin不会为幕后属性生成任何的getter和setter方法。因此,程序不能直接访问幕后属性,必须由开发者为幕后属性提供getter和setter方法
class Person(var name: String) {
private var _name: String = name
var name1
get() = _name
set(value) {
_name = value
}
}
_name 属性是幕后属性,没有getter和setter方法,因此程序无法直接访问Person对象的_name属性。
接下来程序定义了一个name1属性,并重写了name1属性的getter和setter方法,重写的getter和setter方法实际上访问的是_name幕后属性。
13、延迟初始化属性
Kotlin要求所有属性必须由程序员显式初始化 - 要么在定义该属性的时候赋初始值,要么在构造器中对该属性进行赋初始值。
Kotlin提供了lateinit修饰符来解决属性的延迟初始化。使用lateinit修饰的属性,可以在定义该属性时和在构造器中不指定初始值。
对lateinit修饰符有以下限制:
-
只能修饰可变属性(var)
-
lateinit修饰的属性不能自定义getter和setter方法
-
必须是非空类型
-
不能是原生类型(即Java的8种基本数据类型)
与Java不同的是,Kotlin不会为属性执行默认初始化。
14、内联属性 - 与内联方法一个意思,不过此处修饰的是属性或属性的getter或setter方法
15、访问控制符
private / public / internal /protected(类或文件内部、子类,可访问)
与Java的区别有如下的几点:
-
kotlin取消了Java默认的访问权限(包访问权限),引入了internal访问控制符(模块访问权限)
-
Kotlin取消了protected的包访问权限
-
Kotlin的默认访问控制符是public
何为模块?模块是编译在一起的一套Kotlin文件,模块的存在形式可以是一个项目,也可以是一次kotlinc执行所编译的一套文件。
不同作用域的成员可支持的访问控制符
==》顶层成员:顶层类、接口、函数、属性
-
public 或不加修饰符,可在任意地方被访问;
-
使用private修饰,只能在当前文件中被访问;
-
使用internal修饰,只能在当前文件或当前模块中被访问;
注意:直接定义在包内的顶层函数、属性,会转换成Kotlin所生成的类(类名为文件名 + Kt后缀的类)中的静态方法和静态属性。
package lee;
private fun foo(){}
public var bar:Int = 5
internal val baz:Int = 6
==》位于类、接口之内的成员
能使用:private / public / internal /protected其中之一
不加修饰符,默认为public
注意:类的主构造器比较特殊,如果需要为主构造器指定访问权限修饰符,则一定要使用constructor关键字,并在该关键字前面添加private / public / internal /protected其中之一。如果不添加修饰符,默认为public。
==》局部声明
与Java相同,局部声明(局部变量、局部方法、局部嵌套类)的作用域仅在该方法(或函数)内有效,因此访问控制符是没有任何意义的,因此不能使用private / public / internal /protected修饰符。
16、主构造器和初始化块
Kotlin类可以定义0~1个主构造器和0~N个次构造器。如果主构造器没有任何注解或可见性修饰符,则可以省略constructor关键字。
主构造器可以声明形参,但自己没有执行体,主构造器的形参有什么用呢?
-
初始化块可以使用主构造器定义的形参;
-
在声明属性时可以使用主构造器定义的形参;
初始化块:
init{
//初始化块中的可执行代码,可以使用主构造器定义的参数
}
class Person(name:String){
var name:String
init{
this,name = name//属性的初始值也可以通过初始化块指定
println(name)
}}
Person("Tom")结果打印了Tom
==>两者的关系:初始化块是在创建对象时隐式执行的,程序调用主构造器创建对象,实际上就是执行初始化块。由此可见,主构造器的主要作用是为初始化块定义参数,因此主构造器更像是初始化块的一部分。也可以说是,初始化块就是主构造器的执行体。
17、次构造器和构造器重载
Kotlin允许使用constructor关键字定义N个次构造器,次构造器类似传统的Java构造器。注:Java的初始化块其实是假象,其会自动插入到每个构造器的前面执行。
注意:Kotlin要求所有的次构造器必须先调用主构造器,然后才执行次构造器的代码
同一个类中具有多个构造器,多个构造器的形参列表不同,则为构造器重载。如果多个构造器中有相同的代码,则可以把它们放到初始化块中定义。如果这些初始化块需要参数,则可将参数放在主构造器中定义。这样可以提高复用性。
示例:
class ConstructorOverload{
var name:String?
var count:Int
init{
println("初始化块")
}
constructor(){
name = null
count = 0
}
constructor(name:String?,count:Int){
this.name = name
this.count = count
}
}
ConstructorOverload()
ConstructorOverload("tom",5)
==》结果:
初始化块
初始化块
初始化块总会在所有的次构造器之前执行,即次构造器都要委托调用初始化块
上面的类没有定义主构造器,因此次构造器不需要委托主构造器
看如下示例:
class User(name:String){
var name:String?
var age:Int
var info:String?
init{
println("User的初始化块")
this.name = name
this.age = 0。3
}
constructor(name:String,age:Int):this(name){
this.age = age
}
constructor(name:String,age:Int,info:String):this(name,age){
this.info=info
}
}
Kotlin使用“:this(参数)”的语法委托另一个构造器。
18、主构造器声明属性
Kotlin允许在主构造器上声明属性,直接在参数之前使用var 或val即可声明属性。当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性。
class Item(val code:String,var price:Double){
}
var im = Item("123",3.7)
println(im.code)//123
println(im.price)//3.7
子类主构造器必须委托调用父类的主构造器;
子类的次构造器必须先调用主构造器,然后才执行次构造器中的代码;
19、类的继承(单继承,不支持多继承)
修饰符 class SubClass :SuperClass{
//类定义部分
}
如果不写SuperClass,则默认继承Any类,Any类是所有类的父类,要么是其直接父类,要么是其间接父类。
注意:
Any类不是Java中的Object类,Any类只有equals、hashcode、toString这三个方法。
Kotlin的类,默认就有final修饰,因此Kotlin的类默认是不能派生子类的。为了让一个类能够派生子类,需要使用open修饰该类。
19.1、Kotlin与Java的设计相同:所有子类构造器必须调用父构造器一次。
-
子类的主构造器
如果子类定义了主构造器,由于主构造器属于类头部分,为了让主构造器能调用父类构造器,因此主构造器必须在继承父构造器的同时委托调用父类构造器。
open class BaseClass{
var name:String
constructor(name:String){
this.name = name
}}
class SubClass :BaseClass("foo"){}
或
class SubClass(name:String) :BaseClass(name){}
-
子类的次构造器
次构造器同样需要委托调用父类构造器。
如果子类定义了主构造器,由于子类的次构造器总会委托调用子类的主构造器(直接或间接),而主构造器一定会委托调用父类构造器,因此子类的所有次构造器最终也调用了父类构造器。
如果子类没有定义主构造器,则此时次构造器委托调用父类构造器可以分为三种方式:
1)子类构造器显式使用:this(参数)显式调用本类中重载的构造器。调用本类中的另一个构造器最终还是要调用父类构造器;
2)子类构造器显式使用:super(参数)委托调用父类构造器;
3)子类既没有:this也没有:super,系统将会在执行子类构造器之前,隐式调用父类的无参构造器;
open class Base{
constructor(){
}
constructor(name:String){
}}
class Sub : Base{
contructor(){//3
}
contructor(name:String):super(name){//2
}
contructor(name:String):this(name){//1
}}
当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,在执行父类构造器的时候,系统会再次上溯执行其父类构造器.....以此类推,创建任何Kotlin对象,最先执行的总是Any类的构造器。
19.2、重写父类的方法/属性
Kotlin默认为所有的方法添加final修饰符,阻止该方法被重写,添加open关键字用于阻止Kotlin自动添加final修饰符。
open class Bird{
open fun fly(){
}}
class Ostrich:Bird(){
override fun fly(){
}}
方法重写遵从“两同两小一大”
-
两同:函数名、参数列表相同;
-
两小:子类方法的返回值类型应比父类方法的返回值类型更小或相等,子类方法声明抛出的异常类应比父类声明抛出的异常类更小或相等;
-
一大:子类方法的访问权限要比父类方法的访问权限更大或相等;
重写父类的属性与重写父类的方法大致相似:父类被重写的属性必须使用open修饰,子类重写的属性必须使用override修饰。
重写属性有如下限制:
-
重写的子类属性的类型要与父类属性的类型要兼容;
-
访问权限方面:子类属性的访问权限要比父类属性的访问权限要大或相等;
-
只读属性可以被读写属性重写,但是读写属性不能被只读属性重写;
open class item{
open proteced var price =10.9
open val name:String =""
open var count:Int= 0
}
class SubItem:Item{
override public var price =10.9 // 正确,上述第二条
override var name:String =""//正确l上述第三条
override val count:Int= 0 //错误,上述第三条
}
注意:接口中的方法默认有open修饰符。
20、多态
Kotlin的变量也有两种类型:编译时类型和运行时类型。如果编译时类型和运行时类型不一致就出现了多态。
编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
open class BaseClass{
open fun test(){}
}
class SubClass:BaseClass(){
override fun test(){}
fun sub(){}
}
var c:BaseClass = SubClass()
c.test()//正确,访问子类
c.sub()//编译时错误,虽然c引用的对象是SubClass类型,也包含了sub()方法,但是c的编译时类型是Base Class,因此在编译时是无法调用sub()方法的。
21、is类型检查
is:判断前面的变量是否引用了后面的类,或者是其子类、实现类的实例。
is运算符前面的操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译时程序就会报错。
is运算符在进行强制类型转换之前,首先判断前一个变量是否引用后一个类或者其子类的实例,是否可以转换成功。
var hello:Any = "Hello"
println(hello is String)//true,hello变量的实际类型是String
println(hello is Date)//false,Any和Date存在继承关系
var a:String="hello"
println(a is Date)//报错,因此String与Date不存在继承关系
Kotlin的is运算符很智能,只要程序使用了is进行了判断,系统就会自动将变量的类型转换为目标类型。
var a:Any ="fkit"
println(a.length)//错误,因为a的编译时是Any类型,没有length属性
if(a is String) println(a.length)//正确