Start learning free

LESSON 153 OF 348 · KOTLIN ESSENTIALS

Data Classes in Kotlin: toString, equals and copy

Lesson Read the full explanation

Most of the classes you will ever write do one humble job: hold data. A User is a name and an age; a Point is an x and a y. In Kotlin you can declare such a class in a single line — the properties live right in the parentheses (the primary constructor), no body needed:

class User(val name: String, val age: Int)

But a plain class only stores the data — it doesn't know how to show it or compare it. Every class inherits two defaults: toString prints the class name plus a memory address, and == asks "are these the very same object?" — not "do they hold the same values?".


The trap:

Sooner or later, every Kotlin beginner prints an object, or compares two objects built from identical values — and gets this:

val a = User("Ana", 25)
val b = User("Ana", 25)
println(a)       // expecting the data...
println(a == b)  // expecting true...
User@1540e19d
false

No name, no age — just User@ and a hex address. And two users with the exact same data are "not equal", because the default equals compares identity, not contents.


The fix — one keyword:

Mark the class with data and the compiler generates the useful versions for you, straight from the primary-constructor properties:

data class User(val name: String, val age: Int)

println(User("Ana", 25))                    // User(name=Ana, age=25)
println(User("Ana", 25) == User("Ana", 25)) // true — compares values

You also get copy for free: it clones an object while changing only the properties you name — ana.copy(name = "Bob") keeps the age and swaps the name. Since the properties are val, this is the idiomatic way to get a "changed" version without touching the original.

One rule to remember: every parameter in a data class's primary constructor must be a property. Forget the val and the compiler stops you:

error: primary constructor of data class must only have property ('val' / 'var') parameters.
data class User(name: String)
                ^^^^^^^^^^^^

Assignment
Follow the Coding Tutorial to see all three freebies in action:

  • Declare data class User with val name: String and val age: Int.
  • Create ana = User("Ana", 25) and print her — the generated toString does the formatting.
  • Create twin from the same values and print whether ana == twin.
  • Make bob with ana.copy(name = "Bob") and print him.

The exact expected output is:

ana = User(name=Ana, age=25)
same = true
bob = User(name=Bob, age=25)

Hint
If your output looks like User@1540e19d, you forgot the data keyword. To embed a comparison in a printed string, wrap it in braces: ${ana == twin}.


STEP 1 OF 7
    Ctrl + Enter to check

    KOTLIN · KOTLIN ESSENTIALS

    Data Classes in Kotlin, explained

    The problem with plain classes

    Many classes exist only to hold values: a user with a name and an age, a point with x and y. Kotlin lets you declare such a class in one line, with the properties in the primary constructor:

    class User(val name: String, val age: Int)
    

    A plain class stores that data but doesn't know how to show it or compare it. Two defaults get in the way:

    • toString prints the class name and a hash code instead of the properties.
    • == calls equals, which for a plain class asks whether both sides are the same object, not whether they hold the same values.

    So this code surprises beginners:

    val a = User("Ana", 25)
    val b = User("Ana", 25)
    println(a)
    println(a == b)
    

    It prints a line like User@1540e19d (the part after @ varies) and then false. Two users built from identical values are "not equal", because they are two separate objects.

    The fix: the data keyword

    Put data in front of class and the compiler writes the useful versions for you, based on the properties in the primary constructor:

    data class User(val name: String, val age: Int)
    
    println(User("Ana", 25))
    println(User("Ana", 25) == User("Ana", 25))
    
    User(name=Ana, age=25)
    true
    

    A data class gets these generated members:

    • toString, which lists the class name and every property as name=value.
    • equals and hashCode, which compare and hash the property values. That is why equal data gives == a true result, and why equal objects can be found in a HashSet or used as HashMap keys.
    • copy, which makes a new object and lets you change some properties.
    • componentN functions, which allow destructuring: val (n, a) = ana gives n = "Ana" and a = 25.

    The identity check is still available: === asks whether two references point to the very same object. For two separately built users it is false, even when == is true.

    copy: a changed version without changing the original

    When properties are val, an object can't be modified after it is built. To get a variation, call copy and name only what should differ:

    val ana = User("Ana", 25)
    val bob = ana.copy(name = "Bob")
    println(bob)
    println(ana)
    
    User(name=Bob, age=25)
    User(name=Ana, age=25)
    

    The age carries over, and ana is untouched. Properties you leave out keep their current values, and ana.copy() with no arguments makes an equal duplicate.

    Rules and gotchas

    • Every primary-constructor parameter must be a property. Writing data class User(name: String) without val or var is a compile error: primary constructor of data class must only have property ('val' / 'var') parameters.
    • Only constructor properties count. A property declared inside the class body is left out of toString, equals, hashCode and copy. Two objects that differ only in such a property are still equal.
    • copy is shallow. If a property holds a mutable list, the copy and the original share that same list, so changing the list through one shows in the other.
    • Prefer val. A data class may use var properties, but changing a property of an object that sits in a HashSet or is a HashMap key changes its hash code, and the collection can no longer find it. Read-only properties plus copy avoid the problem.

    The exercise

    The lesson asks for a program that shows all three generated members and prints exactly this:

    ana = User(name=Ana, age=25)
    same = true
    bob = User(name=Bob, age=25)
    

    The steps:

    1. Declare the data class at the top, with val name: String and val age: Int. One line is enough, because a data class needs no body.
    2. Create ana by calling the constructor like a function. Kotlin has no new keyword.
    3. Print her. Inside a string template, $ana calls the generated toString.
    4. Create twin from the same name and age. It is a separate object.
    5. Print the comparison. An expression inside a template needs braces: ${ana == twin}. Without them, $ana == twin would insert ana and leave the rest as plain text.
    6. Derive bob with ana.copy(name = "Bob") and print it like the first user.
    data class User(val name: String, val age: Int)
    val ana = User("Ana", 25)
    println("ana = $ana")
    val twin = User("Ana", 25)
    println("same = ${ana == twin}")
    val bob = ana.copy(name = "Bob")
    println("bob = $bob")
    

    Key takeaways

    • Add data to a class that mainly holds values to get toString, equals, hashCode, copy and destructuring.
    • With a data class, == compares property values; === compares object identity.
    • Use copy(...) to build a modified version of an immutable object.
    • Only the primary-constructor properties take part, and each must be marked val or var.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free