Kotlin 类、对象和接口(三)——编译器生成的方法:数据类和类委托

Kotlin 类、对象和接口(一)——定义类继承结构
Kotlin 类、对象和接口(一)——定义类继承结构

Java 平台定义了一些需要在许多类中呈现的方法,并且通常是以一种很机械的方式实现,譬如 equals、hashCode 及 toString。幸运的是,Java IDE 可以将这些方法的生成自动化,所以通常不需要手动书写它们。这种情况下,代码库就包含了样板代码。Kotlin 的编译器领先了一步:它能将这些呆板的代码生成放到母后,并不会因为自动生成的结果导致源代码文件变得混乱。

通用对象方法

就像 Java 中的情况一样,所有的 Kotlin 类也有许多可能需要重写的方法:toString、equals 和 hashCode。先看一下一个简单的用来存储客户名字和邮编的 Client 类。

// 代码清单 3.1     Client 类的最初声明
class Client(val name: String, val postalCode: Int)

字符串表示:toString()

Kotlin 中所有类同 Java 一样,提供了一种方式来获取类对象的字符串表示形式,默认来说,一个对象的字符串表示形如 Client@5e9f23b4,这并不是十分有用。想要改变它,需要重写 toString 方法。

// 代码清单 3.2     为 Client 实现 toString()
class Client(val name: String, val postalCode: Int) {
    override fun toString(): String = "Client(name='$name', postalCode=$postalCode)"
}

对象相等性:equals()

所有关于 Client 类的计算都发生在其外部,这个类只是用来存储数据,这意味着简单和透明。尽管如此,也许还会有一些针对这个类行为的需求。例如,假设想要将包好相同数据的对象视为相等:

>>> val client1 = Client("Alice", 342562)
>>> val client1 = Client("Alice", 342562)
// 在 kotlin 中,== 检查对象是否相等,而不是比较引用。这里会编译成调用"equals"
>>> println(client1 == client2)
false

对象并不相等,意味着必须为 Client 类重写 equals。

==表示相等性
在 Java 中,可以使用 == 运算符来比较基本数据类型和引用类型。如果引用在基本数据类型上,Java 的 == 比较的是值,然而在引用类型上 == 比较的是引用。因此,在 Java 中,众所周知的实践是总是调用 equals。
在 Kotlin 中,== 运算符是比较两个对象的默认方式:本质上说它就是通过调用 equals 来比较两个值的。因此,如果 equals 在类中被重写了,就能够很安全地使用 == 来比较实例。要想进行引用比较,可以使用 === 运算符,这与 Java 中的 == 比较对象引用的效果一模一样。

修改后的 Client 类

// 代码清单 3.3     为 Client 实现 equals()
class Client(val name: String, val postalCode: Int) {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Client

        if (name != other.name) return false
        if (postalCode != other.postalCode) return false

        return true
    }
    override fun toString(): String = "Client(name='$name', postalCode=$postalCode)"
}

Hash 容器:hashCode()

hashCode 方法通常与 equals 一起重写。
创建一个元素的 set:一个名为 Alice 的客户。接着,创建一个新的包含相同数据的 Client 实例并检查它是否包含在 set 中。期望检查会返回 true,因为这两个实例是相等的,但实际上返回的是 false:

>>> val processed = hashSetOf(Client("Alice", 342562))
>>> println(processed.contains(Client("Alice", 342562)))
false

原因是 Client 类缺少了 hashCode 方法。因此它违反了通用的 hashCode 契约:如果两个对象相等,他们必须有着相同的 hash 值。processed set 是一个 HashSet,在 HashSet 中值是以一种优化过的方式来比较的:首先比较它们的 hash 值,然后只有当它们相等时才会去比较真正的值。上面例子中 Client 类的两个不同的实例有着不同的 hash 值,所以 set 认为它不包含第二个对象,即使 equals 会返回 true。
要修复这个问题,可以向类中添加 hashCode 的实现:

// 代码清单 3.4     为 Client 实现 hashCode()
class Client(val name: String, val postalCode: Int) {
    // ...
    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + postalCode
        return result
    }
}

现在这个类在所有的情况下都能按预期来工作了。


数据类:自动生成通用方法的实现

如果想要的类是一个方便的数据容器,需要重写这些方法:toString、equals 和 hasCode。通常来说这些方法的实现十分简单,很多 IDE 也能够自动生成它们,并确保它们的实现是正确且一致的。
不过,在 Kotlin 中不必再去生成这些方法了。如果为类添加 data 修饰符,必要的方法将会自动生成好。

// 代码清单 3.5     数据类 Client
data class Client(val name: String, val postalCode: Int)

这样就得到了一个重写了所有标准 Java 的类:

  • equals 用来比较实例
  • hashCode 用来作为例如 HashMap 这种基于哈希容器的键
  • toString 用来为类生成按声明顺序排列的所有字段的字符串表达式

equals 和 hashCode 方法会将所有在主构造方法中声明的属性纳入考虑。生成的 equals 方法会检测所有的属性值是否相等。hashCode 方法会返回一个根据所有属性生成的哈希值。没有在主构造方法中声明的属性将不会加入到相等性检查和哈希值计算中去。

数据类和不可变性:copy() 方法

虽然数据类的属性并没有要求是 val,同样可以使用 var,但还是强烈建议推荐只是用只读属性,让数据类的实例不可变。如果想使用这样的实例作为 HashMap 或者类似容器的键,这会是必需的要求。因为如果不这样,被用作键的对象在加入容器后被修改了,容器可能会进入一种无效的状态。不可变对象同样更容易理解,特别是在多线程代码中:一旦一个对象被创建出来,他会一直保持初始状态,也不用担心在代码工作时其他线程修改了对象的值。
为了让使用不可变对象的数据类变得更容易,Kotlin 编译器为它们多生成了一个方法:一个允许 copy 类的实例的方法,并在 copy 的同时修改某些属性的值。创建副本通常是修改实例的好选择:副本有着单独的生命周期并且不会影响代码中引用原始实例的位置。


类委托:使用 "by" 关键字

设计大型面向对象系统的一个常见问题就是由继承的实现导致的脆弱性。当扩展一个类并重写某些方法是,代码就变得依赖继承的那个类的实现细节。当系统不断演进并且基类的实现被修改或者新方法被添加进去是,做出的关于类行为的假设会失效,所以代码也许最后就以不正确的行为而告终。
Kotlin 的设计就识别了这样的问题,并默认将类视为 final 的。这确保了只有那些设计成可扩展的类可以被继承。当使用这样的类时,就会意识到它是开放的,就会注意这些修改需要与派生类兼容。
但是常常需要向其他类添加一些行为,即使它没有被设计为可扩展的。一个常用的实现方式以 装饰器 模式文明。这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不会被修改,只需要直接转发到原始类的实例。
这种方式的一个缺点是需要相当多的样板代码(像 Intellij IDEA 一样的众多 IDE 都有专门生成这样代码的功能)。Kotlin 将委托作为一个语言级别的功能做了头等支持。无论什么时候实现一个接口,都可以使用 by 关键字将接口的实现委托到另一个对象。

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

类中所有的方法都消失了,编译器会生成它们。当需要修改某些方法的行为是,可以重写它们,这样方法就会被调用而不是使用生成的方法。可以保留感到满意的委托给内部的实例中种默认实现。

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

推荐阅读更多精彩内容

  • Kotlin的类和接口与Java的类和接口是有一定的区别的。Kotlin的接口是可以包含属性声明。Kotlin默认...
    程自舟阅读 10,324评论 0 11
  • 写在开头:本人打算开始写一个Kotlin系列的教程,一是使自己记忆和理解的更加深刻,二是可以分享给同样想学习Kot...
    胡奚冰阅读 1,415评论 5 11
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,171评论 9 118
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 李小菲掩着双耳坐在座位上,可耳后还是传来“嗡嗡嗡”地议论声。她知道他们定是在讨论她,她的背僵了又僵,无奈地走出教室...
    欧嘉言阅读 7,068评论 272 274