Kotlin 语言学习(1) - Kotlin 基础
Kotlin 语言学习(2) - 函数的定义与调用
Kotlin 语言学习(3) - 类、对象和接口
Kotlin 语言学习(4) - 数据类、类委托 及 object 关键字
Kotlin 语言学习(5) - lambda 表达式和成员引用
Kotlin 语言学习(6) - Kotlin 的可空性
Kotlin 语言学习(7) - Kotlin 的类型系统
Kotlin 语言学习(8) - 运算符重载及其他约定
Kotlin 语言学习(9) - 委托属性
Kotlin 语言学习(10) - 高阶函数:Lambda
Kotlin 语言学习(11) - 内联函数
Kotlin 语言学习(12) - 泛型类型参数
Kotlin
的接口可以包含以下两种类型的方法:
Kotlin
接口使用 interface 关键字来声明,所有实现这个接口的非抽象类都需要实现接口中定义的抽象方法。Kotlin
在类名后面使用 冒号 代替了Java
中的extends
和implements
关键字,一个类可以实现多个接口,但是只能继承一个类。override
修饰符用来标注被重写的父类或者接口的方法和属性,并且是 强制要求 的。下面的例子中定义了一个接口,并演示了如何实现该接口,以及接口中定义的抽象方法:
我们可以给接口的方法提供一个默认的实现,定义的方法和普通函数相同。
如果一个类实现了两个接口,而这两个接口定义了相同的方法,并且都提供了该方法的默认实现,那么该类必须显示实现该方法,否则会在编译时报错:
当需要调用一个继承的实现,可以使用与Java
相同的关键字 super,并在后面的尖括号中指明父类的名字,最后是调用的方法名:
Kotlin
中,类和方法默认都是final
的,如果想允许创建一个类的子类,需要使用open
修饰符来标示这个类,此外还需要给每一个允许被重写的属性或方法添加open
修饰符。open
的,如果想改变这一行为,可以显示地将重写的成员标注为final
。我们可以将一个类声明为abstract
,这种类不能被实例化,一个从抽象类通常包含一些没有实现并且必须在子类重写的抽象成员:
abstract
的,不一定要加上关键字,其访问性始终是open
的。final
的,如果需要重写,那么需要加上open
修饰符。open
、final
和abstract
这三个访问修饰符都 只适用于类,不能用在接口 当中:
open
:用于声明一个类可以被继承,或者方法可以被子类重写。final
:不允许类被继承,或者不允许方法被重写。abstract
:声明抽象类,或者抽象类中的抽象方法。当我们需要重写方法时,必须加上override
修饰符。
Kotlin
的可见性修饰符包括以下四种:
修饰符 | 类成员 | 顶层声明 |
public | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | --- |
private | 类中可见 | 文件中可见 |
Java
和Kotlin
在可见性上的区别包括以下几点:
Java
中默认的可见性是包私有的,而在Kotlin
中,默认的可见性是public
的。Kotlin
用internal
作为包可见的替代方案,它表示“只在模块内部可见”。Kotlin
允许在顶层声明中使用private
可见性,包括类、函数和属性,这些声明就只在声明它们的文件中可见,这是隐藏子系统实现细节的非常有用的方式。private
和protected
成员。Kotlin
中,一个外部类不能看到其内部类中的private
成员。在Kotlin
中,如果我们像Java
一样,在一个类的内部定义一个类,那么它并不是一个 内部类,而是 嵌套类,区别在于嵌套类不会持有外部类的引用,也就是说它实际上是一个静态内部类:
如果要把它嵌套类变成一个 内部类 来持有一个外部类的引用的话需要使用inner
修饰符,并且访问外部类时,需要使用this@{外部类名}
的方式。
之前在介绍when
表达式的时候,我们用了一个表达式的例子,Num
和Sum
继承于基类Expr
,分别表达数字和两个表达式之和,而对于不属于Expr
的子类,我们需要提供额外的else
操作符。
假如我们给Expr
添加了一个新的子类,编译器并不能发现有地方改变了。如果忘记了添加一个新分支,就会选择默认的选项,这有可能导致潜在的bug
。
Kotlin
为这个问题提供了一个解决方案:sealed
类。为父类添加一个sealed
修饰符,对可能创建的子类做出严格的限制,所有的直接子类必须嵌套在父类中。之前的例子修改如下:
这时候,我们在when
表达式中已经处理了所有Expr
的子类,就不再需要提供默认的分支,假如这时候我们给Expr
添加一个新的子类Multi
,但是不修改when
中的逻辑,那么就会导致编译失败:
提示的信息为:
在这种情况下,Expr
类有一个只能在类内部调用的private
构造方法,你也不能声明一个sealed
接口,因为如果这样做,Kotlin
编译器不能保证任何人都不能在Java
代码中实现这个接口。
在Java
中,一个类可以声明一个或多个构造方法,Kotlin
则将构造方法分为两类:
假设,我们需要定义一个包含只读nickname
属性的User
类,最简单的方式为:
上面这段被括号围起来的语句块就叫做 主构造方法,它有两个目的:
nikename
。用于完成上面这两个功能的最明确的代码如下所示:
constructor
:用来开始一个主构造方法和从构造方法的声明。init
:引入一个初始化块语句,这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用,因为主构造方法有语法限制,这就是为什么要使用初始化语句块的原因。在上面的例子中有几个可以简化的点:
nikename
的声明结合,因此可以去掉init
语句。constructor
关键字。val
关键字加在参数前的方式来进行简化。经过了以上三点,就会得到最前面简化后的结果。
对于构造方法,也可以采用之前在 Kotlin 语言学习(2) - 函数的定义与调用 中介绍的 命名参数 和 默认参数值 的技巧,如果所有的构造方法都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。
在Java
中,如果父类定义了一个构造方法,那么在子类的构造方法中,必须要通过super
方法初始化父类,例如:
//父类。
public class User {
private String nikeName;
User(String nikeName) {
this.nikeName = nikeName;
}
}
//子类。
public class TwitterUser extends User {
public TwitterUser(String nikeName) {
super(nikeName);
}
}
而在Kotlin
中,可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:
假如一个类没有声明任何的构造方法,将会生成一个不做任何事的默认构造方法,如果有子类继承了它,那么必须显示地调用父类的构造方法,即使它没有任何的参数。
如果想要确保你的类不被其它代码实例化,必须把构造方法标记为private
,我们对上面的例子进行修改:
报错的原因为:
在大多数真实的场景中,类的构造方法是非常简明的:它要么没有参数或者直接与参数对应的属性关联,这就是为了Kotlin
有为主构造方法设计的简洁的语法。
大多数在Java
中需要重载构造方法的场景都被Kotlin
支持命名参数和参数默认值的语法所覆盖了。
而当我们需要扩展一个框架来提供多个构造方法,以便于通过不同的方式来初始化类的时候,就会需要用到从构造方法,从构造方式使用constructor
方法引出,例如下面的代码:
运行结果为:
如果想要扩展这个类,可以声明同样的构造方法,并使用super
关键字调用对应的父类构造方法:
运行结果为:
如果想要从一个构造方法中,调用你自己的类的另一个构造方法,那么可以使用this
关键字:
运行结果为:
友情提示:如果类没有主构造方法,那么每个从构造方法必须初始化基类(通过super
关键字)或者委托给另一个这样做了的构造方法(通过this
关键字),也就是说,每个从构造方法必须以一个朝外的箭头开始,并且结束于任意一个基类构造方法,就像上面例子中Button
的带有两个参数的从构造方法所做的那样。
在Kotlin
中,接口可以包含抽象属性的声明:
但是接口并没有说明这个值应该存储到一个支持字段还是通过getter
来获取,接口本身并不包含任何状态,因此只有实现这个接口的类在需要的时候会存储这个值。
下面是三个例子:
PrivateUser
:直接在主构造方法中声明了这个属性,这个属性实现了来自于User
的抽象属性,所以要标记为override
。SubscribingUser
:通过一个自定义的getter
实现,这个属性没有一个支持字段来存储它的值,它只有一个getter
在每次调用时从email
中得到昵称。FacebookUser
:在初始化时,将nickname
属性与值关联。接口除了可以声明抽象属性外,还可以包含具有getter
和setter
的属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的):
email
:必须在子类中重写。nickname
:有一个自定义的getter
,可以被子类继承。运行结果为:
现在,我们已经学习了两种属性的用法:
现在,我们结合以上两种,来实现一个既可以存储值,又可以在值被访问和修改时提供额外逻辑的属性:
运行结果为:
上面的address
就是 有支持字段的属性,它和 没有支持字段的属性 的区别在于:
field
,支持字段就不会被呈现出来。访问器的可见性默认与属性的可见性相同,但是如果需要可以通过在get
和set
关键字前放置可见性修饰符的方式来修改它,例如在下面的例子中,我们将setter
的可见性修改为private
:
运行结果为: