本文将伴随大家进入Kotlin语言的正式学习生涯中,希望大家不要半途而废哦!笔者将Kotlin用于Android开发中,因此将从Android开发的视角叙述相关内容,同时将与Java语言有所联系。
1. 继承
在 Kotlin 中所有类都有一个共同的超类Any
,这对于没有超类型声明的类是默认超类:
1 | class Example // 从 Any 隐式继承 |
Any
有三个方法:equals()
、 hashCode()
与 toString()
。因此,为所有 Kotlin 类都定义了这些方法。
如需声明一个显式的超类型,需要使用open
关键字修饰超类型,派生类头中把超类型放到冒号之后:
1 | open class Base(p: Int) |
如果派生类有一个主构造函数,其基类必须用派生类主构造函数的参数就地初始化。如果派生类没有主构造函数,那么每个次构造函数必须使用super
关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
1 | class MyView : View { |
1.1 覆盖方法
Kotlin 对于可覆盖的成员(我们称之为开放)以及覆盖后的成员需要显式修饰符:
1 | open class Shape { |
Circle.draw()
函数上必须加上override
修饰符。如果没写,编译器将会报错。 如果函数没有标注open
如Shape.fill()
,那么子类中不允许定义相同签名的函数, 不论加不加override
。将open
修饰符添加到final
类(即没有 open
的类)的成员上不起作用。
标记为override
的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用final
关键字:
1 | open class Rectangle() : Shape() { |
1.2 覆盖属性
属性覆盖与方法覆盖类似;在超类中声明并用open
修饰,然后在派生类中重新声明的属性必须以override
开头,并且它们必须具有兼容的类型。 每个声明的属性可以由具有初始化器的属性或者具有get
方法的属性覆盖。
1 | open class Shape { |
你也可以用一个var
属性覆盖一个val
属性,但反之则不行。 这是允许的,因为一个val
属性本质上声明了一个get
方法, 而将其覆盖为var
只是在子类中额外声明一个set
方法。
请注意,你可以在主构造函数中使用override
关键字作为属性声明的一部分。
1 | interface Shape { |
1.3 派生类初始化顺序
在构造派生类的新实例的过程中,第一步完成其基类的初始化(在之前只有对基类构造函数参数的求值),因此发生在派生类的初始化逻辑运行之前。
1 | open class Base(val name: String) { |
执行代码Constructing Derived("hello", "world")
,输出结果为:
1 | Argument for Base: Hello |
这意味着,基类构造函数执行时,派生类中声明或覆盖的属性都还没有初始化。如果在基类初始化逻辑中(直接或通过另一个覆盖的open
成员的实现间接)使用了任何一个这种属性,那么都可能导致不正确的行为或运行时故障。设计一个基类时,应该避免在构造函数、属性初始化器以及init
块中使用open
成员。
1.4 调用超类实现
派生类中的代码可以使用super
关键字调用其超类的函数与属性访问器的实现:
1 | open class Rectangle { |
在一个内部类中访问外部类的超类,可以通过由外部类名限定的super
关键字来实现:super@Outer
:
1 | class FilledRectangle: Rectangle() { |
1.5 覆盖规则
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的super
,如super<Base>
:
1 | open class Rectangle { |
可以同时继承Rectangle
与Polygon
, 但是二者都有各自的draw()
实现,所以我们必须在Square
中覆盖draw()
, 并提供其自身的实现以消除歧义。
1.6 抽象类
类以及其中的某些成员可以声明为abstract
。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用open
标注一个抽象类或者函数——因为这不言而喻。
我们可以用一个抽象成员覆盖一个非抽象的开放成员。
1 | open class Polygon { |
2. 接口
Kotlin 的接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
使用关键字interface
来定义接口。
1 | interface MyInterface { |
2.1 实现接口
一个类或者对象可以实现一个或多个接口。
1 | class Child : MyInterface { |
接口的实现方式与继承相似,并且Kotlin的接口可以有属性和实现的方法,可以达到类似于多继承的效果。
2.2 接口中的属性
你可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段(backing field
),因此接口中声明的访问器不能引用它们。
1 | interface MyInterface { |
2.3 接口继承
一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现:
1 | interface Named { |
2.4 解决覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题。例如
1 | interface A { |
上例中,接口 A 和 B 都定义了方法foo()
和bar()
。 两者都实现了foo()
, 但是只有 B 实现了bar()
(bar()
在 A 中没有标记为抽象, 因为没有方法体时默认为抽象)。因为 C 是一个实现了 A 的具体类,所以必须要重写bar()
并实现这个抽象方法。
然而,如果我们从 A 和 B 派生 D,我们需要实现我们从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现(bar()
)的方法也适用于继承多个实现(foo()
)的方法。
3. 扩展
Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做扩展
的特殊声明完成。 例如,你可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。 这种机制称为扩展函数
。此外,也有扩展属性
, 允许你为一个已经存在的类添加新的属性。
3.1 扩展函数
声明一个扩展函数,我们需要用一个接收者类型
也就是被扩展的类型来作为他的前缀。 下面代码为MutableList<Int>
添加一个swap
函数:
1 | fun MutableList<Int>.swap(index1: Int, index2: Int) { |
这个this
关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意MutableList<Int>
调用该函数了:
1 | val list = mutableListOf(1, 2, 3) |
当然,这个函数对任何 MutableList
1 | fun <T> MutableList<T>.swap(index1: Int, index2: Int) { |
为了在接收者类型表达式中使用泛型,我们要在函数名前声明泛型参数。
3.2 扩展是静态解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如:
1 | open class Shape |
这个例子会输出 “Shape”,因为调用的扩展函数只取决于参数s
的声明类型,该类型是Shape
类。
如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、 相同的名字,并且都适用给定的参数,这种情况总是取成员函数。 例如:
1 | class Example { |
这段代码输出“Class method”。
当然,扩展函数重载同样名字但不同签名成员函数也完全可以:
1 | class Example { |
这段代码输出“Extension function”。
3.3 可空接收者
注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为null
,并且可以在函数体内检测this == null
,这能让你在没有检测null
的时候调用 Kotlin 中的toString()
:检测发生在扩展函数的内部。
1 | fun Any?.toString(): String { |
3.4 扩展属性
与函数类似,Kotlin 支持扩展属性:
1 | val <T> List<T>.lastIndex: Int |
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的getters/setters
定义。例如:
1 | val House.number = 1 // 错误:扩展属性不能有初始化器 |
3.5 伴生对象的扩展
如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样, 可以只使用类名作为限定符来调用伴生对象的扩展成员:
1 | class MyClass { |
3.6 扩展的作用域
大多数时候我们在顶层定义扩展——直接在包里:
1 | package org.example.declarations |
要使用所定义包之外的一个扩展,我们需要在调用方导入它:
1 | package org.example.usage |
3.7 扩展声明为成员
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个隐式接收者
—— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为分发接收者
,扩展方法调用所在的接收者类型的实例称为扩展接收者
。
1 | class Host(val hostname: String) { |
对于分发接收者与扩展接收者的成员名字冲突的情况,扩展接收者优先。要引用分发接收者的成员你可以使用限定的this
语法。
1 | class Connection { |
声明为成员的扩展可以声明为open
并在子类中覆盖。这意味着这些函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。
1 | open class Base { } |
输出结果为:
1 | Base extension function in BaseCaller |