类,对象,接口

主要内容

  1. 类和接口
  2. 有用的属性和构造器
  3. data class
  4. 类传递
  5. 使用object关键字

Defining class hierarchies

本小节对比Java讨论Kotlin的类层级设计。并介绍新的修改器sealed

Interfaces in Kotlin

Kotlin的接口类似Java8:包含抽象方法的定义,以及非抽象方法的定义(Java8中接口的default方法),但不能包含任何状态。

Kotlin中的接口使用interface关键定义。

1
2
3
interface Clickable {
fun click()
}

方法声明部分为抽象方法,这为click。继承自接口的所有非抽象类都需要提供接口抽象方法的实现。

1
2
3
4
5
6
class Button: Clickable {
override fun click() = println("I was clicked")
}

>>> Button().click()
I was clicked

Kotlin使用冒号(:)后跟接口的方式来代替Java中extendsimplements关键字的写法形式。Java中,接口可以多重实现(implements),但类仅能extends一个父类,这是面向对象设计的多重继承的“菱形问题”,可以搜索Scala是如何解决“菱形问题”问题的。

Kotlin中对Java语法重写后,对于很多包含注解的部分,尽量变成了使用关键字代替,毕竟在当代编程语言中,“注解”被诟病太多。

override修改器,对应于Java中的@Override注解,用于标注方法和属性重载于父类或接口。和Java不同的是,using the override modifier is mandatory,所以如果不写override关键字会导致编译器报错。

接口可以包含默认的实现。和Java8不同的是,接口的默认方法需要带default关键字,Kotlin不需要任何标注:仅需要提供一个方法体。

1
2
3
4
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!") // 带默认实现的方法
}

如果实现该接口,需要提供对click的实现即可, 或者重写showOff方法,或者保留默认不写。

假设现在有另一个接口定义了showOff方法的实现如下部分:

1
2
3
4
5
6
interface Focusable {
fun setFocus(b: Boolean) =
println("I ${if (b) "got" else "lost")} focus.")

fun showOff() = println("I'm focusable!")
}

如果要同时继承这两个接口会怎样?并且它们都包含了default实现;最终选哪个?最终谁都不选,实际上同时继承这两个接口会得到一个显式的编译错误:

1
2
3
The class 'Button' must
override public open fun showOff() because it inherits
many implementations of it.

Kotlin编译器会强制你对这种特定的方法要在具体类内实现,

1
2
3
4
5
6
7
8
9
class Button: Clickable, Fucusable {
override fun click() = println("I was clicked")
// 当超过一个接口继承拥有相同的方法签名的`default`实现时,子类必须要显示声明默认实现
override fun showOff() {
// Kotlin提供一种选择机制,提供`super<>`的形式来选择最终调用哪个接口方法
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}

现在,你只需要调用其中一个继承的实现即可,写为:

1
override fun showOff() = super<Clickable>.showOff()

下面是具体的main方法实现:

1
2
3
4
5
6
fun main(args: Array<String>) {
val button = Button()
button.showOff()
button.setFocus(true)
button.click()
}

NOTE:相对于Scala的多重继承问题,Scala采用with关键字的深度优先的便利方法。也就是说,最后一个接口的方法将被采用。

Scala写法如下:

1
class Button() extends Clickable with Fucusable

根据深度遍历机制,最终只会调用Fucusable 特质的showOff方法。

Scala 3 的写法与Kotlin类似。

Implementing interfaces with method bodies in Java

Kotlin 1.0 开始被设计面向Java6,接口不支持default方法。因此,它融合了每个接口的default方法和常规接口方法,以及一个类包含方法体的静态方法。基于此,如果你需要在Java类实现这样一个接口,你必须定义对应于Kotlin方法体内的所有方法实现。

下面看看如何重载基础类内定义成员。

Open, final, and abstract modifiers: final by default

如你所认知,Java允许你创建任何类的子类,以及重载任何方法,除非它被显式标记了final关键字。通常这很实用,也是问题所在。

所谓的 脆弱基类(fragile base class) 问题发生在:一个基类的修改会引起子类不正确的行为,因为基类的代表改变后不在适配子类的设定。如果子类不提供额外的规则(譬如重载方法),子类的方法重载会给基类的实现带来风险。因为编写基类的作者不可能分析所有的子类,基类的“脆弱(fragile)”由此而来。

为了规避这类问题,Java编程的著名书籍 Effective Java 的作者Joshua Boch(Addison-Wesley, 2008),推荐你“design and document for inheritance or else prohibit it.” 大意就是所有类和方法没有指定要从基类进行重载的,都应该显式地标记上final

Kotlin遵循同样的哲学(philosophy)。然而Java的类或方法默认是open的,Kotlin默认则是final

如果你希望子类的创建,你需要用open修改器标记。额外地,你需要给需要被重载的属性或方法添加open修改器。

1
2
3
4
5
open class RichButton: Clickable {  // 此类是open的:可以继承
fun disable() {} // 函数是final的:不可以在子类重载
open fun animate() {} // 函数是open的:可以在子类进行重载
override fun click() {} // 该函数重载了一个open函数,并且自身保持open
}

需要注意的是,如果你重载了基类或接口的成员,重载的成员默认变为了open。所以,如果你想要子类的override实现不被修改,需要显式地标记上final

1
2
3
open class RichButton: Clickable {
final override fun click() {} // final在这里不是多余的,因为默认override的成员为open
}

Open classes and smart casts

类默认为final的重大益处在于在大多数情况下可以智能转换。前面的章节描述到只有变量能够智能转换,因为它的类型不会改变。但对于一个类,以为着智能转换仅可被用于一个类属性(property)作为一个val不包含自定义accessor的情况。这种要求意味着属性必须是final的,否则子类可以重载它的属性并自定义accessor,打破了智能转换的关键要求。因为属性是默认final修饰的,你可以对大部分属性进行智能转换。

在Kotlin中,和Java一样可以声明一个类为abstract的,该类不能被实例化。以及一个抽象类通常包含抽象成员,必须由子类重载。抽象成员总是open的,所以不需要显式地指定open修改器。

1
2
3
4
5
abstract class Animated {  // 该类是abstract的:不能创建它的一个实例
abstract fun animate()
open fun stopAnimating() {} // 抽象类中的抽象函数默认仍是`final`的,如有必要需要使用
fun animateTwice() {}
}

下表列出了Kotlin的访问 修改器(access modifiers)。接口总是open的,所以接口不会用到finalopenabstract这些关键字。

Modifier Corresponding member Comments
final Can’t be overridden Used by default for class members
open Can be overridden Should be specified explicitly
abstract Must be overridden Can be used only in abstract classes; abstract members can’t have an implementation
override Overrides a member in a superclass or interface Overridden member is open by default, if not marked final

Visibility modifiers: public by default

可见性修改器控制了声明的访问。严格控制一个类的实现的可见性,可以确保对其的修改不会打破其它类的依赖风险。

基本上,Kotlin的可见性修改器类似于Java。包含有publicprotectedprivate。但默认的修改器不同:如果省略可见性修改器,Kotlin默认是public的。

Java的默认可见性是package-private的,这个在Kotlin中不存在。Kotlin的包的作用仅仅用于组织代码的命名空间;所以不用它做可见性控制。

作为替代,Kotlin提供了一个新的可见性修改器,internal,表示“visible inside a module”。module 的概念指Kotlin编译文件的集合。它可能是一个IntelliJ IDEA模块,一个Eclipse工程,Maven或Gradle工程,或一系列Ant调用的编译任务。

internal可见性的优势在于提供了对模块的真实实现的封装。对于Java,模块的封装是很容易被打破的,因为外部的类只需要使用一个同名的包通过继承的方式就可以修改package-private的声明了。

Kotlin可见性的另外一点不同在于,对于top-level的声明总是private的,不管是类、函数还是属性。这种声明的可见性作用于它所在的文件。这种方式可以用于隐藏实现细节。下面是可见性修改器列表。

Modifier Class member Top-level declaration
public(default) Visible everywhere Visible everywhere
internal Visible in a module Visible in a module
protected Visible in subclasses
private Visible in a class Visible in a file

下面例子中。函数giveSpeech函数的每行都尝试违反可见性规则。会导致编译错误:

1
2
3
4
5
6
7
8
9
internal open class TalkativeButton: Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}

fun TalkativeButton.giveSpeech() { // Error: "public" member exposes its "internal" receiver type TalkativeButton
yell() // Error: cannot access "yell": it is "private" in "TalkativeButton"
whisper() // Error: cannot access "whisper": it is "protected" in "TalkativeButton"
}

Kotlin禁止你引用less-visible类型TalkativeButton。因为internal是模块可见的,只能在模块内访问,以及内部成员的声明不可见;要解决这类问题,要么将扩展函数(extension function)修改问internal的,要么去掉internal open使其是public的。

值得注意的是,Kotlin的protected行为和Java有些许差别。在Java中,你可以在同一个包访问protected声明,但Kotlin不允许这样。在Kotlin中,protected的声明仅仅在自身或子类可见。所以一个类的扩展函数无法访问privateprotected的声明成员。

Kotlin’s visibility modifiers and Java

Kotlin中的关键字publicprotectedprivate是编译为Java字节码的保留字。你可以使用这些Kotlin声明为Java对应的可见性实现。唯一例外的是private类:在底层它会被编译为package-private的声明(因为Java没有private类这种用法)。

但对于internal修改器呢?Java中并没有与之对应的。包私有可见性是一个完全不同的东西:Kotlin中的模块通常包含好几个包,不同模块又包含同一个包的声明。这样一个internal实际上对应了字节码的public

实际上,编译后的internal的成员在类中是被损坏的(mangled)。技术层面上,你可以在Java中调用Kotlin的internal成员,但代码看起来非常丑陋。

另外一个不同之处在于Kotlin的外部类看不到它内部(或者说内嵌)类的成员。

Inner and nested classes: nested by default

和Java一样,在Kotlin中你可以在一个类内声明另外一个类。这样做通常用于封装helper型对的类或放置无关紧要的代码在更容易找到的地方。但不同的是Kotlin的内嵌类不能被外部类实例访问,除非你指定要这样做。下面是一个例子。

1
2
3
4
5
6
interface State: Serializable

interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}

相应Java版的实现如下:

1
2
3
4
5
6
7
8
9
10
11
public class Button implements View {
@Override
public State getCurrentState() {
return new ButtonState();
}

@Override
public void restoreState(State state) {}

public class ButtonState implements State {}
}

代码的问题在于,你会得到一个java.io.NotSerializableException: Button。Button不能被序列化:Button没有序列化,但它对ButtonState的引用会打破序列化。

为了修复该问题,你需要声明ButtonStatestatic的。但在Kotlin中,这种行为是正向的。

1
2
3
4
5
6
7
8
class Button: View {
override fun getCurrentState(): State = ButtonState()

override fun restoreState(state: State) {}

class ButtonState: State {} // 该类是Java静态内嵌类的复刻版

}

由于Kotlin中不再包含有staticnew这类关键字,不需要显式声明为static内嵌类。如果需要允许外部类引用它的内嵌类,需要显式使用inner修改器指定。

下表描述了Java和Kotlin内嵌类的不同之处。

Class A declared within another class B in Java in Kotlin
Nested class (does’t store a reference to an outer class) static class A class A
inner class (stores a reference to an outer class) class A inner class A

Figure 4.1

内嵌类(inner class)引用外部类(outer class)的方式也与Java不同。你需要通过this@Outer来访问Outer类。

1
2
3
4
5
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}

Sealed classes: defining restricted class hierarchies

沿用前面的例子:

1
2
3
4
5
6
7
8
9
10
11
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 -> // You have to check the "else" branch.
throw IllegalArgumentException("Unknown expression")
}

当你使用when结构,Kotlin编译器强制你检查默认值。

总是要处理默认分支并不算方便。另外,当你添加新的子类时,编译器并不能探测到改变。如果你往家添加新的分支,默认分支会被选择,这可能会导致难以捉摸的bug。

Kotlin提供了一种解决方案:sealed类。

1
2
3
4
5
6
7
8
9
10
sealed class Expr {  // Mark a base class as sealed...
class Num(val value: Int): Expr()
class Sum(val left: Expr, val right: Expr): Expr() // ...and list all the possible subclasses as nested classes.
}

fun eval(e: Expr): Int =
when (e) { The "when" expression covers all possible cases, so no "else" branch is needed.
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}

当你使用when表达式处理sealed类内的所有子类时,你不需要提供默认分支。注意到sealed修改了暗含了类是open的;你不需要再显式添加。

sealed类的行为如下图:

Figure 4.2

当你在sealed类中使用when并添加一个新的子类时,when表达式会返回一个值错误,表示代码必须更改。

在底层,Expr类有一个private构造器,它仅能在类内部调用。你不能声明一个sealed接口,为什么?如果可以,Kotlin编译器不能保证接口在Java代码的实现。

对于接口的继承使用冒号的形式:

1
class Num(val value: Int): Expr()

Declaring a class with nontrivial constructors or properties

Ktolin 对于构造器和属性的初始化使用了一种initializer块的形式。

Initializing classes: primary constructor and initializer blocks

一般地,带参数的类声明部分称为 第一构造器(primary constructor)。它有两个作用:指定构造器的参数并初始化定义参数属性。让我们解包看看它的显式代码写的是什么:

1
2
3
4
5
6
7
class User constructor(_nickname: String) {  // 带参数的第一构造器
val nickname: String

init { // initializer block
nickname = _nickname
}
}

这里的例子有两个Kotlin关键字:constructorinitconstructo关键字开始于第一构造器或第二构造器。init关键字引入了initializer块。该块内的代码会在类被创建后执行初始化,并被确定为第一构造器一起使用。因为第一构造器包含约束语法,它自己不包含代码的初始化;如有必要,你可以在同一个类声明几个初始化块。

构造器参数_nickname的下划线部分是用于区分属性名和构造器参数名。你可以使用this关键字来规避使用下划线,和Java的写法类似:this.nickname = nickname

实际上,构造器constructor关键字开始可以省略的,

1
2
3
class User(_nickname: String) {  // Primary constructor with one parameter
val nickname = _nickname // The property is initialized with the parameter.
}

最直接的方式是使用val关键字来声明属性定义:

1
class User(val nickname: String)  // "val" means the corresponding proerty is generated for the constructor parameter.

你可以定义默认的构造参数属性值:

1
class User(val nickname: String, val isSubscribed: Boolean = true)  // Provides a default value for the constructor parameter

如果子类需要继承,子类需要显式初始化父类构造器参数:

1
2
open class User(val nickname: String) {...}
class TwitterUser(nickname: String): User(nickname) {...}

如果不声明任何构造器,会生成一个默认构造器:

1
open class Button  // The default constructor without arguments is generated.

带有默认构造器的子类需要显式调用,即使它没有任何构造器参数:

1
class RadioButton: Button()

如果你希望子类不要作任何初始化,使用声明为private的。

1
class Secretive private constructor() {}  // 该类有一个private constructor.

Secondary constructors: initializing the superclass in different ways

通常Kotlin中不会有多个构造器的惯例。相比Java来说构造器的重载在Kotlin中用默认参数代替了。但仍然会有多个构造器的需求。

1
2
3
4
5
6
7
8
open class View {
constructor(ctx: Context) {
// some code
}
constructor(ctx: Context, attr: AttributeSet) {
// some code
}
}

该类没有声明第一构造器,而是声明了两个 第二构造器(secondary constructor)。第二构造器的引入使用constructor关键字。

如果需要扩展该类,你需要声明同样的构造器:

1
2
3
4
class MyButton: View {
constructor(ctx: Context): super(ctx) { ... }
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) { ... }
}

下图是子类构造器的传递性。

Figure 4.3

和Java一样,你也可以在自己的类中调用其它构造器,使用this()关键字。

1
2
3
4
class MyButton: View {
constructor(ctx: Context): this(ctx,MY_STYLE) { ... }
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) { ... }
}

this()调用的是本类,super()调用的是父类。

Figure 4.4

Implementing properties declared in interfaces

在Kotlin中,接口可以包含有抽象户型的定义。

1
2
3
interface User {
val nickname: String
}

这意味着该接口的子类需要包含有nickname的属性定义。以及接口只包含定义不包含值,不会存储任何属性值。

下面是实现接口属性,和自定义getter的对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PrivateUser(override val nickname: String): User // primary constructor property

class SubscribingUser(val email: String): User {
override val nickname: String
get() = email.substringBefore('@') ) // Custom getter
}

class FacebookUser(val accountId: Int): User {
override val nickname = getFacebookName(accountId) // Property initializer
}

>>> println(PrivateUser("test@kotlinlang.org").nickname)
test@kotlinlang.org
>>> println(SubscribingUser("test@kotlinlang.org").nickname)
test

Accessing a backing field from a getter or setter

目前有两类属性:存值属性、访问器属性。下面例子将两种属性组合起来使用,将一个属性存储为值,访问或修改是添加额外计算逻辑。

下面定义一个可变属性并在setter access部分执行额外的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent()) // Reads the backing field value
field = value // Updates the backing field value
}
}
>>> val user = User("Alice")
>>> user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".

你可以通过底层的setter由表达式user.address = "new value"修改属性。这里的setter被重新定义了,额外的逻辑将被执行。

在setter的body部分,你使用了特定的标识符 field 来访问它的字段。在一个getter中,仅可以读;而在setter中,即可以读也可以写。

注意,你仅能重定义可变属性的一个accessor。示例中的getter是琐碎的、它是个read only字段值,不需要redefine。

默认地,属性的getter和setter部分由编译器生成,不需要显式定义。如果提供自定义的accessor,必须使用field关键修饰才生效。

Changing accessor visibility

accessor的可见性默认等同于property所定义的可见性。但可以修改。只需要在getset关键字前面使用可见性修改器(visibility modifier)修饰即可。

1
2
3
4
5
6
7
class LengthCounter {
var counter: Int = 0
private set // You can't change this proerty outside of the class.

fun addWord(word: String) {
counter += word.length
}

属性部分的修改器为public。为了不要其它类修改属性值counter,将其更改为private

1
2
3
4
>>> val lengthCounter= LengthCounter()
>>> lengthCounter.addWord("Hi!")
>>> println(lengthCounter.counter)
3

More about properties later

  • lateinit 作用于一个non-null property,表示该属性在constructor调用后才被初始化。
  • Lazy initialized properties
  • @JvmField 注解
  • const

Compiler-generated methods: data classes and class delegation

Universal object methods

Universal object methods 指的是toStringequalshashCode。通常用于debug。下面是重写的示例:

1
2
3
4
5
6
7
8
9
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean { // "Any" is the analogue of java.lang.Object: a superclass of all classes in Kotlin. The nullable type "Any?" means "ther" can be null.
if (other == null || other !is Client) // Checks whether "other" is a Client
return false
return name == other.name && postalCode == other.postalCode // Checks whether the corresponding properties are equal
}
override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

== for equality

在Java中,你可以使用==来比较原生类型和引用类型。当作用于原生类型时,Java的==比较值;当作用于引用类型时则比较引用类型。所以在Java中需要特别关注一下equals的实现。

在kotlin中,==为比较对象的默认方式:equals重写的类的实例;比较引用类型。

Data classes: autogenerated implementations of universal methods

如果希望类可以方便地保持数据,你需要override这些方法:toStringequalshashCode。Kotlin 提供了data modifier来自动生成这些方法。

1
data class Client(val name: String, val postalCode: Int)

该类会重写所有的Java Object类的方法:

  • equals 用于实例比较
  • hashCode hash-based 容器的keys
  • toString 字符串表述

equalshashCode 会计算所有在第一构造函数中声明的属性。equals会检测所有属性的equality;类似地,hashCode会整合所有属性的hash codes。

DATA CLASSES AND IMMUTABILITY: THE COPY() METHOD

当使用data class时,推荐声明实例为immutable的,因为它更多是作为read-only属性的一种存储手段。另外,编译data class时会额外生成一个copy方法,用于对其实例的修改操作。因此,data class实际上等效于如下的写法:

1
2
3
4
5
6
7
8
9
class Client(val name: String, val postalCode: Int) {
...
fun copy(name: String = this.name, postalCode: Int = this.postalCode) =
Client(name, postalCode)
}

>>> val bob = Client("Bob", 973293)
>>> println(bob.copy(postalCode = 382555))
Client(name=Bob, postalCode=382555)

Class delegation: using the “by” keyword

传递的Java类要实现Class delegation 有一个著名的设计模式 Decorator 。这种模式的本质就是设计一个新的类,旧的类作为该类的成员变量进行访问,从而达到不需要修改原有类来扩展新的方法实现。最常见的就是代理模式。

这种模式被广泛应用于一些框架如Spring中。这会产生很多样本代码。譬如如下代码实例:

1
2
3
4
5
6
7
8
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun iterator(): Iterator<T> = innerList.iterator()
}

kotlin 支持first-class 的委派形式,上述代码可以重写为:

1
class DelegatingCollection<T>(innerList: Collection<T> = ArrayList()) : Collection<T> by innerList 

编译器会帮你自动生成 delegating 的代码,你需要做的,就是对你感兴趣的方法进行override即可。譬如实现自定义的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CountingSet<T>(private val innerSet: MutableCollection<T> = HashSet()) :
MutableCollection<T> by innerSet { // Delegates the MutableCollection implementation to innerSet
var objectsAdded = 0

override fun add(element: T): Boolean { // no delegate
objectsAdded++
return innerSet.add(element)
}

override fun addAll(elements: Collection<T>): Boolean { // no delegate
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}

>>> val cset = CountingSet<Int>()
>>> cset.addAll(listOf(1, 1, 2))
>>> println("${cset.objectsAdded} objects were added, ${cset.size} remain")
3 objects were added, 2 remain

The “object” keyword: declaring a class and creating an instance, combined

Kotlin提供了object关键字,有不同的使用场景:

  • Object declaration ,作为单例定义。
  • Companion objects ,伴生对象。包含该类的工厂方法和其它方法。这些方法不需要创建实例也可以被调用。通过类名访问。
  • Object expression ,相当于Java的匿名类, 一般用于第一构造函数的初始化。

Object declarations: singetons made easy

单例直接用object修饰,

1
2
3
4
5
6
7
8
9
object Payroll {
val allEmployees = arrayListOf<Person>()

fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}

object的声明可以继承类和接口实现。

1
2
3
4
5
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(o1: File, o2: File): Int {
return o1.path.compareTo(o2.path, ignoreCase = true)
}
}

Companion objects: a place for factory methods and static members

伴生对象在类内使用companion object声明,基本语法如下:

1
2
3
4
5
6
7
8
9
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
>>> A.bar()
Companion object called

伴生对象可以访问该类的所有private成员,包括private 构造器,是典型的Factory pattern。

下面是半生对象声明工厂方法的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Individual {

val nickname: String

constructor(email: String) { // Secondary constructors
nickname = email.substringBefore('@')
}

constructor(fackbookAccountId: Int) { // Secondary constructors
nickname = getFacebookName(fackbookAccountId)
}
}

Secondary constructor可以被替换为companion object作为工厂方法实现:

1
2
3
4
5
6
7
class Individual private constructor(val nickname: String) {

companion object {
fun newSubscribingUser(email: String) = Individual(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = Individual(getFacebookName(accountId))
}
}

Companion objects as regular objects

半生对象跟类名不同名是将作为常规对象,实际上会被编译为Java的静态对象的一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
interface Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}

>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}") // You can use both ways to call fromJSON.
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}")
>>> person2.name
Brent

也可以实现接口类型的伴生对象。

1
2
3
4
5
6
7
8
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object: JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = ... // Companion object implementing an interface
}
}

COMPANION-OBJECT EXTENSIONS

伴生对象也可以实现扩展方法(极度不推荐这样做!!),语法和扩展类、扩展属性一致。

1
2
3
4
5
6
7
8
9
10
11
12
// business logic module
class Person(val firstName: String, val lastName: String) {
companion object { // Declares an empty companion object
}
}

// client/server communication module
fun Person.Companion.fromJSON(json: String): Person { // Declares an extension function
...
}

val p = Person.fromJSON(json)

Object expressions: anonymous inner classes rephrased

匿名对象实际上向导那个要Java内匿名类的实例而已。写法如下:

1
2
3
4
5
6
7
8
9
10
11
window.addMouseListener(
object: MouseAdapter() { // Declares an anonymous object extending MouseAdapter
override fun mouseClicked(e: MouseEvent) {
// ...
}

override fun mouseEntered(e: MouseEvent) {
// ...
}
}
)

这种语法和对象的声明一样,处理省略了对象名。该对象表达式声明一个类被创建该类的一个实例,但不指派类或实例的名字。典型地,它是不需要的,因为仅在方法调用中作为参数。当然,你可以把它提取出来:

1
2
3
val listener = object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }

不同的是,Java的匿名类仅可以继承一个类或接口;Kotlin可以实现继承多个接口或不需要接口。

NOTE:和对象声明不同,匿名对象不是单例的。每次一个对象表达式被执行,相应地创建该实例对象。

因为匿名对象的创建属于表达式,因此可以修改外部对象实例。

1
2
3
4
5
6
7
8
9
fun countClicks(window: Window) {
var clickCount = 0 // Declares a local variable

window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++ // Updates the value of the variable
}
})
// ...

Summary

  • Kotlin的接口和Java类似,但可以包含默认的实现和属性。
  • 默认,所有的声明都是final的、public的。
  • 要令一个声明non-final,标记为open
  • internal为module可见的。
  • 内嵌类(Nested classes)默认不是内部类。使用关键字inner来存储它的外部类的引用。
  • sealed类的子类仅内嵌在它声明的地方(同一个文件)。
  • initializer blocks 和 secondary constructors 为类实例的创建提供了灵活性。
  • 可以使用field标识引用accessor 体内的属性字段。
  • data class提供了编译的equalshashCodetoStringcopy和其它方法。
  • 类的委派避免了类似委派方法的样板代码。
  • kotlin中类的声明作为单例。
  • companion objects代替了Java的static块中的字段定义。
  • companion objects可以实现接口,也可以包含扩展函数和属性。
  • kotlin中的对象表达式可以代替Java的匿名内部类,并且可以实现多个接口、修改声明所在地方的变量,因为它属于表达式,每次执行将创建新的实例。