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