本篇内容清单如下:
- 类:声明方式、实例创建、类成员、构造函数(1 主、n 次)
- 继承:覆盖方法、覆盖属性、类初始化顺序
- 属性:幕后字段、幕后属性
- 接口
- 可见修饰符
- 扩展:扩展函数、扩展属性、伴生对象
- 泛型
- 类的形式:伴生对象、对象表达式与对象声明、抽象类、数据类、嵌套类、内部类、匿名内部类、枚举类、匿名类、内联类
- 委托:属性委托(动态绑定 & 静态绑定)
- 委托属性(没太懂)
一共 16 点内容,原文档中更细,为重点篇章,初学有很多陌生概念。有些可能也不是太理解,不知使用场景,如 函数式(SAM)接口;有些新知识点用起来会弥补 Java 不能实现的一些缺憾,感觉非常不错,如 扩展。
一、类与继承
1. 类
1.1 类声明
- 使用关键字
class
声明。 - 类声明 = 类名 + 类头(指定其类型参数、主构造函数等)[可选]+ 花括号包围的类体[可选]
1.2 构造函数
- 一个 主构造函数:类头的一部分,跟在类名后面。
- 多个 次构造函数:同一般函数,属于 类体 的一部分。
主构造函数
- 使用
constructor
关键字,若主构造函数没有 注解或可见性修饰符 ,则可以省略constructor
。 - 一个类仅一个。
- 不能够包含任何代码。
- 初始化代码可放在
init
关键字为前缀的 初始化块 中;
// 主构造函数语法
class Person(name: String) { ... }
// 访问主构造的参数
class Person2(name: String) {
// 1. 在 类体 内声明的 属性初始化器 中使用 主构造的参数
val introduction = "My name is $name"
// 2. 在 init 初始化块 中使用 主构造的参数
init {
println("name = $name")
}
}
// 简洁语法:声明属性 && 初始化主构造函数的属性
class Person3(val name: String, var age: Int) { ... }
次构造函数
- 类若有一个主构造函数,则每个 次构造函数,必须 委托给 主构造函数:可直接委托,或间接通过其他次构造函数委托。
- 委托使用
this
关键字。
class Person(val name: String) {
val children: MutableList = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.add(this)
}
}
Tips:
-
init
初始化块中的代码,实际上会是 主构造函数的一部分所以,若主构造函数被调用,则init
初始化块 与 属性初始化器 都会一起被执行。所以即使没有主构造,次构造函数这种委托仍会隐式发生,仍会执行初始化块。 - 若一个 非抽象类 没有任何 构造函数,均会有一个生产的 不带参数的主构造函数。
- 若希望类的构造函数为私有,则使用
private
修饰 主构造函数。
1.3 创建类的实例
- Kotlin 没有
new
关键字。 - 创建一个类的实例,就像 普通函数 一样调用 构造函数。
val person = Person("Coral")
val demo = Demo()
1.4 类成员
类 可以包括:
- 构造函数 与 初始化块
- 函数
- 属性
- 嵌套类与内部类 [1]
- 对象声明 [2]
2. 继承
- Kotlin 中所有类都有一个共同的超类
Any
,等同于 Java Object 。 -
Any
有三个方法:equals()、hashCode() 与 toString() 。 - 默认情况下,Kotlin 类是
final
的,不能被继承,要使类可继承,使用open
关键字标记。 - 若需声明一个显式的超类类型,则为
class Derived(p: Int) : Base(p)
。 - 若子类有一个 主构造函数,其 父类,必须用 子类的主构造函数的参数 就地初始化。
- 若子类没有 主构造函数,则子类每个 次构造函数 必须使用
super
关键字 初始化其 父类类型 或 委托给另一个构造函数。 如:class MyView : View { constructor(ctx: Context) : super(ctx) }
。
2.1 覆盖方法
- 必须 类 是
open
可继承的,方法 添加open
修饰才有效,才可被子类覆盖。【类可继承成员才允许开放继承】 - 被覆盖的方法,也必须加上
override
修饰符,否则,编译器会报错。【覆盖方法必须显式添加 override】 - 若方法没有标识
open
,则子类不允许定义相同签名的函数,无论有没有override
。【普通函数不能被继承】 - 若标记为
override
的成员,默认是一直可向下被覆盖的。若想禁止再次覆盖,可使用final
关键字修饰。【final 可禁止覆盖传递】
open class Shape {
open fun draw() { // 可被覆盖 }
open fun color() { }
fun fill() { // 不可覆盖,子类也不允许有同签名方法 }
}
class Circle() : Shape() {
override fun draw() { // 覆盖方法 }
final override fun color() { // 禁止再次覆盖 }
}
2.2 覆盖属性
- 同 覆盖方法 类似。子类重写的属性必须要以
override
开头,并且类型要兼容。 - 每个声明的属性 可由 具有 初始化器 的属性 或
get
方法的属性覆盖。 - 可以用一个
var
覆盖一个val
属性,反之不行。因为 var 有 get() 和 set() 方法,而 val 只有 get() 方法。 - 可以在 主构造函数 中使用
override
关键字 作为 属性声明的一部分。
open class Shape {
open val vertexCount: Int = 0
open val borderColor: String = ""
open val bgColor: String = ""
}
class Rectangle(override val vertexCount: Int = 4) : Shape() { // 在主构造覆盖属性
override val borderColor: String = "#000000"
override var bgColor = "#ffffff" // 使用 var 覆盖 val 属性
}
2.3 派生类初始化顺序
构造派生类实例过程:
- 第一步:完成其基类的初始化(初始化块、属性构造器)
- 第二步:初始化派生类中 声明或覆盖的属性。
Tips:
- 若在 基类初始化 逻辑中,使用了任何 派生类中声明或覆盖的属性,则可能会导致不正确的行为或 运行时故障(因为此时派生类该属性还未初始化)。
- 设计一个基类时,应避免在 构造函数、属性初始化器 以及 init 块中使用
open
成员。
2.4 调用超类实现
- 派生类中 可以使用
super
关键字 调用其超类的函数与属性访问器的实现。 - 在一个 内部类 中访问 外部类的超类,可通过由 外部类名限定的
super
关键字来实现:super@Outer
,如在 Rectangle 类的 inner class 某个方法中调用:[email protected]()
。
2.5 覆盖规则
- 若一个类 从它的直接超类 继承 相同成员的多个实现,则必须覆盖这个成员 并提供自己的实现。
- 为表示从 哪个超类型继承的实现 以消除歧义,使用 尖括号中超类型名限定的
super
,如super
。
open class R1 {
open fun draw() { }
}
// Tips:接口成员默认是 "open" 的
interface P1 {
fun draw() { }
}
// Tips:继承父类,必须要带上父类的构造方法,有参或无参构造
class S1 : R1(), P1 {
override fun draw() {
super.draw() // 调用 P1.draw()
super.draw() // 调用 R1.draw()
}
}
3. 属性
- 属性可使用关键字
var
声明为可变的,也可以是val
声明为只读的。 - 属性 初始化器、getter 和 setter 都是可选的。
3.1 幕后字段
举个例子,就很容易知道幕后字段如何使用,并且对比 Java 的使用场景,就知道 幕后字段 使用场景也非常多。
var counter = 0
set(value) {
this.counter = value
}
上述代码运行后,会出现 stackOverflow,这是为啥?可转换为 Java 代码,set() 里面赋值的那句,直接等价于重新调用了 set() 方法,如此就出现了 自己调用自己,就出现栈溢出错误了。
为了解决上述问题,便可以使用 幕后字段 。
关于某后字段总结如下:
- 使用
field
标识符在 访问器中引用,并且只能在 属性的访问器内使用。 - 并不是所有属性都会有幕后字段,需满足以下条件之一:1)属性至少一个访问器使用默认实现;2)自定义访问器通过
field
引用幕后字段。 - 自定义属性时,借助其他属性赋值,就没有 幕后字段。
// 正确写法
var counter = 0
get() = field
set(value) {
field = value
}
// 以下 isEmpty 没有幕后字段,因为操作的不是字段本身
val isEmpty: Boolean
get() = this.size == 0
set(value) {
this.size = 0
}
3.2 幕后属性
- 有时候有这种需求,我们希望一个属性:对外表现为只读,对内表现为可读可写,我们将这个属性成为 幕后属性。
// _table 为 幕后属性,仅内部操作,table 为对外访问属性
private var _table: Map? = null
public val table: Map
get() {
if (_table == null) {
_table = HashMap() // 类型参数已推断出
}
return _table ?: throw AssertionError("Set to null by other thres")
}
备注:一开始不太理解幕后字段 & 幕后属性这两个概念的,详解与使用场景可查看:https://www.jianshu.com/p/c1a4c04eb33c
3.3 编译期常量
- 定义:只读属性 的值在编译期是已知的,可以使用
const
修饰符标记。
该属性需满足条件: - 位于顶层 或
object 声明
或companion object
的一个成员 - 以
String
或 原生类型值初始化 - 没有自定义
getter
// 对比 Java
private final String a = "A";
private final static String b = "B";
public final static String c = "C";
// 转换为 Kotlin
val a = "A"
companion object {
val b = "B"
const val c = "C" // public 常量才能用 const 标记
}
3. 接口
- 关键字
interface
定义接口 - 一个 类或对象 可实现多个接口 [3]
- 可在接口中定义属性。在接口中声明的属性,要么是抽象的,要么提供访问器实现。另 该属性不能有 幕后字段,因此访问器不能引用。
3.1 函数式(SAM)接口
TODO 暂时不理解定义与使用场景。
4. 可见性修饰符
省略。
5. 扩展
- 能扩展一个 类的新功能 而无需 继承该类 或 使用像装饰者这样的设计模式。如,可为不能修改的第三方库中的类编写一个新的函数。
- 扩展函数:为类新增函数
- 扩展属性:为类新增属性
- 扩展 不能真正修改它们所扩展的类,为 静态分发。如出现同 覆盖方法相同签名的 扩展函数,则具体调用哪个方法,取决于 对象类型是 哪一个。扩展方法,调用时,类型必须同声明一致,没有继承的关系。
- 同签名的 成员函数 与 扩展函数 调用,则调用的一定个是 成员函数。
6. 类的几种表现形式
6.1 伴生对象
- 类内部的对象声明可以用
companion
关键字标记。 - 可以将其写成 该类内 对象声明 中的一员。
- [ 在 类内 声明了一个 伴生对象,就可以访问其成员,只是以 类名 作为 限定符。]
- 伴生对象 看起来像其他语言的静态成员,在运行时仍是 真实对象 的实例成员,并且可以实现接口。
创建一个类实例,但是不用显式声明新的子类,使用 对象表达式 和 对象声明 处理。
6.2 对象表达式
- 和 Java 以匿名方式构造 抽象类 实例很像。
6.3 对象声明
- 单例模式 中常用
- Kotlin 对 单例 声明 非常容易,总是在
object
关键字后跟一个名称。 - 就像变量声明一样,但不是一个表达式,不能用作赋值。
- 对象声明 的初始化过程是 线程安全 的并且在首次访问时进行。
上述三者之间的语义差别:
- 对象表达式 是在使用他们的地方 立即 执行(及初始化的);
- 对象声明 是在第一次被访问到时 延迟 初始化的;
- 伴生对象 是在相应的类被加载(解析)时初始化的,与 Java 静态初始化器的语义相匹配。
代码 DEMO:
// 1. 对象表达式
// Demo01:创建一个继承自 某个/某些 类型的匿名类的对象
abstract class BaseAdapter {
abstract fun getCount(): Int
}
class MyListView {
// 初始化一个 adapter,对象表达式 object : BaseAdapter
var adapter: BaseAdapter = object : BaseAdapter() {
override fun getCount(): Int = 3
}
}
// 2. 对象表达式
class DataProvider
// 对象表达式,object 类名 {} 可以在另一个文件
object DataManager {
fun register(provider: DataProvider) {
//
}
val providers: Collection
get() {
TODO()
}
}
fun testSingleton() {
DataManager.register(DataProvider())
}
/**
* 3. 伴生对象
* - 类内部的对象声明 可以用 companion 关键字标记。
*/
class MyCompanion {
companion object Factory {
fun create(): MyCompanion = MyCompanion()
val globalCount: Int = 0
}
}
// Tips:伴生对象的成员 可通过 只是用类名 作为限定符来调用
val instance = MyCompanion.create()
val count = MyCompanion.globalCount
6.4 抽象类
- 类 及 其中某些成员声明为
abstract
。 - 并不需要用
open
标注一个 抽象类或函数。 - 可以用 一个抽象成员 覆盖 一个非抽象的开放成员。
open class Polygon {
open fun draw() {}
}
abstract class Rectangle : Polygon() {
abstract override fun draw()
}
6.5 数据类
- 使用
data
标记。只保存数据的类
编译器会自动从主构造函数中声明的属性导出/生成以下成员:
- equals() / hashCode 对
- toString() 格式为:
User(name=John, age = 42)
- copy() 函数
为生成合理的代码,数据类满足条件:
- 主构造函数 至少一个参数;
- 主构造函数 搜索页参数都需要标记为
val
或var
; - 数据类不能是 abstract、open、sealed 或 inner;
- (1.1 之前) 数据类只能实现接口。
6.6 密封类
- 使用
sealed
修饰符。 - 用来表示 受限的类继承结构:当一个值为 有限几种的类型、而不能有任何其他类型时。某种意义上,是 枚举类 的扩展。
- 密封类 vs 枚举类:每个枚举常量只存在一个实例,而密封类一个子类可包含状态的多个实例。
- 子类必须与密封类在相同的文件中声明。
- 一个密封类自身是 抽象的,不能实例化 并且 可以有 抽象(
abstract
)成员。 - 密封类 不允许 有非
private
构造函数(默认为 private)。 - 扩展 密封类子类的类可以放在任何位置,无需同一个文件中。
6.7 嵌套类与内部类
- 嵌套类:类可嵌套在其他类中:类、接口可相互嵌套。
- 内部类:使用
inner
标记的嵌套类,能访问外部类的成员,因为会带有一个外部类的对象引用。 - 匿名类:
- 匿名内部类:使用 对象表达式 创建匿名内部类实例(Java 抽象类)。若为接口,则可以使用 lambda 表达式创建(Java 接口)。
6.8 枚举类
- 每个枚举常量都是一个对象,用 逗号 分隔。
- 每个枚举都是 枚举类 的实例,可以如此初始化:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
}
6.9 内联类
仅在 Kotlin 1.3 版本之后才可用,目前处于 Alpha 版本。
- 使用
inline
修饰符声明。
内联类成员限制:
- 内联类 不能有
init
代码块 - 不能含有 幕后字段(因此只能有简单的计算属性)
7. 泛型
TODO
8. 委托
8.1 委托属性
有些属性类型,随每次可手动调用,但是能实现一次并放入一个库会更好。例如包括:
- 延迟属性:其值只在首次访问时计算;
- 可观察属性:监听器会收到有关此属性变更的通知;
- 把每个属性储存在一个 map 中,而不是每个存在于
单独的字段中。
为了涵盖上述情况及其他,Kotlin 支持 委托属性:
class Demo {
var p: String by Delegate()
}
语法:val/var <属性名>: <类型> by <表达式> 。 by
后面的就是该 委托。
更多详见:https://mp.weixin.qq.com/s/BD1zT80IADDZS4CAxmooPg