Kotlin的类和接口与Java的类和接口还是有一点区别的。例如,接口可以包含属性声明。与Java不同,Kotlin的声明默认是final和public的。此外,嵌套的类默认并不是内部类:它们并没有包含对其外部类的隐式引用。
对于构造方法来说,简短的主构造方法语法在大多数情况下都工作的很好,但是依然有完整的语法可以让你声明带有重要初始化逻辑的构造方法。对于属性来说也是一样:简洁的语法非常好用,但是你还是可以方便地定义你自己的访问器实现。
Kotlin编译器能够生成有用的方法来避免冗余。将一个类声明为data类可以让编译器为这个类生成若干标准方法。同样可以避免手动书写委托方法,因为委托模式是Kotlin原生支持的。
object关键字可以声明类并创建这个类的一个实例,还可用来表示单例对象、伴生对象和对象表达式(类似于Java的匿名类)。
Kotlin的可见性和访问修饰符与Java中的类似,但还是有一些不一样的默认行为。sealed修饰符用于限制一个类可能存在的子类。
Kotlin的接口与Java8中的相似:它们可以包含抽象方法的定义以及非抽象方法的实现(与Java8中的默认方法类似),但它们不能包含任何状态。
使用interface关键字而不是class来声明一个Kotlin的接口。
声明一个简单的接口
interface Clickable {
fun click()
}
这声明了一个拥有名为click的单抽象方法的接口。所以实现这个接口的非抽象都需要提供这个方法的一个实现。接下来就是如何实现这个接口。
实现一个简单接口
class Button : Clickable {
override fun click() = println("I was clicked")
}
>>> Button().click()
I was clicked
Kotlin在类名后面使用冒号来替代了Java中的extends和implements关键字。和Java一样,一个类可以实现任意多个接口,但是只能继承一个类。
与Java中的@Override注解类似,override修饰符用来标注被重写的父类或者接口的方法和属性。与Java不同的是,在Kotlin中使用override修饰符是强制要求的。这会避免先写出实现方法再添加抽象方法造成的意外重写:你的代码将不能编译,除非你显式地将这个方法标注为override或者重命名它。
接口地方法可以有一个默认实现。与Java9不同的是,Java8中需要你在这样的实现上标注default关键字,对于这样的方法,Kotlin没有特殊的注解:只需要提供一个方法体。给Clickable接口添加一个带默认实现的方法。
在接口中定义一个带方法体的方法:
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!") // 带默认实现的方法
}
如果实现了这个接口,你需要为click提供一个实现。可以重新定义showOff方法的行为,或者如果你对默认行为感到满意也可以直接省略它。
定义另一个实现了同样方法的接口
interface Focusable {
fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable")
}
如果需要在类中实现这两个接口,由于它们每一个都包含了带默认实现的showOff方法,因此任何一个都不会使用。取而代之的是,如果没用显式实现showOff,会得到如下的编译错误:
The class 'Button' must
override public open fun showOff() because it inherits
many implementations of it
Kotlin编译器强制要求提供自己的实现。
调用继承自接口方法的实现
class Button : Clickable, Focusable {
override fun showOff() { /* 如果同样的继承成员有不止一个实现,必须提供一个显式实现 */
/* 使用尖括号加上父类型名字的“super”表明了你想要调用哪一个父类的方法 */
super<Clickable>.showOff()
super<Focusable>.showOff()
}
override fun click() = println("I was clicked")
}
现在Button类实现了两个接口。通过调用继承的两个父类型中的实现来实现
showOff()。要调用一个继承的实现,可以使用与Java相同的关键字:super。但是选择一个特定实现的语法是不同的。在Java中可以把基类的名字放在super关键字的前面,就像Clickable.super.showOff()这样,在Kotlin中需要把基类的名字放在尖括号中:super
如果只需要调用一个继承的实现,可以这样写:
override fun showOff() = super<Clickable>.showOff()
可以创建一个这个类的实例来验证所有继承的方法都可以被调用到。
fun main() {
val button = Button()
button.showOff() // I'm clickable! I'm focusable!
button.setFocus(true) // I got focus.
button.click() // I was clicked
}
setFocus的实现是在Focusable接口中声明的并且被Button类自动继承了。
在Java中实现包含方法体的接口
Kotlin1.0是以Java6为目标设计的,其并不支持接口中的默认方法。因此,它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。接口中只包含声明,类中包含了以静态方法存在的所有实现。因此,如果需要在Java类中实现这样一个接口,必须为所有的方法,包括在Kotlin中有方法体定义自己的实现。
Java允许创建任意类的子类并重写任意方法,除非显式地使用了final关键字进行标注。这通常很方便,但是也造成了一些问题。
对基类进行修改会导致子类不正确的行为,这就是所谓的脆弱的基类的问题。因为基类代码的修改不再符合在其子类中的假设。如果类没有提供子类应该怎么实现的明确规则(哪些方法需要被重写及如何重写),当事人可能会有按基类作者预期之外的方式来重写方法的风险。因为不可能分析所有的子类,这种情况下基类是如此“脆弱”,任何修改都有可能导致子类出现预期之外的行为改变。
为了防止这种问题,作为优秀Java编程风格最为知名的图书之一,Joshua Bloch的《Effective Java》(Addison-Wesley, 2008)建议你“要么为继承做好设计并记录文档,要么禁止这么做”。这意味着所有没有特别需要在子类中被重写的类和方法应该被显式地标注为final。
Kotlin采用了同样的哲学思想。Java的类和方法默认是open的,而Kotlin中默认都是final的。
如果想允许创建一个类的子类,需要使用open修饰符来标识这个类。此外,需要给每一个可以被重写的属性或方法添加open修饰符。
声明一个带一个open方法的open类:
open class RichButton : Clickable { // 这个类时open的,其他类可以继承它
fun disable() {} // 这个函数是final的:不能在子类中重写它
open fun animate() {} // 这个函数是open的:可以在子类中重写它
override fun click() {} // 这个函数重写了一个open函数并且它本身同样是open的
}
注意,如果你重写了一个基类或者接口的成员,重写了的成员同样默认是open的。如果想改变这一行为,阻止你的类的子类重写你的实现,可以显式地将重写的成员标注为final。
禁止重写:
open class RichButton : Clickable {
final override fun click() {} // 在这里“final”并没有被删减是因为没有“final”的“override”意味着是open的
}
open类和智能转换
类默认为final带来一个重要的好处就是这使得在大量场景中的智能转换变为可能。智能转换只能在进行类型检查后没有改变过的变量上起作用。对于一个类来说,这意味着智能转换只能在val类型并且没有自定义访问器的类属性上使用。这个前提意味着属性必须是final的,否则如果一个子类可以重写属性并定义一个自定义的访问器将会打破智能转换的关键前提。因为属性默认是final的,可以在大多数属性上不加思考地使用智能转换,这提高了代码表现力。
在Kotlin中,同Java一样,可以将一个类声明为abstract的,这种类不能被实例化。一个抽象类通常包含一些没有实现并且必须在子类重写的抽象成员。抽象成员始终是open的,所以不需要显式地使用open修饰符。
声明一个抽象类:
abstract class Animated { // 这个类是抽象的:不能创建它的实例
abstract fun animate() // 这个函数是抽象的,它没有实现必须被子类重写
open fun stopAnimating() {} // 抽象类中的非抽象函数并不是默认open的,但是可以标注为open的
fun animateTwice() {}
}
表4.1列出了Kotlin中地访问修饰符。表中的评注适用于类中的修饰符:在接口中,不能使用final、open或者是abstract。接口中的成员始终是open的,不能将其声明为final的。如果它没有函数体它就是abstract的,但是这个关键字并不是必需的。
表4.1 类中访问修饰符的意义
修饰符 | 相关成员 | 评注 |
---|---|---|
final | 不能被重写 | 类中成员默认使用 |
open | 可以被重写 | 需要明确地表明 |
abstract | 必须被重写 | 只能在抽象类中使用:抽象成员不能有实现 |
override | 重写父类或接口中的成员 | 如果没有使用final表明,重写的函数默认是开放的 |
Kotlin中的可见性修饰符与Java中的类似。同样可以使用public、protectd和private修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明就是public的。
Java中的默认可见性——包私有,在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。
作为替代方案,Kotlin提供了一个新的修饰符,internal,表示“只在模块内部可见”。一个模块就是一组一起编译的Kotlin文件。
internal可见性的优势在于它提供了对模块实现细节的真正封装。使用Java时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你的包私有声明的权限。
另一个区别就是Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就会只在声明它们的文件中可见。这就是另外一种隐藏子系统实现细节的非常有用的方式。表4.2总结了所有的可见性修饰符。
表4.2Kotlin的可见性修饰符
修饰符 | 相关成员 | 评注 |
---|---|---|
修饰符 | 类成员 | 顶层声明 |
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() {
/*
错误:“public”成员暴露了其“internal”接收者类型TalkativeButton
Kotlin禁止从public函数giveSpeech去引用低可见的类型TalkativeButton(这个例子中是internal)
解决:把函数改为internal,或者把类改成public的
*/
yell()
/*
错误:不能访问“yell”:它在“TalkativeButton”中是“private”的
*/
whisper()
/*
错误:不能访问“whisper”:它在“TalkativeButton”中是“protected”的
protected成员只在类和它的子类中可见。
扩展函数不能访问它的private和protected成员
*/
}
Kotlin禁止从public函数giveSpeech去引用低可见的类型TalkativeButton(这个例子中是internal)。一个通用的规则是:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性。这个规则可以确保你在需要调用函数或者继承一个类时能够始终访问到所有的类型。要解决上面例子中的问题,既可以把函数改为internal的,也可以把类改成public的。
注意,protected修饰符在Java和Kotlin中不同的行为。在Java中,可以从同一个包访问一个protected的成员,但是Kotlin不允许这样做。在Kotlin中可见性规则非常简单,protected成员只在类和它的子类中可见。同样还要注意的是类的扩展函数不能访问它的private和protected成员。
Kotlin的可见修饰符和Java
Kotlin中的public、protected和private修饰符在编译成Java字节码时会被保留。从Java代码使用这些Kotlin声明就如同他们在Java中声明了同样的可见性。唯一的例外是private类:在这种情况下它会被编译成包私有声明(在Java中不能把类声明为private)。
Java中没有与internal修饰符类似的东西。包私有可见性是一个完全不同的东西:一个模块通常会由多个包组成,并且不同模块可能会包含来自同一个包的声明。因此internal修饰符在字节码中会变成public。
另一个Kotlin与Java之间可见性规则的区别就是在Kotlin中一个外部类不能看到其内部(或嵌套)类中的private成员。
像Java一样,在Kotlin中可以在另一个类中声明一个类。这样做在封装一个辅助类或者把一些代码放在靠近它被使用的地方时非常有用。区别时Kotlin的嵌套类不能访问外部类的实例。除非特别地做出了要求。
试着定义一个View元素,它的状态是可以序列化的。想要序列化一个视图可能并不容易,但是可以把所有需要的数据复制到另一个辅助类中去。声明State接口去实现Serializable。View接口声明了可以用来保存视图状态的getCurrentState和restoreState方法。
声明一个包含可序列化状态的视图:
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}
可以方便地定义一个保存按钮状态的Button类,在Java中如何实现Button类。
用带内部类的Java代码来实现View:
/* 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接口的ButtonState类,并且持有Button的特定信息。 在getCurrentState方法中,创建了这个类的一个新的实例。在真实情况下,需要使用所有需要的数据来初始化ButtonState。
上面的代码会得到一个java.io.NotSerializableException:Button异常。序列化的变量是ButtonState类型的state,并不是Button类型。在Java中,当在另一个类中声明一个类时,它会默认变成内部类。这个例子中的ButtonState类隐式地存储了它的外部Button类。这就解释了为什么ButtonState不能被序列化:Button不是可序列化的,并且它的引用破坏了ButtonState的序列化。
要修复这个问题,需要声明ButtonState类是static的。将一个嵌套类声明为static会从这个类中删除包围它的类的隐式引用。
在Kotlin中,内部类的默认行为与刚刚描述是相反的。
在Kotlin中使用嵌套类实现View:
class button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ } // 这个类与Java中的静态嵌套类类似
}
Kotlin中没有显式修饰符的嵌套类与Java中的static嵌套类是一样的。要把它变成一个内部类来持有一个外部类的引用的话需要使用inner修饰符。表4.3描述了Java和Kotlin在这个行为上的不同之处:嵌套类和内部类的区别在图4.1中呈现。
表4.3嵌套类和内部类在Java与Kotlin中的对应关系
类A在另一个类B中声明 | 在Java中 | 在Kotlin中 |
---|---|---|
嵌套类(不存储外部类的引用) | static class A | class A |
内部类(存储外部类的引用) | class A | inner class A |
图4.1 嵌套类不持有外部类的引用,而内部类持用
在Kotlin中引用外部类实例的语法也与Java不同。需要使用this@Outer从Inner类去访问Outer类:
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
这就是Java和Kotlin中内部类和嵌套类的区别。
之前关于继承结构表达式的例子。父类Expr有两个子类:表示数字的Num,以及表示两个表达式之和的Sum。在when表达式中处理所有可能的子类固然很方便,但是必须提供一个else分支来处理没有任何其他分支能匹配的情况:
作为接口实现的表达式:
interface Expr
class Num(val value: Int) : Expr
class Num(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 ->
thorw IllegalArgumentException("Unknown expression")
}
当使用when结构来执行表达式的时候,Kotlin编译器会强制检查默认选项。在这个例子中,不能返回一个有意义的值,所以直接抛出一个异常。
Kotlin为这个问题提供了一个解决方案:sealed类。为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有的直接子类必须嵌套在父类中。
作为密封类的表达式:
sealed class Expr { // 将基类标记为密封的......
class Num(val value: Int) : Expr() // ......将所有可能的类作为嵌套类列出
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when (e) { // “when”表达式涵盖了所有可能的情况,所以不再需要“else”分支
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
如果在when表达式中处理所有sealed类的子类,你就不再需要提供默认分支。注意,sealed修饰符隐含的这个类是一个open类,你不再需要显式地添加open修饰符。密封类的行为如图4.2所示。
图4.2密封类不能在类外部拥有子类
在when中使用sealed类并且添加一个新的子类的时候,有返回值的when表达式会导致编译失败,它会告诉你哪里的代码必须要修改。
在这种情况下,Expr类有一个只能在类内部调用的private构造方法。
注意,在Kotlin1.0中,sealed功能是相当严格的。例如,所有的子类必须是嵌套的,并且子类不能创建为data类。Kotlin1.1解除了这些限制并允许在同一文件的任何位置定义sealed类的子类。