接口
接口的方法可以有一个默认实现
interface Clickable {
fun click() // 普通的方法声明
fun showOff() = println("I'm clickable!") // 带默认实现的方法
}
如果你实现了这个接口,并且对默认行为感到满意的话可以省略 showOff
的实现,但你需要为 click
提供一个实现。
如果另外一个借口也有一个同样的方法showOff
interface Focusable {
fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable!")
}
如果你需要在你的类中实现这两个接口会发生什么?
如果你没有显式实现 showOff
,你会得到如下的编译错误:
The class 'Button' must override public open fun showOff() because it inherits many implementations of it.
Kotlin 编译器强制要求你提供你自己的实现。
现在 Button
类实现了两个接口。你通过调用继承的两个父类型中的实现来实现自己的 showOff()
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() { // 如果同样的继承成员有不止一个实现,你必须提供一个显式实现。
super<Clickable>.showOff()
super<Focusable>.showOff() // 使用尖括号加上父类型名字的 "super" 表明了你想要调用哪一个父类的方法
}
}
Java 中你可以把基类的名字放在 super
关键字的前面,就像 Clickable.super.showOff()
这样,在 Kotlin 中需要把基类的名字放在尖括号中:super<Clickable>.showOff()
类
继承
如果你想允许创建一个类的子类,你需要使用 open
修饰符来标示这个类。此外,你需要给每一个可以被重写的属性或方法添加 open
修饰符。
open class RichButton : Clickable { // 这个类是open的:其他类可以继承它。
fun disable() {} // 这个函数是final的:你不能在子类中重写它。
open fun animate() {} // 这个函数是open的:你可以在子类中重写它。
override fun click() {} // 这个函数重写了一个开放函数并且它本身同样是open的。
}
注意,如果你重写了一个基类或者接口的成员,重写了的成员同样默认是
open
。如果你想改变这一行为,阻止你的类的子类重写你的实现,你可以显式将重写的成员标注为final
。open class RichButton : Clickable { final override fun click() {} }
在 Kotlin 中,同 Java 一样,你可以将一个类声明为 abstract
,抽象类始终是开放的,所以你不需要显式使用 open
修饰符。
abstract class Animated { // 这个类是抽象的:你不能创建它的实例。
abstract fun animate() // 这个函数是抽象的:它没有实现必须被子类重写。
open fun stopAnimating() {} // 抽象类中的非抽象函数并不是默认开放的,但是可以标注为开放的。
fun animateTwice() {}
}
可见性
Kotlin 中的可见性修饰符与 Java 中的类似。你同样可以使用 public
, protected
和 private
修饰符。
但是默认的可见性是不一样的:如果你省略了修饰符,声明就是
public
的。Java 中的默认可见性——包私有,在 Kotlin 中并没有使用。
在 Java 中,你可以从同一个包中访问一个
protected
的成员,但 Kotlin 中protected
成员只在类和它的子类中可见。要注意的是类的扩展函数不能访问它的
private
和protected
成员。
嵌套类
默认情况下Kotlin 的嵌套类不能访问外部类的实例,而 Java 中可以
Kotlin 中没有显式修饰符的嵌套类与 Java 中的
static
嵌套类是一样的-
要把它变成一个内部类来持有一个外部类的引用的话你需要使用
inner
修饰符类 A 在另一个类 B 中声明 在 Java 中 在 Kotlin 中 嵌套类(不存储外部类的引用) static class A class A 内部类(存储外部类的引用) class A inner class A
在 Kotlin 中引用外部类实例的语法也与 Java 不同。你需要使用 this@Outer
从 Inner
类去访问 Outer
类:
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
密封类(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
修饰符。当你在
when
中使用sealed
类并且添加一个新的子类的时候,有返回值的when
表达式会导致编译失败,它会告诉你哪里的代码必须要修改。Expr
类有一个只能在类内部调用的private
构造方法。你也不能声明一个sealed
接口。为什么?如果你能这样做的话,Kotlin编译器不能保证任何人都不能在 Java 代码中实现这个接口。
构造函数
主构造函数和初始化语句块
class User constructor(_nickname: String) { // 带一个参数的主构造方法
val nickname: String
init { // 初始化语句块
nickname = _nickname
}
}
简化后
class User(val nickname: String) // "val" 意味着相应的属性会用构造方法的参数来初始化
如果你的类具有一个父类,主构造方法同样需要初始化父类。你可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:
open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }
如果你没有给一个类声明任何的构造方法,将会生成一个不做任何事情的默认构造方法:
open class Button // 将会生成一个不带任何参数的默认构造方法
如果继承了 Button
类并且没有提供任何的构造方法,你必须显式调用父类的构造方法即使它没有任何的参数:
class RadioButton: Button()
这就是为什么在父类名称后面还需要一个空的括号
注意与接口的区别:接口没有构造方法,所以在你实现一个接口的时候,你不需要在父类型列表中它名称后面再加上括号。
数据类
如果你为你的类添加 data
修饰符,必要的方法将会自动生成toString
,equals
和 hashCode
。
data class Client(val name: String, val postalCode: Int)
请注意虽然数据类的属性并没有必须是 val
—— 你同样可以使用 var
—— 但还是强烈推荐只使用只读属性,让数据类的实例不可变。
为了让使用不可变对象的数据类变得更容易,Kotlin 编译器为他们多生成了一个方法copy()
:一个允许拷贝类的实例的方法,并在拷贝的同时修改某些属性的值。
类委托:"by" 关键字
下面这段代码,为了实现 Collection
借口,你必须实现所有的方法
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun contains(element: T): Boolean = innerList.contains(element)
override fun iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean =
innerList.containsAll(elements)
}
而如果使用类委托,就可以变成
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
类中所有的方法实现都消失了,DelegatingCollection
默认会使用ArrayList
的行为,如果你需要修改某一个函数的行为,只需要重写这一个函数即可。
"object"关键字
Tips: 这是我在学习 Koltin 是感受到最需要注意的地方,
object
在Kotlin
中的用法和重要。
Kotlin 中 object
关键字在多种情况下出现,但是它们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(换句话说就是一个对象)。让我们来看看使用它的不同场景:
- 对象声明是定义单例的一种方式。
- 伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。
- 对象表达式用来替代 Java 的匿名内部类。
对象声明
对象声明将 类声明 与该类的 单一实例 声明结合到了一起。
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}
与变量一样,对象声明允许你使用对象名加 .
字符的方式来调用方法和访问属性:
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
你同样可以在类中声明对象。这样的对象同样只有一个单一实例,它们在每个容器类的实例中并不具有不同的实例
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int =
p1.name.compareTo(p2.name)
}
}
在 Java 中使用 Kotlin 对象
Kotlin 中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是
INSTANCE
。如果你在 Java 中实现单例模式,你也许也会顺手做同样地事。因此,要从 Java 代码使用 Kotlin 对象,可以通过访问静态的INSTANCE
字段:/* Java */ CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
在这个例子中,
INSTANCE
字段的类型是CaseInsensitiveFileComparator
。
伴生对象companion
Kotlin 中的类不能拥有静态成员;Java 的 static
关键字并不是 Kotlin 语言的一部分。使用伴生对象,会让我们的方法调用看起来很像static
方法。
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
>>> A.bar()
Companion object called
重要用途,实现工厂方法模式
class User private constructor(val nickname: String) { // 将主构造方法标记为私有
companion object { // 声明伴生对象
fun newSubscribingUser(email: String) = // 声明一个命名的伴生对象
User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = // 用工厂方法通过 Facebook 账号来创建一个新用户
User(getFacebookName(accountId))
}
}
你可以通过类名来调用 companion object
的方法:
>>> val subscribingUser = User.newSubscribingUser("bob@gmail.com")
>>> val facebookUser = User.newFacebookUser(4)
>>> println(subscribingUser.nickname)
bob
作为普通对象使用的伴生对象
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}
>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}") // 你可以通过两种方式来调用 fromJSON
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}") // 你可以通过两种方式来调用 fromJSON
>>> person2.name
Brent
如果你省略了伴生对象的名字,默认的名字将会分配为 Companion
。
在伴生对象中实现接口
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = ... // 实现接口的伴生对象
}
}
这时,如果你有一个函数使用抽象方法来加载实体,你可以将 Person
对象传进去。
fun loadFromJSON<T>(factory: JSONFactory<T>): T {
...
}
loadFromJSON(Person) // 将伴生对象实例传入函数中
注意,Person
类的名字被用作 JSONFactory
的实例。
另外,我们还可以为半生对象定义扩展函数。
class Person(val firstName: String, val lastName: String) {
companion object { // 声明一个空的伴生对象
}
}
// client/server communication module
fun Person.Companion.fromJSON(json: String): Person { // 声明一个扩展函数
...
}
val p = Person.fromJSON(json)
匿名内部类
fun countClicks(window: Window) {
var clickCount = 0 // 声明局部变量
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++ // 更新变量的值
}
})
// ...
}
与 Java 的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但是与 Java 不同,访问并没有被限制在 final
变量;你还可以在对象表达式中修改变量的值。例如,我们来看看你可以怎样使用监听器对窗口点击计数。
fun countClicks(window: Window) {
var clickCount = 0 // 声明局部变量
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++ // 更新变量的值
}
})
// ...
}