Kotlin from the ground up

Classes and data classes

Grouping values with the behaviour that belongs to them, constructors that also declare properties, and the data class whose equals, text form and copy are written for you.

Part 11 of 12

Advanced19 min readBack to Courses

A class keeps related values together

A class describes a kind of thing, and each value made from it is an instance. class Counter(val label: String) says: a counter has a label, and the label is given once when you make one. That list of inputs in parentheses is the primary constructor, and it runs when the instance is built. Two instances made from the same class have the same shape and their own values.

Naming the parameter val does two things at once: it takes the input and it keeps it as a read-only property. Write var instead and the property can change afterwards. Inside the class, this is not needed when the name is unambiguous. The distinction matters at the call site: a val property can be read but not assigned, and the compiler enforces that for every instance.

Behaviour belongs next to its data

A class can hold functions too, and they can read the properties without being handed them. fun report() = label + ": " + count lives inside the class, so a caller writes counter.report() and never has to remember which values belong together. The method has access to the same instance the caller is holding, so it sees the current values, not a copy from construction.

That is the whole argument for a class: the label and the count stop being two loose variables that have to travel as a pair. When a value and the work on it are in the same place, most of the parameters in a file disappear.

A data class is a record

When a class exists mainly to hold values, mark it data. Kotlin then writes three things for you: a readable text form, so printing an instance shows its values rather than a memory address; equals, so two instances with the same values count as equal; and copy, which builds a new instance with the values you name replaced. It also writes hashCode, which keeps the equality consistent when the value is used as a map key.

copy is the one to reach for most. A record is easier to trust when it never changes, so a change becomes a new value: start.copy(x = 5) leaves start exactly as it was. That habit keeps a value you passed to a function safe from being changed behind your back. A data class can still hold var properties, but then the record guarantee is weaker: the values can change after construction.

What the compiler writes for a record

For a data class, the compiler generates equals, hashCode, toString, componentN and copy from the properties in the primary constructor. Properties declared in the class body are not part of that list, which is why two records that differ only in a body property can still compare equal. Keep the values that define the record in the constructor.

componentN is what makes destructuring work: val (x, y) = point calls component1() and component2(). The names are positional, not semantic, so a record with many properties is easier to read through its property names than through a long destructuring line. copy uses the same constructor, so a property added later gets a default only if you give it one.

A small class, and a record that copies

class Counter(val label: String) {
    var count = 0
    fun bump() {
        count += 1
    }
    fun report() = label + ": " + count
}

data class Point(val x: Int, val y: Int)

fun main() {
    val counter = Counter("visits")
    counter.bump()
    println(counter.report())       // visits: 1
    println(counter.count)          // 1

    val start = Point(2, 3)
    val moved = start.copy(x = 5)
    println(start)                  // Point(x=2, y=3)
    println(moved)                  // Point(x=5, y=3)
    println(start == Point(2, 3))   // true
}

A counter that changes, a point that copies

Run it and compare the two halves. Then add a third call to bump, print the counter again, and make a copy of the point that moves both numbers so you can see the original stay put.

Output
Click "Run" to execute your code

A class is a blueprint, an instance is a value

The primary constructor runs once, when an instance is built. A parameter marked val or var becomes a property that lives as long as the instance; a parameter without either is only available while the constructor runs, which is why it cannot be read from a method later. Properties declared in the class body, such as var count = 0, are initialised for each instance as well.

data tells the compiler the class is a record. It generates equals, hashCode, toString, componentN and copy from the properties in the primary constructor. Equality is then structural: two instances are == when their properties are equal. === still asks whether they are the same object, which is a different question and rarely the one a record is for.

Three class mistakes are easy to miss. A constructor parameter without val or var is not a property, so a method that refers to it will not compile. A data class with a var property still copies correctly, but the value can change after construction, which weakens the record guarantee. And == compares values while === compares identity — a data class makes the first useful, but it does not make the second true.

Try these next

  • Add a method to a plain class that reads two properties and returns a sentence.
  • Make two data-class values with the same contents and compare them with == and ===.
  • Copy a record twice, changing a different property each time, and print all three values.

A book, a shelf, and a copy

Run it and compare the two printed books. Then make a copy that changes the title, add a third book to the shelf, and check whether the copy equals the original.

Output
Click "Run" to execute your code

Print an instance of a class that is not a data class and you get the class name and a hash-looking number. That is the moment most people reach for the data keyword: the printed record is a debugging tool you get for free.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does the data keyword add to a class?