A puppy can have a name, breed, and a bunch of cute traits as attributes. If you model it as a class and only use it to hold the attribute data, you should use data classes. When working with data classes, the compiler automatically generates the toString(), equals(), and hashCode() functions for you and provides out-of-the-box deconstruction and copying capabilities to simplify your work and allow you to focus on the data you need to display. The rest of this article takes you through the other benefits, limitations, and internals of data classes.

An overview of the usage

To declare a data class, use the data modifier and specify its attributes as val or var arguments in its constructor. You can provide default arguments to the constructors of data classes, just like any other function and constructor; You can also access and modify properties directly, as well as define functions in classes.

But you get the following benefits over regular classes:

  • The Kotlin compiler is implemented by default for youtoString(),equals()hashCode()functionIn order to avoid a series of manual errors, such as forgetting to update these functions and implementations every time a property is added or updatedhashCodeA logic error occurs or is being implementedequalsPost forget implementationhashCodeAnd so on;
  • Deconstruction;
  • Copying is easy with the copy() function.
/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

data class Puppy(
        val name: String,
        val breed: String,
        var cuteness: Int = 11
)

// Create a new instance
val tofuPuppy = Puppy(name = "Tofu", breed = "Corgi", cuteness = Int.MAX_VALUE)
val tacoPuppy = Puppy(name = "Taco", breed = "Cockapoo")

// Access and modify properties
val breed = tofuPuppy.breed
tofuPuppy.cuteness++

/ / deconstruction
val (name, breed, cuteness) = tofuPuppy
println(name) // prints: "Tofu"

// Copy: Create a puppy with the same breed and cuteness as tofuPuppy, but with a different name
val tacoPuppy = tofuPuppy.copy(name = "Taco")
Copy the code

limit

Data classes have a number of limitations.

Constructor argument

Data classes are created as data holders. To enforce this role, you must pass at least one argument to its main constructor, and the argument must be a val or var attribute. Trying to add a parameter without val or var will result in a compilation error.

As a best practice, consider using VAL instead of VAR to improve immutability, or subtle problems may arise. If you use a data class as the key of a HashMap object, the container may get invalid results due to changes in its var value.

Similarly, trying to add the vararg argument to the main constructor results in a compilation error:

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

data class Puppy constructor(
    val name: String,
    val breed: String,
    var cuteness: Int = 11.// Error: The main constructor of a data class can only contain attributes (val or var)
  playful: Boolean.// Error: The vararg argument is disabled for the primary constructor of the data type
   vararg friends: Puppy 
)
Copy the code

Vararg is not allowed because the EQUALS () of arrays and collections are implemented differently in the JVM. Andrey Breslav explains:

Equals () of collections does structured comparisons, but arrays do not. Arrays useequals()Equivalent to determining whether references are equal: this= = =The other.

* read more: blog.jetbrains.com/kotlin/2015…

inheritance

Data classes can inherit from interfaces, abstract classes, or ordinary classes, but not from other data classes. Data classes cannot also be marked open. Adding the Open Modifier causes an error: the Modifier ‘open’ is incompatible with ‘data’.

Internal implementation

To understand why this is possible, let’s examine what Kotlin actually generates. To do this, we need to look at the decompiled Java code: Tools -> Kotlin -> Show Kotlin Bytecode, and then click the Decompile button.

attribute

Just like a normal class, Puppy is a public final class that contains the properties we defined along with their getters and setters:

/* 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; }... }Copy the code

The constructor

The constructor we define is generated by the compiler. Since we use default arguments in the constructor, we also get a second composite constructor.

/* 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); }... }Copy the code

ToString (), hashCode(), and equals()

Kotlin generates the toString(), hashCode(), and equals() methods for you. When you modify a data class or update a property, it automatically updates you to the correct implementation. As follows, hashCode() and equals() always need to be synchronized. In the Puppy class they look like this:

/* 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; }}...Copy the code

The toString and hashCode functions are implemented fairly directly, similar to what you would normally implement, while Equals uses Intrinsics. AreEqual to implement structured comparisons: Intrinsics.

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

By using method calls rather than direct implementations, Kotlin language developers gain more flexibility. They can change the implementation of the areEqual function in a future version of the language if necessary.

Component

To implement the deconstruction, the data class generates a series of componentN() methods that return only one field. The number of components depends on the number of constructor arguments:

/* 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; }...Copy the code

You can learn more about deconstruction by reading our previous Kotlin Vocabulary article.

copy

The data class generates a copy() method to create a new instance of the object, which can hold any number of original object property values. You can think of copy() as a function that takes all the data object fields as arguments, and uses the original object’s field values as the default method arguments. With this in mind, you can understand why Kotlin created two copy() functions: copy and copy$default. The latter is a synthetic method used to ensure that the value of the original object is used correctly when no value is passed:

/* 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;
      }

      returnvar0.copy(var1, var2, var3); }...Copy the code

conclusion

Data classes are one of the most commonly used features in Kotlin for a simple reason — they reduce the amount of template code you need to write and provide capabilities like deconstructing and copying objects so you can focus on what matters: your application.