类、对象和接口

Kotlin接口

使用interface关键字声明一个Kotlin接口。

interface Clickable {
    fun click()
}
1
2
3

Kotlin实现接口是在类名后面使用:来代替Java中的implements关键字。接口继承也是用:来代替extends关键字。

class Button : Clickable {
    override fun click() = println("click")
}
1
2
3

override修饰符用来标注被重写的父类或接口的方法或属性。不同于Java中的@Override注解,Kotlin的override修饰符是强制要求的。

接口的方法可以有一个默认实现。这个实现不需要特殊注解,只需要提供一个方法体。

interface Clickable {
    fun click()
    fun showOff() = println("showOff")
}
1
2
3
4

如果某个类实现了多个接口,而这些接口有同名的默认方法,那么Kotlin编译器强制要求这个类要提供自己的实现。

interface Focusable {
    fun setFocus(b: Boolean) =
        println("${if (b) "got" else "lost"}")
    
    fun showOff() = println("showOff")
}
1
2
3
4
5
6
class Button : Clickable, Focusable {
    override fun click() = println("clicked")
    
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}
1
2
3
4
5
6
7
8

可通过super<父类/接口名>.方法名()来调用父类或接口的方法。

Kotlin1.0是以Java6为目标设计的,其并不支持接口中的默认方法。因此,它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。

访问修饰符

修饰符 相关成员 说明
final 不能被重写 类中成员默认使用
open 可以被重写 需要明确地表明
abstract 必须被重写 只能在抽象类中声明;抽象成员始终是open的。
override 重写父类或接口中的成员 如果没有使用final表明,重写的成员默认是open的。

Java的类和方法默认是open的,而Kotlin中默认都是final

在Kotlin中,和Java一样,可以将一个类声明为abstract。这种类被称作抽象类,它是不能被实例化的,它通常包含一些没有实现并且必须在子类重写的抽象成员。抽象成员是不能有实现的。

可见性修饰符

Kotlin的可见性修饰符与Java的类似,同样是publicprotectedprivate

Kotlin只把包作为在命名空间里组织代码的一种方式,并没有将其用作可见性控制。

Kotlin新增了一个叫internal的修饰符,它表示“只在模块内部可见”。一个模块就是一组一起编译的Kotlin文件,它可能是一个Intellij IDEA模块、一个Eclipse项目、一个Maven或Gradle项目、一组使用Ant任务进行编译的文件等等。

Java的默认可见性为包私有,而Kotlin默认为public

Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就只会在声明它们的文件中可见。

修饰符 类成员 顶层声明
public(默认) 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见 X
private 类中可见 文件中可见

注意protected修饰符在Java和Kotlin中的不同行为。

  • 在Java中,可以从同一个包中、同一个类中和它的子类中访问一个protected成员。

  • 在Kotlin中,protected成员只在类和它的子类中可见。

记住一个通用的规则:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性。

internal open class TalkativeButton : Focusable {
    private fun yell() = println("yell")
    protected fun whisper() = println("whisper")
}

// 错误:public成员暴露了internal的接收者类型
fun TalkativeButton.speech() {
    // 错误:类的扩展函数不能访问它的private和protected成员
    yell()
    whisper()
}
1
2
3
4
5
6
7
8
9
10
11

内部类和嵌套类

内部类和嵌套类都是一个在另一个类的内部定义的类。区别在于内部类存储外部类的引用,而嵌套类没有。

在Kotlin中,在类的内部定义的类默认为嵌套类。而Java默认为内部类。

内部类和嵌套类在Java和Kotlin中的对应关系如下。

在Java中 在Kotlin中
内部类 class A inner class A
嵌套类 static class A class A

在Kotlin的内部类中,需要使用this@外部类名来访问外部类。

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

密封类

使用sealed修饰符标注的类叫作密封类。它对子类的创建有着严格的限制:所有的直接子类必须嵌套在父类中。

sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}
1
2
3
4

注意,sealed修饰符隐含了这个类是一个open类,不再需要显式地添加open修饰符。

如果你在when表达式中处理所有sealed类的子类,那么你就不再需要提供默认分支(else ->分支)。

fun eval(e: Expr): Int = 
    when(e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
1
2
3
4
5

当你在when中使用sealed类,并且sealed类添加了一个新的子类时,有返回值的when表达式会出现编译失败,编译器会告诉你需要在哪些when代码块中添加新增的sealed类的子类的判断分支。

在Kotlin1.0中,sealed类的限制是相当严格的。例如,所有的子类必须是嵌套的,并且子类不能创建为data类。而Kotlin1.1解除了这些限制,并允许在同一个文件的任何位置定义sealed类的子类。

类的构造方法

主/从构造方法

在Java中,一个类可以声明一个或多个构造方法。

Kotlin区分主构造方法和从构造方法。

  • 主构造方法是在类体外部声明,通常是主要而简洁的初始化类的方法。

  • 从构造方法是在类体内部声明的。

主构造方法和初始化语句块

constructor关键字用来开始一个主构造方法或从构造方法的声明。主构造方法声明在类体外部,而从构造方法声明在类体内部。

init关键字用来引入一个初始化语句块,它包含了在类被创建时执行的代码。因为主构造方法有语法限制,不能包含初始化代码,所以常使用初始化语句块。

class User constructor(_nickname: String) {
    val nickname: String
    
    init {
        nickname = _nickname
    }
}
1
2
3
4
5
6
7

如果主构造方法没有注解或可见性修饰符,可以去掉constructor关键字。另外,这里可以直接用主构造方法的参数来初始化属性,从而去掉init代码块。

class User(_nickname: String) {
    val nickname = _nickname
}
1
2
3

如果属性用相应的构造方法参数来初始化,代码可以通过把valvar关键字加在该参数前进行简化。

class User(val nickname: String)
1

可以像函数参数一样为构造方法参数声明一个默认值。另外,如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。

class User(val nickname: String,
        val isSubscribed: Boolean = true)
1
2

如果你的类具有一个父类,主构造方法同样需要初始化父类。

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

class GoogleUser(nickname: String) : User(nickname) { ... }
1
2
3

如果没有给一个类声明任何的构造方法,编译器会生成一个不做任何事情的默认构造方法。注意,它的子类必须显式地调用它的这个默认构造方法。

open class User { ... }

class GoogleUser : User() { ... }
1
2
3

可以将构造方法标记为private来确保类不被其他代码实例化。

class Secretive private constructor() {}
1

多个构造方法

大多数在Java中需要重载构造方法的场景都被Kotlin支持参数默认值和参数命名的语法涵盖了。

但还是会有需要多个构造方法的情景,最常见的是当你需要扩展一个框架类来提供多个构造方法,以便于通过不同的方式来初始化类。例如,Android的View类。

open class View {
    constructor(ctx: Context) { // 从构造方法
    }
    
    constructor(ctx: Context, attr: AttributeSet) { // 从构造方法
    }
}
1
2
3
4
5
6
7

子类可以通过super()关键字来调用对应的父类构造方法。

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

也可以使用this()关键字来从一个构造方法中调用同一个类中的另一个构造方法。

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

如果类没有主构造方法,那么每个从构造方法必须直接(super())或间接(this())地调用一个基类构造方法。

类的属性

属性的声明

可以在主构造方法中直接声明一个属性。

class User(val nickname: String)
1

也可以在类体内部声明一个属性。

class User {
    val nickname: String
}
1
2
3

通过override关键字来重写父类或接口的属性。

interface User {
    val nickname: String
}
1
2
3
class PrivateUser(override val nickname: String) : User

class GoogleUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

class FacebookUser(val account: String) : User {
    override val nickname: String = account
}
1
2
3
4
5
6
7
8
9
10

访问器与支持字段

在访问器的函数体中,可通过field标识符来访问支持字段。在getter中,只能读取field的值;而在setter中,既能读取它也能修改它。

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("$name's address was changed: $field -> $value")
            field = value
        }
}
1
2
3
4
5
6
7

访问器的可见性

属性的访问器的可见性默认与属性的可见性相同,但可以在setget关键字前放置可见性修饰符来修改对应访问器的可见性。

class Counter {
    var count: Int = 0
        private set
}
1
2
3
4

相同的JVM签名

Kotlin在实现Java接口时,属性的访问器可能会跟Java接口定义的方法冲突,出现“相同的JVM签名”错误。

public interface UserDetails {
    String getUsername();
}
1
2
3
// var username自动生成的getter与UserDetails接口定义的getUsername(),会因为JVM签名相同而报错
class Reader(var username: String = "") : UserDetails
1
2

解决方法是:用private声明属性,并手写实现Java接口的方法。

class Reader(private var username: String = "") : UserDetails {
    override fun getUsername(): String = username
}
1
2
3

通用对象方法

toString()

和Java一样,Kotlin中的所有类都提供了一种方式来获取类对象的字符串表示形式。

一个对象的字符串表示默认类似User@5e9f23b4,你可以重写它对应类的toString()来修改。

class User(val nickname: String) {
    override fun toString() = "User(nickname=$nickname)"
}
1
2
3

equals()

使用equals()来比较两个对象是否相等。

class User(val nickname: String) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is User) {
            return false
        }
        return nickname == other.nickname
    }
}
1
2
3
4
5
6
7
8

在Java中,可以使用==运算符来比较基本数据类型(比较两者的值)和引用类型(比较两者的引用)。而在Kotlin中,==运算符是通过调用equals()来比较两个值;要想进行引用比较,需要使用===运算符。

hashCode()

调用对象的hashCode()获取它的hash值。子类需要遵循Java同样的规则来重写这个方法,否则会影响对象在Hash容器中的表现。

class User(val name: String, val id: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + id
}
1
2
3

数据类

为类添加data修饰符来声明一个数据类。

data class User(val name: String, val id: Int)
1

数据类的作用

数据类为我们自动生成如下的东西:

  • equals()hashCode()

  • toString()

  • componentN()

  • copy()

通常将数据类的所有属性都声明为val,使它的实例不可变。然后外界通过数据类的copy()来获得该实例的一个副本。普通的不可变类也可以手动实现一个copy函数。

class User(val name: String, val id: Int) {
    fun copy(name: String = this.name,
            id: Int = this.id) {
        User(name, id)
    }
}
1
2
3
4
5
6

数据类的要求

主构造函数必须至少有一个参数。

主构造函数中的所有参数必须被标记为valvar。虽然数据类的属性可以是var,但强烈推荐只使用val属性,让数据类的实例不可变。

数据类不能有以下修饰符:abstractinneropensealed等。

在Kotlin1.1之前,数据类只能实现接口。从Kotlin1.1开始,数据类也可以继承其他类。

类委托

在Java中,可通过装饰者模式来为一个类添加一些行为。这种模式的本质是:

  • 创建一个新类,实现与原始类一样的接口,并将原始类的实例作为字段保存。

  • 这个新类可添加新的方法来支持新的行为。

  • 而与原始类拥有同样行为的方法不用被修改,只需要直接转发到原始类的实例上即可。

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)
        
    // 定义新行为...
}
1
2
3
4
5
6
7
8
9
10
11
12

按照Java做法来实现会出现相当多的样板代码,幸好Kotlin将委托作为一个语言级别的功能做了头等支持。可以使用by关键字将接口的实现委托给另一个对象,编译器会自动实现接口的方法并在方法的内部调用被委托对象的对应方法。

class DelegatingCollection<T>(
    innerList: Collection<T> = arrayListOf<T>()
) : Collection<T> by innerList {}
1
2
3

object关键字

常用object关键字来实现以下场景:

  • 对象声明。这是定义单例的一种方式。

  • 伴生对象。它可以持有工厂方法、与这个类相关但在调用时并不依赖类实例的方法。

  • 对象表达式。用来替代Java的匿名内部类。

object实现的这些场景都遵循着一个核心理念:object关键字定义一个类并同时创建一个实例。

对象声明

Kotlin使用对象声明为单例模式提供了最高级的语言支持。对象声明将类声明与该类的单一实例声明结合在一起。

object Payroll
1
public final class Payroll {
    public static final Payroll INSTANCE;

    static {
        Payroll var0 = new Payroll();
        INSTANCE = var0;
    }
}
1
2
3
4
5
6
7
8

与类一样,一个对象声明也可以包含属性、方法、初始化语句块等声明。唯一不允许的就是构造方法

object Payroll {
    val allEmployees = arrayListOf<Person>()
    
    fun calculateSalary() {
        for (person in allEmployees) {
            // ...
        }
    }
}
1
2
3
4
5
6
7
8
9

与变量一样,对象声明允许你使用对象名.来调用方法和访问属性。

Payroll.allEmployees.add(Person(...))

Payroll.calculateSalary()
1
2
3

对象声明同样可以继承自类和接口

可以在任何可以使用普通对象的地方使用单例对象。

可以在一个类的内部声明对象。这样的对象同样只有单一实例,不会在每个容器类的实例中具有不同的实例。

data class Person(val name: String) {
    // 使用对象声明定义的名称比较器是一个单例
    object NameComparator: Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int = 
            p1.name.compareTo(p2.name)
    }
}
1
2
3
4
5
6
7

Kotlin中的对象声明会被编译成通过静态字段来持有它的单一实例的类,这个字段名为INSTANCE

NameComparator.INSTANCE.compare(p1, p2);
1

伴生对象

Kotlin中的类不能拥有静态成员,Java的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数对象声明

  • 包级别函数,即顶层函数,在大多数情形下能够替代Java的静态方法。

  • 对象声明能够替代Java的静态方法和静态字段。

在大多数情况下,还是推荐使用顶层函数,但它不能访问类的privateprotected成员。

在类的内部定义的对象声明,可以加上companion关键字使其变成伴生对象。这样做的好处是:不再需要显式地指明对象的名称,可以直接通过容器类名称来访问这个对象的方法和属性。

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

注意,类的伴生对象成员在子类中不能被重写

伴生对象实现工厂模式

伴生对象可以访问类中的private成员,包括private构造方法,它是实现工厂模式的理想选择。

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) {
            User(email.substringBefore('@'))
        }
        
        fun newFacebookUser(account: String) {
            User(account)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

命名伴生对象

伴生对象不需要显式地指明名称,默认分配为Companion。但若需要,可以显式地对它进行命名。

class Person(val name: String) {
    companion object Loader {
        fun fromJson(jsonText: String): Person = ...
    }
}
1
2
3
4
5
Person.fromJson("{name: 'daking'}")

Person.Loader.fromJson("{name: 'daking'}")
1
2
3

伴生对象和静态成员

类的伴生对象会被编译成常规对象:类中的一个引用了它的实例的静态字段。

如果伴生对象没有命名,在Java代码中可通过Companion引用来访问;如果伴生对象有名字,那就用这个名字来替代Companion。

Java需要类中的成员是静态的,Kotlin可以在对应的成员上使用@JvmStatic注解来达到这个目的。可以在一个顶层属性或者声明在object中的属性上使用@JvmField注解来声明一个static字段。

Kotlin可以使用与Java相同的语法来访问Java类中声明的静态方法和字段。

伴生对象实现接口

伴生对象可以实现接口,并且可以直接将包含它的类的名称当作实现了该接口的对象实例来使用。

interface JSONFactory<T> {
    fun fromJson(jsonText: String): T
}
1
2
3
class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJson(jsonText: String): Person = ...
    }
}
1
2
3
4
5
fun loadFromJson<T>(factory: JSONFactory<T>): T { ... }
1
loadFromJson(Person) // 直接将Person的名字作为JSONFactory的实例
1

伴生对象扩展

可以为伴生对象定义一个扩展函数,使包含它的类具有新的行为。

class Person(val name: String) {
    companion object {
    }
}

fun Person.Companion.fromJson(jsonText: String): Person {
    ...
}
1
2
3
4
5
6
7
8
val p = Person.fromJson(jsonText)
1

对象表达式

object关键字不仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象替代了Java中匿名内部类的用法。

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

与对象声明不同,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。其实,匿名对象本质上是定义一个匿名类并同时创建一个实例

与Java匿名内部类只能扩展一个类或实现一个接口不同,Kotlin的匿名对象可以实现多个接口或不实现接口

与Java的匿名类一样,在对象表达式中可访问创建它的函数中的变量。但与Java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。