一、 类
1. 可以声明一个空类
class Empty
java 中即使是空类,也需要写类体
2. 构造器
关键字constructor,当主构造函数没有任何注解或者可见性修饰符,可以省略。
class Person constructor(name: String, age: Int)
等价于
class Person(name: String, age: Int)
3. 主构造函数与次构造函数
主构造函数:定义在类头中的构造函数。
次构造函数:定义在类体中的构造函数。
在 Kotlin 中,类中可以声明主构造函数(零个或一个)和次构造函数(零个或多个)。次构造函数可以使用this关键字调用其他的构造函数,没有主构造函数也可以写次构造函数,这时次构造函数不需要委托给主构造函数(无主构造函数)。
l 主构造函数只能调用父类的构造函数
l 次构造函数可以调用其他次构造函数(次构造函数不能循环调用),但是如果存在主构造函数,最终需要调用主构造函数。
l 如果一个类中,没有主构造函数,但是有init初始化块和次构造函数,那么在调用这个类的次构造函数时,init初始化块会在次构造函数执行前执行。
在主构造函数中定义的属性,如果有var或val修饰,就相当于类的属性,可以在类内部使用,如果没有修饰,则只能在init初始化块和类体内声明的属性初始化器中使用。次构造函数中的参数不能用val与var修饰。
类头中声明的主构造函数中不能包含任何的代码,只能放在init初始化块中。
Q:与swift的区别?
l kotlin中只能有一个主构造器,多个次构造器。
l swift中可以有多个指定构造器,多个便利构造器。
Q:kotlin中为什么有主构造函数和次构造函数之分?
个人理解:在主构造函数中,可以声明并初始化类中全部的存储属性,可以简化类结构。而且次构造函数最终需要调用主构造函数,这样可以保证类中的存储属性都可以完成初始化。但如果次构造函数也可以声明成员属性,那么就不能保证所有的属性都可以初始化,类结构会比较混乱。所以构造函数需要有主次之分。
没有主构造函数,只有多个次构造函数的情况,是为了方便从java转到kotlin开发的程序员使用。
4. 可以使用无参方式调用构造函数初始化实例对象的条件:
下列条件满足一条即可:
l 类中没有任何一个构造函数(主构造函数、次构造函数)
l 主构造函数所有参数都有默认值。
l 有一个次构造函数所有参数都有默认值。
注意:
a) 只有主构造函数的参数全部都有默认值的情况下,编译器才会真正的生成一个无参构造方法,此时,通过无参方式调用构造函数创建实例对象时,会调用有默认值的有参构造函数,并不会调用额外生成的无参构造函数,这个额外的无参构造函数主要是给java代码中用的。
b) 次构造函数的参数都有默认值的时候,不会额外生成无参构造函数,只是在调用的时候,使用参数的默认值。
5. 利用可见性修饰符修饰主构造函数
默认可见为public,可以用private修饰 ---->引申,java中单例
Q:主构造函数用private修饰,并且无参数,当有一个次构造函数全部参数都有默认值时,会怎样?
A:在代码中可以使用无参构造函数创建对象。应该避免这么做!!!
二、 属性和字段
1. 声明属性
在kotlin中属性分为只读属性和可变属性两种,只读属性用关键字val声明,可变属性用关键字var 声明。
2. Getters 和 Setters
kotlin中可以给属性设置自定义的get和set访问器。多用于计算属性。
get与set可以任意设置,只有get、只有set或者都有。
l 当getter可以推断出属性类型时,可以省略类型声明。
l 在get与set内部,不可以用属性名本身执行语句(互相引用,导致栈溢出),如果想使用本身属性值时,需要用幕后字段field。
l 可以对get与set进行可见性修饰和加注解
a) getter必须与属性可见性一致
b) setter可以随意设置,但是不会超出类的可见性
get() = 可以使用函数执行结果赋值
set(value) = 后面可以加if、when、try/catch表达式
3. 幕后字段backing field
Kotlin 中类不能有字段。然而,当使用自定义访问器时,有时有一个幕后字段(backing field)有时是必要的。为此 Kotlin 提供一个自动幕后字段,它可通过使用 field 标识符访问。
field 标识符只能用在属性的访问器内。
如果属性至少一个访问器使用默认实现,或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段。
4. 幕后属性
个人理解:类似于幕后字段的手动实现,可控性强。
5. 编译期常量
已知值的属性可以使用 const 修饰符标记为编译期常量,可以使用在注解中。这些属性需要满足以下要求:
l 指定定义在顶层、 object或伴生对象中;
l 用 String 或原生类型 值初始化;
l 没有自定义 getter。
6. 延迟初始化属性
关键字lateinit,该修饰符只能用于在类体中(不是在主构造函数中)声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是原生类型。
在使用延迟初始化属性之前,必须要初始化,否则将会抛出异常。
使用延迟初始化属性,是因为有些情况下,在声明属性的时候不能确定该属性的初始化值,但是在后续的程序中,一定可以为其设置一个初始化值。lateinit修饰的属性,需要程序员保证非空!
使用场景:在Android中,需要声明页面的组件,在没有findViewById时,需要设置一个null初始化值,如果用lateinit修饰,则可以不设置null。
在后面会有一个延迟属性,只可以用val声明,一般用于声明一些初始化耗时的计算属性,只有在第一次访问时计算并将计算结果保存为属性值,再次访问时,会直接使用保存的值,不会再次计算。示例代码如下:
val lazyValue: String by lazy { println("computed!") "Hello" }
引申:声明属性时,默认初始化值在什么情况下可以不设置?
l 属性用abstract或lateinit修饰时。
l 声明在接口中的属性
l 扩展的属性
l 属性的所有自定义访问器都没有用过幕后字段field
7. 使用函数、匿名函数、lambda表达式给属性赋值
class SetValue {
val funReturnUnit = returnUnit() //用无返回值的函数给属性赋值
val funReturnValue = returnValue()//用有返回值的函数给属性赋值
val lambda = { "lambda表达式" } //用lambda表达式给属性赋值
val lambdaRun = { "运行的lambda表达式" }() //用lambda表达式执行结果给属性赋值
val anonymousFun = fun(): String { //用匿名函数给属性赋值
return "匿名函数"
}
val anonymousFunRun = fun(): String { //用匿名函数执行结果给属性赋值
return "执行的匿名函数"
}()
fun returnValue(): String {
return "有返回值的函数"
}
fun returnUnit() {}
}
fun main(args: Array<String>) {
val impl = SetValue()
println("用无返回值的函数给属性赋值: ${impl.funReturnUnit}")
println("用有返回值的函数给属性赋值: ${impl.funReturnValue}")
println("用lambda表达式给属性赋值: ${impl.lambda}")
println("用lambda表达式执行结果给属性赋值: ${impl.lambdaRun}")
println("用匿名函数给属性赋值: ${impl.anonymousFun}")
println("用匿名函数执行结果给属性赋值: ${impl.anonymousFunRun}")
}
程序执行结果如下:
用无返回值的函数给属性赋值: kotlin.Unit
用有返回值的函数给属性赋值: 有返回值的函数
用lambda表达式给属性赋值: () -> kotlin.String
用lambda表达式执行结果给属性赋值: 运行的lambda表达式
用匿名函数给属性赋值: () -> kotlin.String
用匿名函数执行结果给属性赋值: 执行的匿名函数
三、 继承
1. 继承
继承是强耦合的!
kotlin中所有类都隐式继承自Any。类的默认修饰符是final,如果想要可以被继承,那么需要显式声明为open。
如果子类中不存在主构造函数时,可以在子类的次构造函数中使用super关键字初始化其基类型,调用父类的构造函数。
2. 子类调用父类的构造方法
子类的构造函数最终要调用父类构造函数。
l 当子类中存在主构造函数或者不存在任何构造函数时,需要在类头初始化父类,父类后有括号。
l 当子类中只存在次构造函数时,需要在次构造函数后面用super调用父类的构造函数,这时,父类后无需括号。当父类可以使用无参方式调用构造函数初始化时,super关键字可以省略,这时,默认调用父类该构造函数。
区别:
Java中,构造函数没有主次之分,所以调用任何一个父类构造函数都可以。
swift中,一个指定构造器必须调用直接父类的指定构造器,一个便利构造器只能调用当前类的其他构造器,一个便利构造器必须最终调用一个指定构造器。
kotlin中,存在主构造函数时,与swift相同,当不存在主构造函数时,次构造函数可以调用父类中任何一个构造函数。
在初始化子类时,会按照当前类中构造方法的调用顺序,反向依次执行。
完整的执行顺序是:父类的主构造函数---->父类的次构造函数---->子类的主构造函数---->子类的次构造函数。
l 当子类不存在主构造函数时,次构造函数可以直接调用父类的构造函数。
l 当子类存在主构造函数时,只能由主构造函数调用父类的构造函数。
3. 覆盖函数
父类中方法默认修饰符是final,如果想要可以被子类重写,需要显式open,子类中重写时,需要加override关键字,Java中不加关键字也不会报错。
如果想要禁止再次被重写,那么需要显式加上final。
构造函数不能被重写。
4. 覆盖属性
与覆盖函数类似,父类中声明为open的属性,可以在子类中使用override关键字重写。
父类中的val属性可以在子类中重写为var,但是var属性不可以被重写为val的。
5. 覆盖规则
父类与接口中,有相同名字的函数,必须要在子类中重写,在重写的方法中使用super<超类型名>.functionName()来声明调用的是哪个超类型名中的函数。
当父类中与接口中同名的方法为final修饰时,在子类中无法重写同名方法,会报错。
当父类与接口有同名函数,并且接口中的函数没有默认实现时:
a) 不重写同名函数,会使用从父类中继承来的函数,作为接口函数的实现。
b) 如果重写,则可以使用super. functionName ()来调用父类函数。
kotlin中:如果接口同名函数有默认实现,那么必须在子类重写该方法,因为该子类继承了较多的实现。在生产过程中需要尽量避免该现象的产生。
6. 抽象类
l 抽象类与类中的函数不需要open修饰,就可以被继承。
l 可以用一个抽象成员覆盖一个非抽象成员。
7. 伴生对象:关键字companion
l 可省略伴生对象的名称,系统默认使用Companion作为伴生对象名称
l 可以继承类、实现接口
l 可以通过在伴生对象中创建外部类的实例来调用外部类的函数和属性。
当伴生对象继承了一个类的时候,那么该类的所有成员属性和成员方法,都可以用伴生对象外部类的点表达式静态调用。
Q:取消static而引入伴生对象的意义:
A:把所有的静态的属性和方法都集中到了一起,代码易读性高。
引申:单例模式的实现
a) 伴生对象 val c = AA.instance
b) Object val c = C
四、 接口
1. 接口中的函数
kotlin中接口的函数可以有默认的实现。
l 有默认实现的函数可以不在实现类中重写
l 有默认实现的函数,可以再实现类中的init块中、次构造函数中、成员函数中、给属性赋值时调用。
2. 接口中的属性
它可以有属性但必须声明为抽象或提供访问器实现。
l 抽象的属性:需要在实现类中重写
l 提供getter(可以加setter),可以用作计算属性
3. 解决覆盖冲突
与继承的覆盖规则相同,使用<超类型名>.functionName()。
五、 可见性修饰符
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 (getter 总是与属性有着相同的可见性。) 在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public。
l 如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见;
l private用于顶层声明时,于当前文件内可见;用于类中声明时,于当前类中可见;
l internal用于顶层声明时,它会在相同模块内随处可见;用于类中声明时,即使类的修饰符为public,用internal修饰的对象(成员属性、成员函数等),在其它模块也是不可见的;
l protected 本类与子类中可见,不适用于顶层声明。
可见性修饰符作用域如下:
作用域 |
其他Module |
当前Module |
当前文件 |
本类 |
子类 |
public |
√ |
√ |
√ |
√ |
|
internal |
|
√ |
√ |
√ |
√ |
private |
|
|
√ |
√ |
|
protected |
|
|
|
√ |
√ |
六、 扩展
kotlin中能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式,(仅)支持扩展函数和属性。
1. 扩展是静态解析的
静态解析:在编译时,就已经确定类型。
动态解析:在编译时不确定类型,在执行时才会确认到底是什么类型(比如:多态中的继承,父类中定义抽象方法,每个子类有不同的实现)。
按照调用的扩展函数所定义的接收参数类型决定,该调用哪一个类的扩展(父子类)。
2. 扩展函数
成员函数:被扩展的类原有的函数
l 当扩展函数与成员函数同名同参时,调用函数,执行成员函数。
l 扩展函数可以重载成员函数。
l 可以给子类和父类扩展相同名称的函数。
l 不可以扩展构造函数,扩展构造函数会改变原有的类。
l 扩展函数需要有函数体。
3. 扩展属性
由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义(只能扩展计算属性)。
不能扩展类中已有的属性。
4. 其他
对伴生对象、内部类、嵌套类、接口以及Java中定义的类和接口进行扩展。
l 对伴生对象、内部类、嵌套类的扩展方式:外部类.扩展类.方法或属性,伴生对象如果省略了类名,则用Companion代替。
l 对接口扩展,与普通扩展相同,扩展函数需要有函数体。
l 对java中定义的类与接口的扩展与在kotlin中相同。
5. 扩展的作用域
l 要使用所定义包之外的一个扩展,我们需要在调用方导入。
6. 扩展声明为成员
分发接收者:如果一个类内部有其他类的扩展,那么它的实例就是分发接收者。
扩展接收者:被扩展的类的实例。
注意:
a) 在顶层定义的扩展,可以在任意可以见到当前扩展的地方使用(可见性修饰符修饰)。
b) 在一个类中给其他类进行扩展,那么这些扩展,只能在当前类或者当前类的子类中使用,不可以在类外部使用。
Q1:给一个类扩展两个相同名称的函数会怎样?
a) 如果在顶层扩展,将会报错,不可以有这样的写法。
b) 如果扩展声明为成员,在父类与子类中分别扩展,可以override。
Q2:扩展一个与父类成员函数相同的函数会怎样?
无效果,因为子类继承了父类的成员函数,调用时会执行子类中的成员函数。
七、 数据类
在java中的Model类是把有关系的一组数据封装在一起的类文件,通常Model类中会只定义属性和get、set方法,而且每个Model类都是一个单独的文件。kotlin中对此进行了优化,将这些只保存数据的类定义为数据类并用data关键字修饰,一个文件中可以声明多个数据类。
编译器自动从主构造函数中声明的所有属性导出以下成员:
l equals()/hashCode() 对,
l toString() 格式是 "User(name=John, age=42)",
l componentN()函数 按声明顺序对应于所有属性(多用于解构声明),
l copy() 函数,复制出克隆对象,可以在复制的时候,对保存的属性进行修改
数据类可继承类与实现接口。
可以在数据类中定义自己的方法,可扩展。
八、 密封类
相当于枚举类型的扩展,但可以比枚举类型做更多的工作。
密封类的子类只能与密封类定义在同一个文件内,但继承密封类子类的类(间接继承者)可以放在任何位置,不仅限于同文件内。
密封类不能实例化,但密封类的子类可以。
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了