一只小奶狗会有名字、种类以及一堆可恶的特点作为其属性。如果将其建模为一个类,并且只用来保留这些属性数据,那么您该当应用数据类。在应用数据类时,编译器会为您主动生成 toString()equals()hashCode() 函数,并提供开箱即用的 解构 与拷贝性能,从而帮您简化工作,使您能够专一于那些须要展现的数据。接下来本文将会带您理解数据类的其余益处、限度以及其实现的外部原理。

用法概览

申明一个数据类,须要应用 data 修饰符并在其构造函数中以 val 或 var 参数的模式指定其属性。您能够为数据类的构造函数提供默认参数,就像其余函数与构造函数一样;您也能够间接拜访和批改属性,以及在类中定义函数。

但相比于一般类,您能够取得以下几个益处:

  • Kotlin 编译器已为您默认实现了 toString()equals()hashCode() 函数 ,从而防止了一系列人工操作可能造成的小谬误,例如: 遗记在每次新增或更新属性后更新这些函数、实现 hashCode 时呈现逻辑谬误,或是在实现 equals 后遗记实现 hashCode 等;
  • 解构;
  • 通过 copy() 函数轻松进行拷贝。
/* 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.breedtofuPuppy.cuteness++// 解构val (name, breed, cuteness) = tofuPuppyprintln(name) // prints: "Tofu"// 拷贝:应用与 tofuPuppy 雷同的种类和可恶度创立一个小狗,但名字不同val tacoPuppy = tofuPuppy.copy(name = "Taco")

限度

数据类有着一系列的限度。

结构函数参数

数据类是作为数据持有者被创立的。为了强制执行这一角色,您必须至多传入一个参数到它的主构造函数,而且参数必须是 val 或 var 属性。尝试增加不带 val 或 var 的参数将会导致编译谬误。

作为最佳实际,请思考应用 val 而不是 var,来晋升不可变性,否则可能会呈现一些轻微的问题。如应用数据类作为 HashMap 对象的键时,容器可能会因为其 var 值的扭转而获取出有效的后果。

同样,尝试在主构造函数中增加 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 的解释是:

汇合的 equals() 进行的是结构化比拟,而数组不是,数组应用 equals() 等效于判断其援用是否相等: this === other。

*浏览更多: https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/

继承

数据类能够继承于接口、抽象类或者一般类,然而不能继承其余数据类。数据类也不能被标记为 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);   }...}

toString()、hashCode() 和 equals()

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;      }   }...

toStringhashCode 函数的实现很间接,跟个别您所实现的相似,而 equals 应用了 Intrinsics.areEqual 以实现结构化比拟:

public static boolean areEqual(Object first, Object second) {    return first == null ? second == null : first.equals(second);}

通过应用办法调用而不是间接实现,Kotlin 语言的开发者能够取得更多的灵活性。如果有须要,他们能够在将来的语言版本中批改 areEqual 函数的实现。

Component

为了实现解构,数据类生成了一系列只返回一个字段的 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() 函数: copycopy$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 中最罕用的性能之一,起因也很简略 —— 它缩小了您须要编写的模板代码、提供了诸如解构和拷贝对象这样的性能,从而让您能够专一于重要的事: 您的利用。