這邊來討論一下 data class,什麼是 data class,簡單來說就是會自動幫你生成一些實用或者標準的方法。
說明
舉個例子來說好了,假設現在有一個類別叫做 Person,如下面這樣宣告。
class Person(name: String, age: Int) {
private lateinit var userName: String
private var userAge: Int
init {
userName = name
userAge = age
}
}
接著對其反組譯看看 Java 會長怎樣?
public final class Person {
private String userName;
private int userAge;
public Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.userName = name;
this.userAge = age;
}
}
結果程式長出來跟我們做的事情差不多,只是把傳入的參數放置到指定的類別成員上,但是如果你想要有 getter/setter 的話就要再額外寫方法來處理。
不過 Kotlin 提供了另外一個方法可以讓你很輕鬆的就產出 getter/setter 了,只要將傳入建構子參數前面加上 val 它就會自動產生了,程式碼如下。
class Person(val name: String, val age: Int) {}
我們將其反組譯來看,可以看到在 Java 程式會長這樣。
public final class Person {
@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 Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
}
這邊我們可以發現,從建構子內傳進來的參數會自動轉化成 getter / setter,透過這樣我們就可以直接存取傳進來的變數。
但是我們會想,如果想要內建一些 toString、hashCode 以及 equals 這些方法呢?Kotlin 也提供了相關的語法可以讓你輕鬆操作,如果我們對於這個類別加入了 data 的宣告,如下面所示。
data class Person(val name: String, val age: Int)
接著我們對其反組譯來看看長怎樣。
public final class Person {
@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 Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(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 Person copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new Person(name, age);
}
// $FF: synthetic method
// $FF: bridge method
@NotNull
public static Person copy$default(Person 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);
}
public String toString() {
return "Person(name=" + this.name + ", age=" + this.age + ")";
}
public int hashCode() {
return (this.name != null?this.name.hashCode():0) * 31 + this.age;
}
public boolean equals(Object var1) {
if(this != var1) {
if(var1 instanceof Person) {
Person var2 = (Person)var1;
if(Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
你會發現除了剛剛講的三個方法以外,還有增加許多內建功能,比如說 copy、componentN等相關方法。
那我們來釐清一下到底發生什麼事情了?
首先,如果你透過建構子傳進來的參數,將會產生以下的幾種方法。
-
equals()
/hashCode()
-
toString()
會變成Person(name=givemepass, age=25)
-
會產生
componentN()
functions ,N 代表你有幾個成員變數是從建構子傳進來的,如果你是宣告在類別內部的話,就沒你的份。 -
copy()
有幾個重點可能需要注意一下。
-
如果你要宣告成 data class,那麼就必須至少傳入一個建構子參數。
-
傳入的參數必須標記成 val 或者 var
-
這個 class 不能是 abstract、open、sealed 或者 inner。
-
如果你要覆寫
equals()
、hashCode()
或toString()
這些方法,那麼就必須在 data class 的內部事先宣告,那麼就不會出現內建的方法了。
data class Person(val name: String) {
override fun toString(): String {
return "abc"
}
}
反組譯以後就會看到這個方法被你宣告的方法覆寫了。
@NotNull
public String toString() {
return "abc";
}
- copy 會直接回傳一個新的物件
@NotNull
public final Person copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new Person(name, age);
}
如果你想要複製一個物件可以這樣玩。
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val p1 = Person("givemepass1", 20)
val p2 = p1.copy("givemepass2", 30)
println(p1)
println(p2)
println("p1 == p2 :${p1 == p2}")
}
結果如下。
Person(name=givemepass1, age=20)
Person(name=givemepass2, age=30)
p1 == p2 :false
這邊有一個很有趣的例子。
data class Person(val name: String){
var age = 30
}
fun main(args: Array<String>) {
val p1 = Person("givemepass")
val p2 = Person("givemepass")
p1.age = 20
p2.age = 30
println("p1 == p2 :${p1 == p2}")
}
猜看看結果會是怎樣?
公佈答案,結果是 true。
p1 == p2 :true
為什麼啊?因為 data class 對於 equals 只限定於有傳入的參數,因此,如果你的成員是宣告在類別內部的,那麼就會被視為同一個物件。