Kobular data

A baby suckling dog will have a name, breed and a bunch of lovely characteristics as its attributes. If you model it as a class and only use it to hold these attribute data, you should use the data class. When using data classes, the compiler will automatically generate toString(), equals() and hashCode() functions for you, and provide them out of the box deconstruction And copy function, which helps you simplify your work and enable you to focus on the data that needs to be displayed. Next, this article will take you through the other benefits, limitations and internal principles of data classes.

Usage overview

To declare a data class, you need to use the data modifier and specify its attributes in the form of val or var parameters in its constructor. You can provide default parameters for the constructor of the data class, just like other functions and constructors; You can also directly access and modify properties and define functions in classes.

However, compared with ordinary classes, you can get the following benefits:

  • The Kotlin compiler has implemented the toString(), equals() and hashCode() functions for you by default, thus avoiding small errors that may be caused by a series of manual operations, such as forgetting to update these functions after adding or updating attributes each time, logic errors when implementing hashCode, or forgetting to implement hashCode after implementing equals;
  • Deconstruction;
  • It is easy to copy through 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 loveliness as tofuppuppy, but with a different name
val tacoPuppy = tofuPuppy.copy(name = "Taco")

limit

Data classes have a series of limitations.

constructors parameters

Data classes are created as data holders. To enforce this role, you must pass in at least one parameter to its main constructor, and the parameter must be 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, otherwise some subtle problems may arise. If the data class is used as the key of HashMap object, the container may get invalid results due to the change of its var value.

Similarly, trying to add vararg parameter to the main constructor will also lead to 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 primary constructor of a data class can only contain attribute (val or var) parameters         
  playful: Boolean,
  // Error: vararg parameter is disabled in the primary constructor of data type
   vararg friends: Puppy 
)

vararg is not allowed because the implementation of equals() of arrays and collections in the JVM is different. Andrey Breslav explained:

The equals() of a collection is a structured comparison, not an array. Using equals() of an array is equivalent to judging whether its references are equal: this === other.

*Read more: https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/

inherit

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

Internal implementation

To understand why these functions can be implemented, let's check what Kotlin generates. To do this, we need to check the decompiled Java code: Tools - > Kotlin - > show Kotlin bytecode, and then click the decode button.

attribute

Just like ordinary classes, Puppy is a public final class, which contains the properties defined by us and their getter s and setter s:

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

Constructor

The constructor we define is generated by the compiler. Since we use default parameters 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);
   }
...
}

toString(), hashCode(), and equals()

Kotlin will generate toString(), hashCode() and equals() methods for you. When you modify the data class or update the properties, it can also be automatically updated to the correct implementation for you. As follows, hashCode() and equals() always need to be synchronized. In the purchase class, they are as follows:

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

The implementation of toString and hashCode functions is very direct, similar to what you usually implement, while equals uses Intrinsics.areEqual Structured comparison to achieve:

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

Developers of the Kotlin language can gain more flexibility by using method calls rather than direct implementations. If necessary, they can modify the implementation of areEqual function in future language versions.

Component

In order to realize 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 parameters:

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

You can read our previous Kobulary article To learn more about deconstruction.

Copy

The data class generates a copy() method for creating a new object instance, which can maintain any number of original object attribute values. You can think of copy() as a function that contains all the data object fields as parameters. It also uses the field value of the original object as the default value of the method parameters. Knowing this, you can understand why Kotlin created two copy() functions: copy and copy$default. The latter is a synthesis method to ensure that the value of the original object can be used correctly when the parameter is not transferred:

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

summary

Data classes are one of the most commonly used functions in Kotlin for a simple reason - it reduces the template code you need to write and provides functions such as deconstructing and copying objects, so that you can focus on what's important: your application.

Keywords: Java Apache

Added by vumpler on Fri, 04 Mar 2022 23:18:21 +0200