A Puppy has a name, a breed and a whole lot of cuteness. To model a class that holds just data, you should use a data class. The compiler simplifies your work by auto generating toString()equals() and hashCode() for you and providing destructuring and copy functionality out of the box letting you focus on the data you need to represent. Read on to learn more about other advantages of data classes, their restrictions and to take a look under the hood of how they’re implemented.

Usage overview

To declare a data class, use the data modifier and specify the properties of the class as val or var parameters in the constructor. As with any function or constructor, you can also provide default arguments, you can directly access and modify properties and define functions inside your class.

But, you get several advantages over regular classes:

  • toString()equals() and hashCode() are implemented for you, by the Kotlin compiler, avoiding a series of small human errors that can cause bugs: like forgetting to update them every time you add or update your properties, logic mistakes when implementing hashCode, forgetting to implement hashCode when you’re implementing equals and more.
  • destructuring
  • ease of copying by calling the copy() method:
/* Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */

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

// create new instances
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++


// destructuring
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")

 

Restrictions

Data classes come with a series of restrictions.

 

Constructor parameters

Data classes were created as a data holder. To enforce this role, you have to pass at least one parameter to the primary constructor and the parameters need to be val or var properties. Trying to add a parameter without val/var leads to a compile error.

As a best practice, consider using vals instead of vars, to promote immutability. Otherwise, subtle issues can appear for example when using data classes as keys for HashMap objects, as the container can get in an invalid state when the var value changes.

Similarly, trying to add a vararg parameter in the primary constructor leads to a compile error as well:

/* 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: Data class primary constructor must have only property (val / var) parameters         
  playful: Boolean,
  // error: Primary constructor vararg parameters are forbidden for data classes
   vararg friends: Puppy 
)

vararg is not allowed due to how equals() works on the JVM for arrays and collections. Andrey Breslav explains:

Collections are compared structurally, while arrays are not, equals() for them simply resorts to referential equality: this === other.

Inheritance

Data classes can inherit from interfaces, abstract classes and classes but cannot inherit from other data classes. Data classes also can’t be marked as open. For example, adding open will result in an error: Modifier ‘open’ is incompatible with ‘data’.

Under the hood

Let’s check what exactly does Kotlin generate to be able to understand how some of these features are possible. To do this, we’ll look at the Java decompiled code: Tools -> Kotlin -> Show Kotlin Bytecode then press the Decompile button.

Properties

Like with a regular class, Puppy is a public final class, containing the properties we defined and the getters and setters for them:

/* 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(s)

The constructor we defined is generated. Because we use a default argument in the constructor, then we get the 2nd synthetic constructor as well.

To find out more about default arguments and the generated code, check out this blog post.

/* 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 generates the toString()hashCode() and equals() methods. When you modify the data class, updating properties, the right method implementations are generated for you, automatically. Like this, you know that hashCode() and equals() will never be out of sync. Here’s how they look for our Puppy class:

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

While toString and hashCode are quite straightforward an look similar to the way you’d implement it, equals uses Intrinsics.areEqual that performs a structural equality:

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

By using a method call rather than the direct implementation, the Kotlin language developers get more flexibility, as they can change the implementation of areEqual in future language versions, if needed.

Components

To enable destructuring, data classes generate componentN() methods that just return a field. The component number follows the order of the 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;
   }
...

Find out more about destructuring from our Kotlin Vocabulary post.

Copy

Data classes generate a copy() method that can be used to create a new instance of the object, keeping 0 or more of the original values. You can think of copy() as a method that gets all fields of the data class as parameters, with the values of the fields as default values. Knowing this, you won’t be surprised that Kotlin generates 2 copy() methods: copy and copy$default. The latter is a synthetic method that ensures that when a value isn’t passed in for a parameter, then the right value is used from the base class:

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

Conclusion

Data classes are one of the most used Kotlin features and for a good reason — they decrease the boilerplate code you need to write, enable features like destructuring and copying an object and let you focus on what matters: your app.

This article is republished from Medium by Florina Muntenescu.

Previous 6 Ways To Delete Yourself From The Internet
Next Google Cloud And Managecore: Partnering To Accelerate SAP Cloud Migrations