Kotlin 类、对象、接口摘要

接口

接口的方法可以有一个默认实现

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, protectedprivate 修饰符。

  • 但是默认的可见性是不一样的:如果你省略了修饰符,声明就是 public 的。

  • Java 中的默认可见性——包私有,在 Kotlin 中并没有使用。

  • 在 Java 中,你可以从同一个包中访问一个 protected 的成员,但 Kotlin 中protected 成员只在类和它的子类中可见。

  • 要注意的是类的扩展函数不能访问它的 privateprotected 成员。

嵌套类

  • 默认情况下Kotlin 的嵌套类不能访问外部类的实例,而 Java 中可以

  • Kotlin 中没有显式修饰符的嵌套类与 Java 中的 static 嵌套类是一样的

  • 要把它变成一个内部类来持有一个外部类的引用的话你需要使用 inner 修饰符

    类 A 在另一个类 B 中声明 在 Java 中 在 Kotlin 中
    嵌套类(不存储外部类的引用) static class A class A
    内部类(存储外部类的引用) class A inner class A

在 Kotlin 中引用外部类实例的语法也与 Java 不同。你需要使用 this@OuterInner 类去访问 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 修饰符,必要的方法将会自动生成toStringequalshashCode

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 是感受到最需要注意的地方,objectKotlin中的用法和重要。

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++ // 更新变量的值
        }
    })
    // ...
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容