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.

Sources

  1. Kotlin Docs, Data Classes
  2. Kotlin Docs, Destructuring Declarations


Get new posts in your inbox


icon by smalllikeart