Kotlin 中类继承的相关概念
类继承的相关概念
和 Java 中一样的概念
接口的定义
interface Clickable{
fun click()
}
和 Java 中不一样的概念
继承和实现的方式
- 用冒号
:
代替了extends
和implements
. - 强制使用关键字
override
来重写方法.
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
}
接口默认方法
Java 1.8 支持了接口设置默认方法, Java 中使用 default
关键字来实现默认方法.
interface Clickable {
default void click(){
System.out.println("I'm clickable");
}
}
在 Kotlin 的接口中, 我们可以直接设置:
interface Clickable {
fun click() = println("I'm clickable!")
}
和 Java 中相同的是, 如果一个类实现了两个接口, 这两个接口包含有签名相同的默认方法, 那么我们必须显示实现这个方法, 不然会有编译错误.
Kotlin 和 Java 中对父类方法的调用也有不同:
interface Clickable {
fun click()
// default method in kotlin
fun showOff() = Println("I'm clickable!")
}
interface Focusable {
fun setFocus(b : Boolean) =
println("I ${if (b) "got" else "lost"} focus")
}
class Button : Clickable, Focusable {
// use override to override a method
override fun click() = println("I was clicked")
// cannot use default because the both parents have this method
override fun showOff() {
// use cusp brackets to specify parent
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
我们都使用了关键字 super
来对父类的调用, 在 Java 中, 我们将基类的名字放在 super
关键字的前面, 比如 Clickable.super.showOff()
, 在 Kotlin 中我们将基类放在尖括号中 super<Clickable>.showOff()
.
对继承的默认行为 open, final 和 abstract
在 Java 中, 子类对基类的方法默认是可以重写的 (open 的), 除非使用被显示的标注为了 final. 不恰当的重写可能使子类的实现偏离了基类的设计目的, 这就是所谓的 脆弱的基类 问题, 较好的习惯是要么为继承做好设计并写好文档, 要么就标位 final.
根据这一思想, 在 Kotlin 中, 默认都是 final 的. 一个类默认是 final 的, 如果你想允许这个类被继承, 那么需要用 open
修饰; 一个方法默认是 final 的, 如果你想允许这个方法被重写, 那么需要用 open
修饰.
open class RichButton : Clickable {
// method disable cannot be overwrite by the subclass
fun disable() {}
// use open to permit a fun to be overwrite
open fun animate() {}
// overwrite a fun, and this fun can be overwrite by subclass.
// if you want to prohibit it to be overwrite, use final.
override fun click() {}
}
如上例表示的, 如果一个方法是我们从父类继承而来, 那么这个方法默认是可以被重写的, 如果我们不想让其被重写, 那么就需要使用 final 来修饰.
可见性修饰
- Java 中的可见性: private, public , protected, package-private.
- Kotlin 中的可见性: public, private, protected, internal.
internal: 模块内可见, 一个模块就是一组一起编译的 Kotlin 文件, 可能是一个 Intellij IDEA 模块, 一个 Eclipse 项目, 一个 Maven 或 Gradle 项目或者一组使用调用 Ant 任务进行编译的文件.
Java 中 protected 成员可以被该类和其子类和同一个包内访问, 而 Kotlin 中只能被该类和子类中可见.
同样要注意的是, Kotlin 的扩展函数不能访问 private 和 protected 成员.
内部类和嵌套类
什么是内部类和嵌套类:
Java 有内部类的概念, 内部类会隐式地存储它的外部类引用, 这在有些情况下会出现问题.
Java 中如果想声明一个类为嵌套类, 使用 static 关键字.
Kotlin 的默认行为不同, Kotlin 默认为嵌套类, 如果声明内部类, 使用 inner 关键字.
Kotlin 访问外部类使用 this@Outer
的方式. 而 Java 中使用 Outer.this
的方式.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
密封类
Kotlin 增加了一种定义受限的继承结构: 密封类
密封类使用 sealed 关键字修饰, 默认为 open 的. sealed 类对子类做出了限制, 要求子类必须定义在同一个文件中, 编译时会进行检查, 适用于如下场景:
sealed class Expr {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
class Sub(val left: Expr, val right: Expr) : Expr()
fun eval(e: Expr): Int = when (e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
is Sub -> eval(e.left) - eval(e.right)
}
fun main(args: Array<String>) {
val no1 = Expr.Num(3)
val no2 = Expr.Num(1)
val sub = Sub(no1, no2)
println(eval(sub))
}
我们在 when 中需要判断具体是什么类型, 如果我们添加默认类型, 那么可能会因为我们添加了新的子类而没有修改 when 而产生 bug (因为新的子类的情况会导向默认情况), 而 sealed 类将限制子类, 编译器将知道有哪些子类, 所以我们不用添加默认情况 (else), 在有了新的子类后, 将会检查我们需不需要添加新的分支.
类初始化
Java 中类的初始化我们都非常熟悉了, 通过构造方法来实现, 可以声明一个或多个构造方法.
Kotlin 中, 对主构造方法和从构造方法作了区分
主构造方法
主构造方法一般是主要而间接的初始化类的方法, 比如下面的简单的类.
class User(val nickname: String)
主构造方法就是括号中的内容 (对, 就这么简单), 它完成了两个功能, 一个是声明了 nickname
这个属性, 再一个是完成了这个属性的初始化.
对的, 主构造方法没有语句块, 如果需要一些初始化语句, 需要在类内部使用 init
初始化语句块:
class User(val nickname: String) {
init {
// init 语句块, 完成一些初始化工作.
// 在类创建时执行
// 可以创建多个 init 语句块
}
}
另一个需求是对主构造方法由访问限制, 比如如果我们想要一个 private 的主构造方法, 那么使用如下形式:
class User private constructor(val nickname: String)
这里使用了 constructor
关键字, 用 private 进行修饰, 表示这是一个 private 的主构造方法.
继承
继承中, 子类的构造方法要对父类进行初始化, 用如下形式:
open class User(val nickname: String) { ... }
class MyUser(nickname: String) : User(nickname) { ... }
注意冒号后面的部分, 继承一个类, 必须显示的调用其主构造方法进行初始化 (即使父类没有任何参数也要显示调用).
继承一个接口, 接口没有构造方法, 所以, 实现一个借口时, 是没有后面那么括号的.
// 即使父类构造构造方法没有参数也要显式调用
class RadioButton : Button()
// 接口没有构造方法
interface SimpleInterface
class MyClass : SimpleInterface
从构造方法
首先需要注意, 多个构造方法在 Kotlin 中不如 Java 常见, 因为 Kotlin 支持参数默认值和参数命名的语法.
但是还是会有需要多个构造方法的场景, 使用从构造方法:
open class view {
constructor(ctx: Context) {
// some code
}
constructor(ctx: Context, attr: AttributeSet) {
// some code
}
}
将一个构造方法委托给另一个构造方法 (从一个构造方法中调用自己的类的另一个构造方法), 使用 this()
关键字:
open class Bus {
val name: String
val price: Int
constructor(name: String): this(name, 10) {
println("一辆新车")
}
constructor(name: String, price: Int){
this.name = name
this.price = price
println("name: $name\nprice: $price")
}
}
当我们使用第一个构造方法时, 会先调用第二个构造方法, 再调用自己的语句块.
这里这个例子是为了说明从构造方法, 实际上我们可以使用默认值来代替, 更加简洁:
open class Bus(val name: String, val price: Int = 10)
继承
class MyBus: Bus {
constructor(name: String): this(name, 10)
constructor(name: String, price: Int): super(name, price)
}
实现接口中声明的属性
在 Kotlin 中, 接口可以包含抽象属性声明, 例如:
interface User {
val nickname: String
}
这表示, 实现 User 接口的类, 需要提供一个取得 nickname 的值的方式, 获取值的方法主要有三种: 主构造方法, 自定义 getter(), 属性初始化.
// 主构造方法属性
class PrivateUser(override val nickname: String) : User
// 自定义 getter()
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.subStringBefore('@')
}
// 属性初始化
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId)
}
我们这里思考一下第二种和第三种初始化方式有什么区别?
- 第二种, 自定义
getter()
的初始化方法, 我们并没有一个字段来存储 nickname 这个变量, 而是在需要获取该值的时候, 从 email 中截取, 截取的开销很小, 所以只需在需要的时候获取. - 第三种, 我们存在一个字段来存储 nickname, 因为
getFacebookName()
方法是一个开销很大的方法, 可能需要网络请求或者数据库查询, 合理的方法是在初始化的时候获取一次并存储在一个字段中.