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.
From our partners:
But, you get several advantages over regular classes:
<strong class="hz kc">toString()</strong>
,<strong class="hz kc">equals()</strong>
and<strong class="hz kc">hashCode()</strong>
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 implementinghashCode
, forgetting to implementhashCode
when you’re implementingequals
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 <strong class="hz kc">val</strong>
or <strong class="hz kc">var</strong>
properties. Trying to add a parameter without val
/var
leads to a compile error.
As a best practice, consider using val
s instead of var
s, 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 <a class="cm ix" href="https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Intrinsics.java#L166" target="_blank" rel="noopener nofollow noreferrer">Intrinsics.areEqual</a>
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.
For enquiries, product placements, sponsorships, and collaborations, connect with us at [email protected]. We'd love to hear from you!
Our humans need coffee too! Your support is highly appreciated, thank you!