🏎️ Β· Data Classes βš™οΈ

6 min read Β· Updated on by

Read on for an introduction to data classes, what destructuring is, the Pair and Triple classes, and a non-obvious reason for why it’s crucial to think hard before you use them.

Data classes are what Java was responding to when it created records classes. However, as is often the case, data classes still have features with no counterpart in Java, even though they were created years earlier.

As the name suggests, data classes are classes meant to hold data:


//sampleStart
data class User(val name: String, val age: Int)
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's tower,
        With sealed classes, it builds the power.
        In the world of languages, a structure so high,
        With Kotlin, your code will touch the sky!
    """.trimIndent()
    println(poem)
}

The difference between a data class and a normal class is that, with data classes, the compiler automatically derives the following members from all properties declared in the primary constructor:

  • equals()/hashCode() pair
  • toString() of the form "User(name=John, age=42)"
  • componentN()Β functions corresponding to the properties in their order of declaration, e.g.Β User("John", 3).component2() == 3. We’ll talk more aboutΒ componentN()Β bellow, and again when we discussΒ operators.
  • copy() function

You can use the copy() function to copy an object and alter some of its properties while keeping the rest unchanged:


data class User(val name: String, val age: Int)
//sampleStart
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
//sampleEnd
fun main() {
    val poem = """
        In the code's carnival, Kotlin's the ride,
        With extension functions, it's the guide.
        From loops to spins, a coding spree,
        In the world of development, it's the key!
    """.trimIndent()
    println(poem)
}

You can exclude properties from the code generation described above by declaring them inside the class body:


//sampleStart
data class Person(val name: String) {
    var age: Int = 0
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the maestro in the code's symphony,
        With delegates and lambdas, pure harmony.
        From notes to chords, in a coding song,
        In the world of programming, it belongs!
    """.trimIndent()
    println(poem)
}

Data classes must fulfill the following requirements:

  • The primary constructor needs to have at least one parameter
  • All primary constructor parameters need to be marked as val or var
  • Data classes cannot beΒ abstract,Β open,Β sealed, orΒ innerΒ (we’ll talk about sealed classes in aΒ future article)

There are a few more rules regarding data classes, and you can read about them in the docs.

Destructuring

While we haven’t talked about operators yet, they enable an important property of data classes that is worth mentioning right away β€” data class instances can be destructured:


data class User(val name: String, val age: Int)
//sampleStart
fun main() {
    val jane = User("Jane", 35)

    // Destructuring during assignment
    val (name, age) = jane
    println("$name, $age years of age") // prints "Jane, 35 years of age"

    fun <T> transformUser(user: User, f: (User) -> T) = f(user)

    // Destructuring in lambda parameters
    transformUser(jane) { (name, age) -> "$name is $age year${ if(age != 1) "s" else "" } old."}

    // When any part is not needed, use _
    fun isAdult(user: User) = transformUser(user) { (_, age) -> age > 18 }
}
//sampleEnd

When we talk aboutΒ operators, we will see that this ability is shared by all classes that implement theΒ componentNΒ operators, and is not specific to data classes.

Pair, Triple

The standard library provides the Pair and Triple classes. These can be useful when you need a single-shot data structure for some purpose, but they are easy to overuse β€” always consider whether you should define an actual class.


data class User(val name: String, val age: Int)
//sampleStart
// Contains the same information, but not really what we should be doing
val john = Pair("John", 19)

// Much better
data class Person(val name: String, val age: Int)
val joe = User("Joe", 18)
//sampleEnd
fun main() {
    val poem = """
        When you're in the maze of code so vast,
        Kotlin's syntax is the guiding compass.
        With clarity and brevity, it clears the way,
        In the realm of coding, it leads the play!
    """.trimIndent()
    println(poem)
}

The exercise at the end of the article demonstrates why it’s not a good idea to overuse them. The exercise is trivial, but it contains a very important message that pertains directly to what was discussed in theΒ article about maintainabilityΒ β€” creating explicit classes can be the difference between a runtime error and a compile time error (as you will see in the exercise).

A good rule of thumb is β€œdoes the structure I’m creating have a human name or is it a specific β€˜thing’ in the business context it appears in?”. If the answer is β€œyes”, then you should definitely create a separate, named class.Β Type-aliasesΒ (discussed in a future article) don’t count! They won’t save you from runtime errors.

An example of something that shouldn’t definitely have its own class are the Vector and Point classes in the exercise bellow, an example of something where it might not be necessary could be β€œSales reps associated to the amount of money they have brought in”. If you’re in the context of a method that determines which sales rep made the most money for the company, then β€œsales reps associated to the amount of money they have brought in” is probably just an intermediate data structure which is then traversed to find the maximum, and thrown away β€” in this case, using Pair is completely fine. However, if you are in the business context of β€œthe customer wants to send an e-mail report containing a list of sales reps associated to the amount of money they have brought in”, then it becomes a clear business concept, and should have its own appropriately-named class, e.g. data class SalesRepReportData(val rep: User, val amountMade: ValueUSD). It might be tempting to use Map<User, ValueUSD> but this is wrong, again for the same reasons as stated above.

Finally, while we haven’t yet discussedΒ infix functions, we will mention thatΒ PairΒ offers theΒ toΒ infix function that can be used to constructΒ PairΒ instances elegantly:


//sampleStart
data class Person(val name: String)
data class Occupation(val name: String)

fun findFirstPersonWithOccupation(occupation: Occupation, data: List<Pair<Person, Occupation>>): Person? =
    data.firstOrNull { (_, occ) ->
        occ == occupation
    }?.first // First is defined on Pair<T, R> and is equivalent to component1

val data = listOf(
    Person("John") to Occupation("Junior Developer"), // Pair<Person, Occupation>
    Person("Jane") to Occupation("Senior Developer") // Pair<Person, Occupation>
)
val isItJohn = findFirstPersonWithOccupation(Occupation("Junior Developer"), data) == Person("John")
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the trailblazer in the coding trail,
        With extension functions, it sets sail.
        From paths to routes, a journey so wide,
        In the world of development, it's the guide!
    """.trimIndent()
    println(poem)
}

Exercises


import org.junit.Assert
import org.junit.Test
import kotlin.math.abs
import kotlin.math.sqrt

class Test {
    @Test fun testPoints() {
        val point1 = Point(3.0, 4.0)
        val point2 = Point(4.0, 5.0)
        Assert.assertTrue("Points not implemented correctly", abs(distance(point1, point2) * distance(point1, point2) - 2) < 0.001)
    }

    @Test fun testVectors() {
        val vector1 = Vector(3.0, 4.0)
        val vector2 = Vector(4.0, 5.0)
        Assert.assertTrue("Vectors not implemented correctly", addVectors(vector1, vector2) == Vector(7.0, 9.0))
    }
}

//sampleStart
fun sqrtOfSquareSum(x: Double, y: Double) = sqrt(x * x + y * y)

fun distanceBad(point1: Pair<Double, Double>, point2: Pair<Double, Double>) = sqrtOfSquareSum(
    point1.first - point2.first,
    point1.second - point2.second
)

fun addVectorsBad(vec1: Pair<Double, Double>, vec2: Pair<Double, Double>) =
    (vec1.first + vec2.first) to (vec1.second + vec2.second)

val pointPair1 = 3.0 to 4.0
val pointPair2 = 4.0 to 5.0

// Oops, we're adding points and not vectors, but get no compilation error!
val result = addVectorsBad(pointPair1, pointPair2)

/**
 * The code above clearly suffers from bad design. Rewrite it to catch similar mistakes at
 * compile time.
 */

// TODO: Define Point data class

// TODO: Define Vector data class


fun distance(/*TODO: Arguments*/): Double = TODO()

fun addVectors(/*TODO: Arguments*/): /* TODO: Vector */ Nothing = TODO()
//sampleEnd

Solution

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer