Kotlin中没有static关键字。如果你需要像Java中的静态函数那样方便调用的函数,Kolin官方推荐使用定义在顶层位置的函数。
3 类和对象
3.1 定义类
Kotlin中使用关键字class声明类,如class Invoice
类声明由类名、类头(类头包括类型参数(即泛型)、主构造函数等)和类体(即{…}部分)构成。类头和类体都是可选的。
主构造函数
一个类可以有一个主构造函数和多个次构造函数。
主构造函数是类头的一部分,它跟在类名和可选的类型参数之后:
class Person constructor(firstName: String) {
}
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字:
class Person(firstName: String) {
}
如果主构造函数有注解或可见性修饰符,则不能省略constructor关键字:
class Customer public @Inject constructor(name: String) {...}
主构造函数不能包含任何的代码。初始化工作可以放到以init开头的初始化块(initializer blocks)中进行:
class Customer(name: String) {
init {
初始化工作...
//在初始化块中可以使用主构造函数的参数
}
}
在类的属性的初始化器(initializer)中也可以使用主构造函数的参数:
class Customer(name: String) {
val customerKey = name //这就是所谓的“初始化器”
}
//对比:在java中只能这样
class Customer{
String customerKey;
Customer(String name){
this.customerKey = name;
}
}
☆实际上,声明属性并用主构造函数的参数来初始化属性有更简单的写法(常用):
class Customer(val customerKey: String) {
}
与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。
次构造函数
也可以通过关键字constructor来声明次构造函数,次构造函数的参数之前不允许有var或val:
class Person {
constructor(parent: Person) {
...
}
}
如果类有主构造函数,则每个次构造函数必须委托到主构造函数(即调用主构造函数),可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数使用this关键字即可:
class Person(val name: String) {
constructor(name: String, parent: Person) : this(name) {
...
}
}
如果一个类没有声明任何主或次构造函数,则会自动生成一个无参数的主构造函数,此构造函数的默认可见性为public。如果不希望在外部创建此类的对象(比如写单列模式的时候),则随便声明一个private的构造函数即可,一般是声明一个空参数的主构造函数:
class DontCreateMe private constructor () {
}
抽象类
类中的成员都可以声明为抽象(abstract)的,抽象的成员函数没有函数体,抽象的成员变量不能进行初始化(在《01开始.md》中的“1.1.4 定义变量”中提到过这一点)。含有抽象成员的类也是抽象的,抽象类不能创建对象。
注意,尽管抽象类不能创建对象,但抽象类是有构造函数的。
3.2 创建对象
Kotlin中没有new关键字。创建对象的格式非常简单,就像调用函数一样:
val invoice = Invoice()
val customer = Customer("Joe Smith")
3.3 类的成员
类可以包含:
- 构造函数和初始化块
- 方法(即成员函数)
- 属性(即成员变量)
- 嵌套类和内部类
- 对象声明
3.4 继承
3.4.1 继承的格式
默认情况下,Kotlin中所有的类都是final的(抽象类默认是open的),即不允许被继承,若要让其可以被继承,需要将其声明为“open”。
在Kotlin中所有类都有一个共同的超类Any,类似于Java中类Object的地位。
要让一个类继承类A,只要在类名后跟上“:A”即可:
(1) 如果派生类有主构造函数,则必须就地调用基类的构造函数(基类名后直接跟(…)):
open class Base //没有定义任何构造函数,则会自动生成一个空参数的主构造函数
//派生类有主构造函数,则必须就地调用基类的构造函数(基类名后直接跟(...))
class Derived1(val name: String) : Base() {
fun printName() {
print(name)
}
}
(2) 如果派生类没有主构造函数,则必须在所有次构造函数后面调用基类的构造函数(使用super(…)),或委托给另一个构造函数做到这一点:
//派生类没有主构造函数,则必须在所有次构造函数后面调用基类的构造函数(使用super(...))
class Derived2:Base{
constructor():super()
constructor(str:String):super()
constructor(i:Int):this()
}
3.4.2 覆写方法和属性
Java中只有方法有覆写的概念,而Kotlin中方法和属性都是可以覆写的。默认情况下,Kotlin类中的方法和属性都是final的(抽象的方法和属性默认是open的),即不允许被子类覆写,要让其可以被子类覆写,需要显式的将其声明为open。而子类中覆写的方法和属性必须显式的声明为override。
open class Base {
open fun v() {} //允许覆写
fun nv() {} //不允许覆写
}
class Derived : Base() {
override fun v() {} //覆写父类的v()方法
}
标记为override的成员本身是开放的,也就是说,它可以在子类中被覆写。如果你想禁止再次覆写,使用final关键字:
open class AnotherDerived : Base() {
final override fun v() {}
}
对于属性,var可以覆写val,但val不能覆写var:
open class Father {
open val x: Int = 1
}
class Son : Father() {
override var x: Int = 2
}
可以在主构造函数中使用override关键字作为属性声明的一部分,如class Son(override val count: Int) : Father(){...}
3.5 属性
当我们定义了一个顶层变量或成员变量,系统会自动为其生成访问器(getter、setter)。
带有访问器的变量称为属性。顶层变量和成员变量都有访问器,而局部变量没有,因此顶层变量和成员变量都是属性,而局部变量不是。
当我们定义了一个属性(即一个顶层变量或成员变量),我们所看到的变量其实只是一个名字而已,本质上它有两个组成部分:访问器(getter、setter)和幕后字段。幕后字段不是必须的。
可以这样理解:getter和setter才是属性的本体,因为属性可以没有幕后字段(后面会提到)。
对比Java:在Java中,属性 = 成员变量 = 字段。
定义和使用属性的一般格式:
class Address {
var name: String = ……
var street: String = ……
var city: String = ……
var state: String? = ……
}
fun copyAddress(address: Address): Address {
val result = Address()
result.name = address.name
result.street = address.street
...
return result
}
定义属性的完整格式:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
方括号中的部分都不是必须的。在上面Address类的例子中,系统为name、street等属性生成了隐含的默认getter和setter。
也可以自己定义setter和getter:
class Person {
var name: String = "Lisi"
get() {
return field //field的含义后面会说
}
set(value) {
field = value
}
}
我们对属性的任何访问操作,都是通过其getter和setter来完成的:当我们读取属性的值时,会调用其getter,当我们向属性写入值时,会调用其setter:
val person = Person()
val name = person.name //会通过name属性的getter来读取属性值
person.name = "LiSi" //会通过name属性的setter来写入值
幕后字段(backing field)
我们希望对name属性的getter和setter动一点手脚,比如像下面这样:
class Person {
var name: String = "ZhangSan"
get() {
return name + "哈哈哈"
}
set(value) {
name = value + "呵呵呵"
}
}
当用户读取name时,我们返回给访问者的并不是name的真实值,而是加了”哈哈哈”后缀;当用户往name写入值时,我们写入的也不是用户给的值,而是在用户给的值后拼接了”呵呵呵”。看起来只是一个简单的恶作剧,似乎没什么的大问题。
然后当程序运行起来之后,你会发现,对name的任何读取操作都会导致getter的无限递归调用,最终引发栈溢出和程序崩溃;对name的任何写入操作都会导致setter的无限递归调用,最终引发栈溢出和程序崩溃。为什么会这样呢?答案显而易见,上面已经说过了:对属性的任何读取操作都会调用其getter,对属性的任何写入操作都会调用其setter,在getter和setter内部读写属性也不例外。
那么在getter和setter中该如何读写属性(而不引起getter和setter的递归调用)呢?答案就是使用幕后字段(backing field)。在getter和setter中使用“field”来表示幕后字段。那么上面的那个恶作剧就应该写成这样:
class Person {
var name: String = "ZhangSan"
get() {
return field + "哈哈哈"
}
set(value) {
field = value + "呵呵呵"
}
}
幕后字段是真正存储属性的值的地方。之所以叫做“幕后”,是因为该字段是隐藏在属性内部或者说是“背后”的,属性的使用者只知道自己在读写属性,并不会感知到该字段的存在,因此叫做“幕后字段”。
一个属性也可以没有幕后字段。当我们显式定义了getter和setter,并且在getter和setter中都没有用到幕后字段时,则这个属性就是没有幕后字段的。不能对没有幕后字段的属性进行初始化(因为没有存储值的地方了):
class Person {
//没有幕后字段,因此不能对name进行初始化
var name: String // = "ZhangSan"
get() {
return "haha"
}
set(value) {
Log.e("Log","do nothing")
}
}
3.6 接口
接口中的属性和方法默认都是抽象的,可以省略abstract关键字。
与Java不同的是,Kotlin的接口中还允许存在已实现的方法(即非抽象的方法)。Kotin的接口与抽象类的区别在于,接口中只允许存在抽象属性:
interface C{
var a:Int//抽象属性
var b:Int = 1//报错,不允许有非抽象的属性
fun aaa()//抽象方法
fun bbb(){//可以有非抽象的方法
}
}
与类和抽象类不同,接口中没有也不能有构造函数,因此,当子类实现接口时自然也就不需要去调用接口的构造函数了:
interface MyInterface {
fun bar()
fun foo() {
...
}
}
class Child : MyInterface {//实现接口
override fun bar() {
...
}
}
单继承、多实现以及避免歧义
和Java一样,Kotlin中的类也只能继承一个父类,但是可以实现多个接口,即“单继承、多实现”。
open class A {
open fun f() {
print("A")
}
fun a() {
print("a")
}
}
interface B {
fun f() {
print("B")
}
fun b() {
print("b")
}
}
interface C {
fun f() {
print("C")
}
fun c() {
print("c")
}
}
class Z: A(),B,C {
//☆注意:为了避免歧义,编译器要求必须覆写f()
override fun f() {
super<A>.f() // 调用 A.f()
super<B>.f() // 调用 B.f()
super<C>.f() // 调用 C.f()
}
}
3.7 扩展类
Kotlin能够为一个类扩展新功能而无需继承该类或使用装饰者模式。Kotlin支持扩展函数和扩展属性,即动态的为一个类添加函数或属性。
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员,而仅仅是可以通过该类型的对象去访问扩展的函数或属性而已。
3.7.1 扩展函数
声明一个扩展函数,我们只需以接收者类型(也就是被扩展的类型)作为函数的前缀即可。例如,下面代码为MutableList添加了一个swap函数:
//声明扩展函数
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] //this指接收者对象,即MutableList<Int>对象
this[index1] = this[index2]
this[index2] = tmp
}
//然后就可以在MutableList<Int>对象上调用swap函数了:
val list = mutableListOf(1, 2, 3)
list.swap(0, 2)
我们还可以声明一个泛化的扩展函数(参考函数泛型相关章节):
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
扩展是静态解析的
调用的扩展函数是由函数调用所在的表达式的类型来决定的,而不是由表达式在运行时的求值结果决定的。例如:
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
//实际输出的是c而不是d。因为调用的扩展函数只取决于参数c的声明类型,该类型是C。
printFoo(D())
可空接收者
可以为可空的接收者类型定义扩展函数。可以在扩展函数体内检测接收者是否为null:
fun Any?.toString(): String {
if (this == null) return "null"
return toString()
}
3.7.2 扩展属性
声明一个扩展属性,我们只需以接收者类型(也就是被扩展的类型)作为属性的前缀即可。
val Man.gender:String
get() = "male"
需要注意的是,扩展属性都是没有幕后字段的,因此不能进行初始化,例如val Man.gender = "male"
就是错误的。扩展属性的行为只能由显式提供的getters/setters定义。
3.7.3 声明扩展的位置
在顶层位置声明扩展
大多数时候我们都在顶层定义扩展,即直接在包里:
package foo.bar
//为Baz类添加一个goo()函数
fun Baz.goo() {
...
}
如果要在另一个包的文件中使用扩展,我们需要先导入它:
package com.example.usage
import foo.bar.goo
//或者import foo.bar.*
fun usage(baz: Baz) {
baz.goo()
)
在类中声明扩展
在一个类内部你可以为另一个类声明扩展。扩展声明所在的类的实例称为分发接收者,被扩展的类的实例称为扩展接收者。在扩展函数内部可以直接访问分发接收者和扩展接收者的成员:
class D {
fun bar() { …… }
}
class C {
fun baz() { …… }
fun D.foo() {
bar() //调用D.bar
baz() //调用C.baz
}
fun caller(d: D) {
d.foo() //调用扩展函数
}
}
对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者优先。要访问分发接收者的成员你可以使用带标签的this:
class C {
fun D.foo() {
toString() // 调用D.toString()
this@C.toString() // 调用C.toString()
}
}
以下内容仅了解:
在类中声明的扩展可以声明为open并在子类中覆写。扩展函数的分发对于分发接收者类型是虚拟的(即动态的),对于扩展接收者类型是静态的:
package cn.szx.kotlindemo
open class D {}
class D1 : D() {}
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() //调用扩展函数
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
fun test() {
C().caller(D()) // 输出 "D.foo in C"
C1().caller(D()) // 输出 "D.foo in C1" —— 分发接收者虚拟解析(分发接收着即C还是C1是在运行时决定的)
C().caller(D1()) // 输出 "D.foo in C" —— 扩展接收者静态解析(扩展接收者即D还是D1是在运行之前决定的)
}