4.1 定义类层级
作为Java的对比,这一节讨论Kotlin中类层级的定义。我们将会着眼于Kotlin中跟Java类似但有些不同的默认可见性和访问修饰符。你也将会了解到新的sealed
修饰符。它将限制创建子类的可能性。
4.1.1 Kotlin中的接口:带有默认实现的方法
我们将会以接口的定义和实现为开篇。Kotlin接口跟Java 8中的(概念)相似:它们可以包含抽象方法的定义和非抽象方法的实现(类似于Java 8的默认方法),但是它们不能包含任何的状态。
为了在Kotlin中声明一个接口,请使用interface
关键字而不是class
:
interface Clickable {
fun click()
}
这将声明一个只带有一个叫做click()
抽象方法的接口。所有实现了这个接口的的非抽象类都要提供这个方法的一个实现。
以下是你如何实现接口的(演示):
class Button : Clickable {
override fun click() = println("I was clicked")
}
Kotlin在类名后面使用一个冒号来替代Java中的extends
和implements
关键词。
override
修饰符跟Java中的@Override
标注类似,是用来标记覆盖来自父类或接口的方法和属性的。跟Java不同的是,Kotlin强制使用override
修饰。如果某个方法是在你编写完你的实现以后添加的,这能防止你意外的覆盖了它。你的代码无法编译,除非你显式的将方法标记为override
或者将其重新命名。
一个接口方法可以有一个默认的实现。不同于Java 8,要求你用default
关键字标记这样的实现,Kotlin没有用于这些方法的特殊标注:你只需要提供一个方法的主体。让我通过添加一个有默认实现的方法来改变Clickable
接口。
interface Clickable {
fun click() // 1 常规的方法声明
fun showOff() = println("I'm clickable!") // 2 带有默认实现的方法
}
如果你实现了这个接口,你需要为click()
提供一个实现。你可以重写定义showOff()
方法的行为,或者如果你对默认的实现不感冒也可以忽略它。
我们现在假定另一个接口也定义了一个showOff
方法并为它添加了以下的实现:
interface Focusable {
fun setFocus(b: Boolean) =
println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable!")
}
如果你需要在你的类中同时实现这两个接口,会发生什么呢?两个接口都包含带有默认实现的showOff
方法,哪一个实现会胜出呢?两个都不会。相反的,如果你没有显式的实现showOff
,你会得到下面的编译错误:
NOTE
Button
类必须覆盖public open fun showOff()
,因为它继承了多个showOff
实现。
Kotlin编译器强制你提供自己的实现:
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() { // 1 如果继承的成员(函数)有一个以上的实现,你必须提供一个显式的实现。
super<Clickable>.showOff() // 2 “super”限制于尖括号中指定的超类名。这是你想调用的。
super<Focusable>.showOff()
}
}
Button
类现在实现了两个接口。通过调用你从超类继承的两个实现,你使showOff()
生效了。为了调用一个继承的实现,你使用跟Java中相同的关键字:super
。但是选取特定实现的语法是不同的。在Java中,你可以把基类的名字放在super
关键字之前,就像在Clickable.super.showOff()
方法中那样。然而,在Kotlin中,你把基类名放在尖括号中:super<Clickable>.showOff()
。
如果你只需要调用一个继承的实现,你可以写成这样:
NOTE
override fun showOff() = super<Clickable>.showOff()
你可创建这个类的一个实例同时验证:所有继承的方法都可以被调用:
fun main(args: Array<String>) {
val button = Button()
button.showOff() // 1 我是可调用的!我是可获取焦点的!
button.setFocus(true) // 2 我获取到焦点了。
button.click() // 3 我被点击了
}
setFocus
的实现声明在Focusable
接口中,同时在Button
类中自动被继承。
SIDEBAR 在Java中实现有方法体的接口 Kotlin 1.0把不支持接口默认方法的Java 6 作为设计目标。因此,它为常规接口与包含了静态方法的类的组合用默认方法编译了每一个接口。接口只包含了声明,同时类包含了所有作为静态方法的实现。所以,如果你需要在Java类中实现这样一个接口,你必须自定义所有方法的实现,包括Kotlin中有方法体的。 Kotlin的未来版本将会支持生成Java 8字节码作为可选项。如果你选择Java 8作为目标平台,Kotlin的方法体将会编译成默认函数,同时你也可以在Java中实现这样一个接口而无需提供这个方法的实现。
现在,你已经看到了Kotlin是如何允许你实现接口中定义的方法的。让我们来看看这个故事的第二部分:覆盖基类中定义的成员。
4.1.2 open
、final
和abstract
修饰符:final
是默认的
如你所知,Java允许你创建任意一个类的子类,并覆盖任意的方法,除非它已经被显式的用final
关键字标记了。这往往是很方便的,但也有问题。
当由于基类的代码改变却没有匹配子类的假设时,基类的修改会导致子类的不正确行为,所谓的脆弱的基类问题就会发生。如果类没有为子类提供精确的规则(那个方法支持覆盖以及如何覆盖),客户端就会面临没有按照基类作者的预期的方式来覆盖方法的风险。由于不可能分析到所有的子类,基类在某种程度上来说是脆弱的,任意的改变都可能导致子类无法预期的行为改变。
为了避免这个问题,Java优良编程风格方面最出名的一本书,Joshua Bloch写的《Effective Java》推荐你:”设计并为继承编写文档或者干脆禁止“。这就意味着所有的类和方法并没有特意声明为在子类中可被覆盖的,应该显式的标记为final
。
Kotlin遵循同样的哲学。然而Java的类和方法默认是公开的,Kotlin的类和方法默认是final
的。
如果你想允许一个类创建子类,你需要用open
修饰符标记这个类。另外,你需要添加open
修饰符到每个可以被覆盖的属性或者方法。
open class RichButton : Clickable { // 1 这个类是开放的:其他类可以继承它。
fun disable() {} // 2 这个类是*不可修改的(final)*:你不可以在子类中覆盖它。
open fun animate() {} // 3 这个函数式开放的:你可以在子类中覆盖它。
override fun click() {} // 4 这个函数覆盖了一个开放的函数同时它也是开放的。
}
注意,如果你覆盖了一个基类或者接口的成员,覆盖的方法将会是默认open
的。如果你想改变这个可继承性并禁止你的类的子类覆盖你的实现,你可以显式的把覆盖后的成员标记为final
:
open class RichButton : Clickable {
final override fun click() {} // 1 这里的`final`并不是多余的,因为没有`final`修饰的覆盖将会是开放的。
}
SIDEBAR 开放类和智能类型转换 类默认为
final
的一个很明显的好处是在更广泛的场景开启智能转型。正如我们在when
智能类型转换一节中提到的,一个智能转型仅能被用于val
修饰同时没有自定义访问器的类属性这个要求意味着属性必须是final
型的,因为不这样做的话,一个子类可以重写(override)属性并定义一个自定义的访问器,(这就)破坏了智能类型转换的要求了。由于属性默认是final
的,你可以无需考虑并对大部分属性使用智能类型转换。这提升了你的代码的表达能力。 Kotlin中,跟Java一样,你可以把一个类声明为abstract
,同时这个类不能被初始化。一个抽象类通常包含抽象成员,其不需要具体实现但必须在子类中重写。抽象成员总是开放的,因此你不需要显式的使用open
修饰符,这有一个例子:
abstract class Animated { // 1 这个类是抽象的:你不能创建它的实例
abstract fun animate() // 2 这个函数是抽象的:它并没有一个实现同时必须在子类中被覆盖。
open fun stopAnimating() { // 3 抽象类中的非抽象方法默认是不开放的,但是看可以被标记为开放的 。
}
fun animateTwice() { // 3 抽象类中的非抽象方法默认是不开放的,但是看可以被标记为开放的 。
}
}
表格 4.1 列出了Kotlin中的访问修饰符
表格 4.1 类中的访问修饰符的含义
修饰符 | 对应的成员 | 备注 |
---|---|---|
final |
不能被覆盖 | 类成员的默认修饰符 |
open |
可以被覆盖 | 必须显式的指定 |
abstract |
必须被覆盖 | 只能在抽象类中使用,抽象成员不能有实现 |
override |
在一个子类中覆盖一个成员 | 如果没有被标记为final ,覆盖的成员默认是开放的。 |
表中的备注可用于类中的修饰符。在接口中,你不能使用final, open
或者abstract
。接口中的成员总是为open
属性。你不能将其声明为final
。如果接口没有代码体,那么它为abstract
,但是关键词并不是必须的。
TIP Tip 就像跟Java中一样,一个类可以实现很多的接口。但是只能继承一个类。
讨论了控制继承的修饰符,让我们现在继续另一种类型的修饰符的讨论:可见性修饰符。
4.1.3 可见性修饰符:默认是公开的
可见性修饰符有助于限制访问你的代码中的声明。通过限制类实现细节的可见性,你可以确保你改变实现细节但不会有破坏依赖代码的风险。
基本上,Kotlin中的可见性修饰符跟Java中的很相似。你会遇到同样的public, protected
和private
修饰符。但是默认的可见性是不同的:如果你省略了修饰符,(默认的)声明将会是public
。
Java中的默认可见性package-private
并不会在Kotlin中出现。Kotlin把包仅仅作为命名空间中的代码的一种组织方式,并没有用于可见性控制。
作为一个可选方案,Kotlin提供了一个新的可见性修饰符:internal
,它意味着'模块内可见'。一个模块是一组Kotlin文件编译在一起组成的。它也可以是一个IntelliJ IDEA模块、一个Eclipse项目、一个Maven或者Gradle项目,又或者Ant任务调用所编译的一组文件。
internal
可见性的优势是它为你的模块的实现细节提供了实际的封装。使用Java,封装性很容易被破坏。因为外部代码可以在你所使用的同一个包内定义类,并由此获得包内声明的访问权。
更多跟Java的差异来自Kotlin在类的外部定义函数和属性的能力。你可以把这样的声明标记为private
。这意味着“在包含文件内部可见”。如果一个类应该仅在一个文件中使用,你也可以让它私有。表格4.2列出了所有的可见性修饰符。
表4.2 Kotlin可见性修饰符
修饰符 | 对应的成员 | 顶层声明 |
---|---|---|
public(默认可见性) |
所有地方可见 | 所有地方可见 |
internal |
模块内可见 | 模块内可见 |
protected |
子类内部可见 | 不可见 |
private |
类内部可见 | 在文件中可见 |
让我们来看一个例子。giveSpeech
函数的每一行都在尝试着违反可见性的规则,结果编译出错:
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // 1 错误:'public'成员暴露了它的'internal'接收器类型TalkativeButton
yell() // 2 错误:无法访问'yell':它在‘TalkativeButton’内部是'protected'
whisper() // 3 错误:无法访问'whisper':它在'TalkativeButton'内部是'private'
}
译注:此处yell的修饰符是'private',whisper的修饰符是'protected'。注释与代码不符。已经提出勘误
Kotlin禁止你从public
访问性的giveSpeech
函数引用低可见性类型的TalkativeButton
(在这个例子中是internal
)。这是一个有关要求基础类型列表使用的所有类型和类的类型参数,或者函数签名都要跟类或者方法本身的可见性一致的通用规则的例子。这一规则确保了你总是可以访问各种你可能需要调用的函数或者扩展的一个类。为了解决这个问题,你也可以让函数为internal
或者让类为public
。
注意Java和Kotlin的protected
修饰符的表现差异。在Java中,你可以从同一个包中访问一个protected
成员。但是Kotlin并不允许这样做。在Kotlin中,可见性规则是简单的。一个protected
成员仅在类及其子类中可见。也要注意类的扩展函数并不能访问它的私有和保护成员。
SIDEBAR Java中的可见性 当被编译为Java字节码时,Kotlin中的
public, protected
和private
修饰符是保留的。你在Java代码中使用这样的Kotlin声明,好像它们用Java中相同的可见性声明的那样。唯一例外的是private
了:名义上它被编译为package-private
声明(在Java中你不能让一个类私有)。但是,你可能会问,internal
修饰符会发生什么?Java中没有直接的类似概念。package-private
访问性是完全不同的东西:一个模块通常有多个包组成。不同模块可能包含来自同一个包的声明。因此internal
修饰符在字节码中变成了public
。 Kotlin声明和Java对应物之间的一致性解释了为什么有时候你可以从Java代码访问却不能从Kotlin中访问。举个例子,你可以从另一个模块的Java代码中访问一个internal
类或者顶层声明。或者从同一个包中的Java代码总访问保护成员(跟你在Java中的方式相似)。但要注意,类的internal
成员的名字是不完整的。技术上将,internal
成员可以用于Java,但是他们在Java代码中看上去很丑陋。当你扩展来自另一个模块的类时,这有助于避免在覆盖时发生意外的冲突。同时它也能阻止你意外的使用内部类。
Kotlin和Java之间的可见性规则的更多不同之处在于,Kotlin中,外部类无法看到内部(或嵌套)类的成员。让我们接下来以一个例子来讨论Kotlin中的内部类和嵌套类。
4.1.4 内部类和嵌套类:默认为嵌套
跟Java一样,在Kotlin中你可以在一个类中声明另一个类。这样做对于封装一个助手类或把代码放在更接近于使用它的地方是很有用的。不同之处在于,Kotlin嵌套类并没有访问外部类实例,除非你特别需要它。我们看一个例子来了解为什么特性这是重要的。
想象一下,你想要定义一个View
元素,而它的状态是可以被序列化的。序列化一个视图并不容易,但是你可以复制所有的必要数据到另一个助手类。你声明实现了Serializable
接口的State
接口。View
接口声明了可以被用于保存一个视图状态的getCurrentState
和restoreState
方法:
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}
在Button
类中定义一个保存按钮状态的类是非常方便的。让我们来看看在Java中可以怎么做(稍后展示类似的Kotlin代码):
/* Java */
public class Button implements View {
@Override
public State getCurrentState() {
return new ButtonState();
}
@Override
public void restoreState(State state) { /*...*/ }
public class ButtonState implements State { /*...*/ }
}
你定义了一个实现了State
接口并为Button
类保存特定信息的ButtonState
类。在getCurrentState
方法中,你创建了这个类的一个新的实例。在一个真实的案例中,你已经用所有的必要数据初始化了ButtonState
。
这份代码有什么错误呢?为什么当你尝试着去序列化声明在按钮中的状态是,你得到一个java.io.NotSerializableException: Button
异常?第一眼看起很奇怪:你序列化的变量是一个ButtonState
类型的state
,并不是Button
类型。
当你在Java中调用它时,一切都变得清晰了。当你把一个类声明在另一个类里面时,它默认是变为内部类。例子中的ButtonState
类隐式保存了它的外部Button
类的应用。这解释了为什么ButtonState
不能被序列化:Button
不能被序列化,同时指向它的应用破坏了ButtonState
的序列化。
为了解决这个问题,你需要把ButtonState
类声明为static
。把一个嵌套类声明为静态移除了来自外部类的隐式引用。
在Kotlin中,内部类的默认行为跟我们刚描述的相反:
class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ } // 1 这个类是Java中静态嵌套类的对等物
}
Kotlin中,没有显式修饰符的嵌套类跟Java静态嵌套类是一样的。为了把它变成一个内部类,它包含了一个外部类的引用。你使用innner
修饰符。表格4.3描述了这种行为在Java和Kotlin之间的差别。图4.1描述了嵌套类和内部类之间的差别。
表格4.3 Java和Kotlin中嵌套类和内部类的对比
在另一个类B中什么类A | Java | Kotlin |
---|---|---|
嵌套类(不保存外部类的引用) | 静态类A | 类A |
内部类(保存外部类的引用) | 类A | 内部类A |
图4.1 嵌套类不会引用它们的外部类,然而内部类会这样做
Kotlin引用外部类实例的语法也跟Java不同。你写this@Outer
来访问Inner
类的Outer
类。
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
你已经了解到了内部类和嵌套类在Java和Kotlin之间的差别。现在让给我们讨论嵌套类在Kotlin可能有用的另一个用例:创建一个包含有限个类的继承层级。
4.1.5 密封类:定义受限的类层级
回想一下when
智能类型转换一节得到表达式层级示例。超类Expr
有两个子类:Num
,表示一个数;Sum
,表示两个表达式的和。处理when
表达式中所有可能的子类是非常方便的。但是你必须提供else
分支来指定如果没有一个别的分支匹配上时会发生什么:
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> // 1 你必须检查else分支
throw IllegalArgumentException("Unknown expression")
}
当你使用when语法计算一个表达式的时候,Kotlin编译器强迫你检查默认选项。在这个例子中,你没能返回有意义的东西,所以你抛出了一个异常。 一直要添加默认分支是不方便的。更重要的是,如果你添加了一个新的子类,编译器不会检测有到哪些东西发生了改变。如果你忘了添加一个新的分支,默认的分支就会被选中,而这可能会造成一些微妙的bug。 Kotlin为这个问题提供了一个解决方案:密封(sealed)类。你使用sealed修饰符标记一个超类。这限制了创建子类的可能性。所有的直接子类都必须被嵌套在超类中:
sealed class Expr { // 1 把一个基类标记为密封类
class Num(val value: Int) : Expr() // 2 列出所有可能的子类作为嵌套类
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when (e) { // 3 "when"表达式覆盖了所有可能的情况,所以不需要""else"分
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
如果你在一个when
语句中处理一个密封类的所有子类,你不需要提供默认的分支。注意sealed
修饰符意味着类是开放的。你不需要一个显式的open
修饰符。图4.2解释了密封类的行为。
图4.2 密封类不能有定义在外部的继承者
事实上,Expr
类有一个只能在类内部被调用的私有构造函数。你不能声明一个密封接口。为什么呢?如果你可以(这样做)的话,Kotlin编译器不能够保证程序无法在Java代码中实现这个接口。注意,使用这个方法,当你添加一个新的子类时,when
表达式返回一个值并且编译失败,同时指出你的代码哪里需要修改。
NOTE 注意 此时,
sealed
功能是相当受限的。举个例子,所有的子类都必须被嵌套。同时子类不能是数据类(数据类会在后续章节谈到)。未来版本的Kotlin计划要放松这些限制。
跟你回想的一样,在Kotlin中,你在继承一个类或者实现一个接口时都在使用冒号。让我们窥探一下子类的声明(语法):
class Num(val value: Int) : Expr()
除了Expr()
中类名后面的括号的含义以外,这个简单的例子应该很清晰。我们将会在下一节讨论它们,也会覆盖Kotlin中初始化类的内容。