What do data classes give you in Kotlin?
Data classes are a well-known feature in Kotlin. They’re appropriately named, since it’s designed for classes that hold data—that’s where the main value of this feature come into play.
When you mark a class with the data
keyword, the benefits are as follows.
toString()
The first and most dominant benefit is that data classes define a toString()
function for you. If it was just this, I’d be happy already. Its so simple, but everytime I use a data class I appreciate it.
Here’s the the toString
function in action:
data class Country(val name: String, val population: Int)
val norway = Country ("Norway", 5)
println(norway) // Country(name=Norway, population=5)
equals() and hashcode()
A second, nice benefit we get when using data classes is an equals()
and a hashCode()
method.
This equals()
refers to structural equality, meaning if two objects have all the same properties, count them as equal. They don’t have to sit at the same place in memory, i.e. literally be the same object. To use referential equality (memory location), use ===
.
If two objects are structurally equal, the value returned by their two corresponding hash code functions will be the same.
val a = Country("China", 100)
val b = Country("China", 100)
a.equals(b) // true
a == b // equivalent to `a.equals(b)`
a.hashCode() == b.hashCode() // true
Destructurability (componentN() Functions)
Oftentimes you’ll want to destructure values from an object. We can easily use destructuring with an object whose type is a data class:
val (name, population) = norway
Can we do this with a normal class?
class State(val name: String, val population: Int)
val chile = State("Chile", 18)
val (name, population) = chile
// destructuring declaration initializer of type (...).State must have a 'component1()' function.
When we destructure, it gets compiled to something called componentN()
functions. They have to be present on the object for the object to be destructurable. Data classes put them there.
val name = norway.component1()
val population = norway.component2()
// same as
val (name, population) = norway
How many of these are generated? The compiler only uses the properties included in the primary constructor, when you define the class. So in this case, there’s just 2.
Note that if you use an underscore, the componentN()
function doesn’t even get called.
val (_, population) = norway
// Compiles to
val population = norway.component2()
Copy
Another benefit is data classes automatically define a copy method, allowing us to copy some or all of the properties.
val macedonia = Country("Macedonia", 2)
val northMacedonia = macedonia.copy(name = "North Macedonia")
This is useful if you want to write a pure function, and you’re returning an output that’s only a slightly altered version of the endpoint.
data class Graph<T>(
val vertices: Set<T>,
val edges: Map<T, Set<T>>
)
fun reverseGraph(g: Graph<T>): Graph<T> {
val reversedEdges = // TODO
return g.copy(edges = reversedEdges)
}
Without the data
keyword
Just to finalize our understanding, let’s look at all the stuff we’d have to do if the data
keyword didn’t exist.
class Country(val name: String, val population: Int) {
override fun toString(): String {
return "Country(name=" + this.name + ", population=" + this.population + ")"
}
override fun hashCode(): Int {
return (this.name?.hashCode() ?: 0) * 31 + this.population
}
override fun equals(other: Any?): Boolean {
if (this !== other) {
if (other is Country) {
val otherCountry = other as Country?
if ((this.name == otherCountry!!.name) && (this.population == otherCountry.population)) {
return true
}
}
return false
} else {
return true
}
}
operator fun component1(): String {
return this.name
}
operator fun component2(): Int {
return this.population
}
fun copy(name: String, population: Int): Country {
return Country(name, population)
}
}
When should you not use data classes?
Whenever some feature seems too good to be true, like database indexes or exactly once processing in Kafka, remember nothing comes for free.
Here’s an interesting article measuring the cost of the data class, but I would still strongly advocate for making all model / entity / schema classes data classes.
Summary
One of the goals of the Kotlin language designers was to have a concise but understandable language. Clearly, one of the chief complaints about Java is that it is verbose. With data
classes, Kotlin eliminates a ton of unnecessary code, in a simple and easy to use way.