一隻小奶狗會有名字、品種以及一堆可愛的特色做爲其屬性。若是將其建模爲一個類,而且只用來保存這些屬性數據,那麼您應當使用數據類。在使用數據類時,編譯器會爲您自動生成 toString()
、equals()
與 hashCode()
函數,並提供開箱即用的 解構 與拷貝功能,從而幫您簡化工做,使您能夠專一於那些須要展現的數據。接下來本文將會帶您瞭解數據類的其餘好處、限制以及其實現的內部原理。html
聲明一個數據類,須要使用 data 修飾符並在其構造函數中以 val 或 var 參數的形式指定其屬性。您能夠爲數據類的構造函數提供默認參數,就像其餘函數與構造函數同樣;您也能夠直接訪問和修改屬性,以及在類中定義函數。java
但相比於普通類,您能夠得到如下幾個好處:git
toString()
、equals()
與 hashCode()
函數 ,從而避免了一系列人工操做可能形成的小錯誤,例如: 忘記在每次新增或更新屬性後更新這些函數、實現 hashCode
時出現邏輯錯誤,或是在實現 equals
後忘記實現 hashCode
等;/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ data class Puppy( val name: String, val breed: String, var cuteness: Int = 11 ) // 建立新的實例 val tofuPuppy = Puppy(name = "Tofu", breed = "Corgi", cuteness = Int.MAX_VALUE) val tacoPuppy = Puppy(name = "Taco", breed = "Cockapoo") // 訪問和修改屬性 val breed = tofuPuppy.breed tofuPuppy.cuteness++ // 解構 val (name, breed, cuteness) = tofuPuppy println(name) // prints: "Tofu" // 拷貝:使用與 tofuPuppy 相同的品種和可愛度建立一個小狗,但名字不一樣 val tacoPuppy = tofuPuppy.copy(name = "Taco")
數據類有着一系列的限制。github
數據類是做爲數據持有者被建立的。爲了強制執行這一角色,您必須至少傳入一個參數到它的主構造函數,並且參數必須是 val 或 var 屬性。嘗試添加不帶 val 或 var 的參數將會致使編譯錯誤。數組
做爲最佳實踐,請考慮使用 val
而不是 var
,來提高不可變性,不然可能會出現一些細微的問題。如使用數據類做爲 HashMap
對象的鍵時,容器可能會由於其 var
值的改變而獲取出無效的結果。jvm
一樣,嘗試在主構造函數中添加 vararg
參數也會致使編譯錯誤:函數
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ data class Puppy constructor( val name: String, val breed: String, var cuteness: Int = 11, // 錯誤:數據類的的主構造函數中只能包含屬性 (val 或 var) 參數 playful: Boolean, // 錯誤:數據類型的主構造函數已禁用 vararg 參數 vararg friends: Puppy )
vararg
不被容許是因爲 JVM 中數組和集合的 equals()
的實現方法不一樣。Andrey Breslav 的解釋是:this
集合的 equals() 進行的是結構化比較,而數組不是,數組使用
equals()
等效於判斷其引用是否相等: this===
other。code*閱讀更多: https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/component
數據類能夠繼承於接口、抽象類或者普通類,可是不能繼承其餘數據類。數據類也不能被標記爲 open
。添加 open
修飾符會致使錯誤: Modifier ‘open’ is incompatible with ‘data’ (‘open’ 修飾符不兼容 ‘data’)
。
爲了理解這些功能爲什麼可以實現,咱們來檢查下 Kotlin 究竟生成了什麼。爲了作到這點,咱們須要查看反編譯後的 Java 代碼: Tools -> Kotlin -> Show Kotlin Bytecode
,而後點擊 Decompile
按鈕。
就像普通的類同樣,Puppy
是一個公共 final
類,包含了咱們定義的屬性以及它們的 getter 和 setter:
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ public final class Puppy { @NotNull private final String name; @NotNull private final String breed; private int cuteness; @NotNull public final String getName() { return this.name; } @NotNull public final String getBreed() { return this.breed; } public final int getCuteness() { return this.cuteness; } public final void setCuteness(int var1) { this.cuteness = var1; } ... }
咱們定義的構造函數是由編譯器生成的。因爲咱們在構造函數中使用了默認參數,因此咱們也獲得了第二個合成構造函數。
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ public Puppy(@NotNull String name, @NotNull String breed, int cuteness) { ... this.name = name; this.breed = breed; this.cuteness = cuteness; } // $FF: synthetic method public Puppy(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) { if ((var4 & 4) != 0) { var3 = 11; } this(var1, var2, var3); } ... }
Kotlin 會爲您生成 toString()
、hashCode()
與 equals()
方法。當您修改了數據類或更新了屬性以後,也能自動爲您更新爲正確的實現。就像下面這樣,hashCode()
與 equals()
老是須要同步。在 Puppy
類中它們以下所示:
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ ... @NotNull public String toString() { return "Puppy(name=" + this.name + ", breed=" + this.breed + ", cuteness=" + this.cuteness + ")"; } public int hashCode() { String var10000 = this.name; int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31; String var10001 = this.breed; return (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31 + this.cuteness; } public boolean equals(@Nullable Object var1) { if (this != var1) { if (var1 instanceof Puppy) { Puppy var2 = (Puppy)var1; if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.breed, var2.breed) && this.cuteness == var2.cuteness) { return true; } } return false; } else { return true; } } ...
toString
和 hashCode
函數的實現很直接,跟通常您所實現的相似,而 equals 使用了 Intrinsics.areEqual 以實現結構化比較:
public static boolean areEqual(Object first, Object second) { return first == null ? second == null : first.equals(second); }
經過使用方法調用而不是直接實現,Kotlin 語言的開發者能夠得到更多的靈活性。若是有須要,他們能夠在將來的語言版本中修改 areEqual 函數的實現。
爲了實現解構,數據類生成了一系列只返回一個字段的 componentN()
方法。component 的數量取決於構造函數參數的數量:
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ ... @NotNull public final String component1() { return this.name; } @NotNull public final String component2() { return this.breed; } public final int component3() { return this.cuteness; } ...
您能夠經過閱讀咱們以前的 Kotlin Vocabulary 文章 來了解更多有關解構的內容。
數據類會生成一個用於建立新對象實例的 copy()
方法,它能夠保持任意數量的原對象屬性值。您能夠認爲 copy()
是個含有全部數據對象字段做爲參數的函數,它同時用原對象的字段值做爲方法參數的默認值。知道了這一點,您就能夠理解 Kotlin 爲何會建立兩個 copy()
函數: copy
與 copy$default
。後者是一個合成方法,用來保證參數沒有傳值時,能夠正確地使用原對象的值:
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ ... @NotNull public final Puppy copy(@NotNull String name, @NotNull String breed, int cuteness) { Intrinsics.checkNotNullParameter(name, "name"); Intrinsics.checkNotNullParameter(breed, "breed"); return new Puppy(name, breed, cuteness); } // $FF: synthetic method public static Puppy copy$default(Puppy var0, String var1, String var2, int var3, int var4, Object var5) { if ((var4 & 1) != 0) { var1 = var0.name; } if ((var4 & 2) != 0) { var2 = var0.breed; } if ((var4 & 4) != 0) { var3 = var0.cuteness; } return var0.copy(var1, var2, var3); } ...
數據類是 Kotlin 中最經常使用的功能之一,緣由也很簡單 —— 它減小了您須要編寫的模板代碼、提供了諸如解構和拷貝對象這樣的功能,從而讓您能夠專一於重要的事: 您的應用。