我正在参加「掘金·启航计划」。但凡接触过 Kotlin 的开发者,应该对 Data class 都不陌生。这种类的定义,常用于数据封装,使用起来也很方便。今天咱们来深入认识下。
基本说明
It is not unusual to create classes whose main purpose is to hold data. In such classes, some standard functionality and some utility functions are often mechanically derivable from the data. In Kotlin, these are called data classes and are marked with
data
上面是来自官网的基本说明,意思是:通常情况下,不应该构建一个类只专门用于持有数据。在 Kotlin 中,有专门的类型做这件事,即「数据类」,用关键字 data 声明。
比如:
data class User(val name: String, val age: Int)
这里定义了一个数据类 User,其包含两个数据域,没有成员方法。
既然是「数据类」,那 Data class 就必须包含数据的,要不然就失去了存在的意义,连 IDE 都会报错:
Data class must have at least one primary constructor parameter
就是告诉你:Data class 至少要包含一个数据参数
除此之外,构造 Data class 还有两点值得注意:
- 所有数据类的构造参数,都需要标为
val或var - 数据类不能是抽象的,不能被继承。不能是密封类,也不能是内部类(inner)
第一点,很好理解。因为数据类存在的意义,就是持有数据,如果不标为 val 或 var,那参数就为普通的「传递参数」,传给父类使用。这和数据类的设计初衷是相违背的。
第二点,说明了数据类的纯粹性和确定性。
数据类和普通类
那么,数据类和普通类,对于 Kotlin 来说,到底区别在哪儿?下面通过实际例子来对比分析下。
data class UserInfo(
val name: String,
val age: Int
)
class UserInfoEx(
val name: String,
val age: Int
)
上面定义了两个类:UserInfo 是数据类,包含一个 String 和一个 Int 数据域,而 UserInfoEx 是一个普通类,但是,也包含了相同的两个数据域。
首先,来看看编译器生成的 UserInfoEx 的 Java 代码:
public final class UserInfoEx {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public UserInfoEx(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
}
嗯,很常规的一个 Java 类,因为 val 的标记,两个数据域都成了 final 的数据,且只能 get。
再来看 UserInfo 的 Java 代码:
public final class UserInfo {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public UserInfo(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final UserInfo copy(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
return new UserInfo(name, age);
}
// $FF: synthetic method
public static UserInfo copy$default(UserInfo var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
@NotNull
public String toString() {
return "UserInfo(name=" + this.name + ", age=" + this.age + ")";
}
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof UserInfo) {
UserInfo var2 = (UserInfo)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
这份代码相较于普通类的,可丰富多了。
首先,编译器生成了 componentN 方法族,其实就是对应着数据域,相当于 get 的变种,按序的数据域 getter。然后自动生成了拷贝函数 copy 和描述函数 toString。
接着,生成了两个很重要函数:hashCode 和 equals。为什么重要呢?因为这才是数据类和普通有着根本区别的精髓所在。
我们先来看 equals:
public boolean equals(@Nullable Object var1) {
if (this != var1) {
// 不同对象
if (var1 instanceof UserInfo) {
UserInfo var2 = (UserInfo)var1;
// 相同的数据类型,且内部的域的值相同,则「相等」
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false; // 是不同类型,自然是「不等」
} else {
// 相对对象,肯定是「相等」的
return true;
}
}
总结一下就是:所有数据域的值相同的数据类,就是相等的。这在普通类是行不通的,因为普通类默认比的是对象地址。比如:
val uex = UserInfoEx("bonjour", 10)
val uex2 = UserInfoEx("bonjour", 10)
if (uex == uex2) {
println("two ex equal")
} else {
println("two ex not equal")
}
// 输出:two ex not equal
再来看 hashCode:
public int hashCode() {
String var10000 = this.name;
// 参数1(即String)取出 hashcode,乘以 31 后,与第二个参数的 hashcode 相加
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}
我们再加一个域会怎么样呢?
data class UserInfo(
val name: String,
val age: Int,
val info: UserInfoEx // 新加一个类类型数据
)
其对应的 hashCode 方法实现为:
public int hashCode() {
String var10000 = this.name;
int var1 = ((var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age)) * 31;
UserInfoEx var10001 = this.info;
// 前面一样,这里又加上了第三个参数的 hashCode
return var1 + (var10001 != null ? var10001.hashCode() : 0);
}
不论是基本类型,还是类类型,其 hashCode 的生成方法是固定的,所以对于数据类来说:因为有固定的 hashCode 计算过程,只要其数据是相同的,最终的 hashCode 也相同
由此,我们不难得出结论:数据类从值的角度讲,和基本类型等同。
小结
本篇内容很简单,但是也很重要。Kotlin 的 Data class,其在底层实现上,就达到了名副其实的目的:关注数据本身。看到 data class,更多地侧重于 data 而非 class,当成基础类型来用,就不会出问题了。