JavaRush/Courses/Kotlin SELF/data class as a dat...

data class as a data model

Open

1. Getting to know data class

If you’ve ever written a class like “User(name, age)” or “Expense(amount, category)”, then you’ve almost certainly run into this situation: you create an object, you want to print it — and it comes out like Expense@6d06d69c. Then you want to compare two objects “by field values”, and the comparison suddenly doesn’t work the way you expect. And somewhere nearby a thought starts looming: “Alright, I’ll just write toString() and equals() by hand…” — and there you are, in the club of people who write the same thing for the hundredth time.

In data models, the same needs usually repeat. We want to quickly and readably print an object to the console (for debugging), compare two objects by fields (not by “this is the same object”), sometimes create “almost the same object, but with one field changed”, and sometimes it’s handy to “unpack” an object into several variables. Kotlin can automatically give you all of this — if you honestly tell the compiler: “This is a data class; the main thing here is the values.”

What is a data class

A data class is a regular Kotlin class, but marked with the data keyword. By doing this, you kind of sign a contract with the compiler: “My class primarily stores data, and its behavior should be standard and predictable.” In return, the compiler generates several methods that are needed in typical models almost all the time: printing, comparison, copying, and destructuring. This isn’t “magic”, but a very specific set of functions.

The minimal example looks like this:

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

fun main() {
    val u = User("Ann", 20)
    println(u) // User(name=Ann, age=20)
}

The nicest part of this example is that println(u) already works “like a human would expect”, because the compiler automatically made toString() in the form User(name=..., age=...).

2. What the compiler generates for a data class

From the outside, a data class looks small, but inside it saves you a lot of manual routine work. The compiler automatically generates a set of methods, and only based on the properties in the primary constructor (we’ll come back to this).

What gets generated How it feels in code Why you need it in real life
toString()
println(expense) shows “human-readable” text Debugging, logs, quick diagnostics
equals()
a == b compares by fields Checks for “same meaning”, searching/removing in collections
hashCode()
goes together with equals() Needed for correct behavior in Set/Map
copy()
val new = old.copy(amount = 500) “Update via copy”, convenient for immutable models
componentN()
allows val (x, y) = point Destructuring

toString() — printing an object for debugging

data class Product(val id: Int, val title: String)

fun main() {
    val p = Product(10, "Milk")
    println(p) // Product(id=10, title=Milk)
}

Here’s an important point: this is diagnostic output. It’s ideal for debugging, but it doesn’t have to be “pretty for the user” (user-facing output is usually done separately).

equals() and hashCode() — comparing by data

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

fun main() {
    val a = Point(1, 2)
    val b = Point(1, 2)

    println(a == b) // true
}

For a data class, the == operator compares the values of constructor properties (that is, it calls the generated equals). hashCode() is generated consistently with equals(), so these objects work correctly in a Set and as keys in a Map.

copy() — a copy with changes

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

fun main() {
    val u1 = User("Ann", 20)
    val u2 = u1.copy(age = 21)

    println(u1) // User(name=Ann, age=20)
    println(u2) // User(name=Ann, age=21)
}

Why it’s convenient: you don’t “mutate the object”; you create a new version.

componentN() and destructuring

data class Rect(val width: Int, val height: Int)

fun main() {
    val r = Rect(10, 20)
    val (w, h) = r

    println("w=$w, h=$h") // w=10, h=20
}

Destructuring is syntactic sugar, and “under the hood” there are methods component1(), component2(), etc.

3. data class rules

Only val/var in the primary constructor are considered

This is where many people stumble for the first time. In a data class, the compiler uses for auto-generation only the properties (val or var) from the primary constructor. If you declare a field in the class body, it will not participate in toString/equals/hashCode/copy/componentN. This is not a “bug”; it’s a way to explicitly say: “this field is not part of the model’s data.”

An example that looks innocent but leads to surprises:

data class User(val name: String) {
    var age: Int = 0
}

fun main() {
    val a = User("Ann").also { it.age = 10 }
    val b = User("Ann").also { it.age = 99 }

    println(a == b) // true
    println(a)      // User(name=Ann)
}

Why is that? Because age is declared in the body, not in the constructor, and by Kotlin rules it is excluded from auto-generated functions.

There is a healthy idea here: the state of a data model should be “officially” described in the constructor. Then you, the compiler, and the person reading the code all understand what exactly is considered the meaningful state of the object.

Requirements and limitations of a data class

The compiler can generate methods only if the class structure is clear enough and doesn’t contradict itself. That’s why a data class has several requirements.

You need at least one parameter in the primary constructor:

data class Empty() // will not compile: at least one parameter is required

Parameters must be val or var:

data class User(name: String, val age: Int) // will not compile: name is not val/var

You cannot make it open:

open data class User(val name: String) // will not compile

On a practical level, this is useful: a data class promises predictable equality, copying, and destructuring. Inheritance often makes these promises blurry, so Kotlin forbids some combinations so you don’t accidentally end up with “logical mush”.

4. Practice: an expense model and readable results

Converting an expense model to a data class

Now we’ll do what we came here for: apply data class in our mini expense-tracking app. Let’s say we have an expense with an amount in cents (so we don’t have to deal with Double), a category, and a note.

Suppose the model used to look like this (a regular class):

class Expense(
    val amountCents: Long,
    val category: String,
    val note: String
)

fun main() {
    val e = Expense(199_00, "food", "Pizza")
    println(e) // Expense@6d06d69c (roughly like this)
}

That’s fine as a start, but it’s inconvenient for diagnostics: println(e) says nothing about the expense.

Now we’ll replace it with a data class:

data class Expense(
    val amountCents: Long,
    val category: String,
    val note: String
)

fun main() {
    val e = Expense(199_00, "food", "Pizza")
    println(e) // Expense(amountCents=19900, category=food, note=Pizza)
}

To get a bit closer to our app, let’s add a list of expenses and print it:

data class Expense(val amountCents: Long, val category: String, val note: String)

fun main() {
    val expenses = listOf(
        Expense(199_00, "food", "Pizza"),
        Expense(350_00, "transport", "Taxi")
    )

    println(expenses)
    // [Expense(amountCents=19900, category=food, note=Pizza), Expense(amountCents=35000, category=transport, note=Taxi)]
}

Yes, the output is not super pretty for users, but for debugging it’s very convenient. Before, you would have had to either write manual formatting or print fields one by one.

Why data class is better than Pair/Triple

In previous topics, we used Pair/Triple as a quick way to return 2–3 values from a function. It’s a working tool, but it has a drawback: “first” and “second” don’t carry meaning by themselves. A week later, you look at pair.first and think: “is first the amount? or the category? or what is even going on?”

Compare the feel.

A Pair version:

fun parseAmountAndCategory(raw: String): Pair<Long, String> {
    return 199_00L to "food"
}

fun main() {
    val p = parseAmountAndCategory("19900 food")
    println(p.first)  // 19900
    println(p.second) // food
}

It works, but the meaning is hidden in the programmer’s head.

A data class version:

data class ParsedExpense(val amountCents: Long, val category: String)

fun parseAmountAndCategory(raw: String): ParsedExpense {
    return ParsedExpense(199_00L, "food")
}

fun main() {
    val p = parseAmountAndCategory("19900 food")
    println(p.amountCents) // 19900
    println(p.category)    // food
}

Same result, but readability is noticeably higher. And readability is not “beauty”; it’s “less chance to break the program at 2 a.m.”

“A little bit of behavior” inside a data class is fine

Sometimes beginners take data class too literally: “If it’s a ‘data class’, then you can’t do anything inside at all.” You can. A data class is still a class. You can add methods, computed properties, small checks, etc. The question isn’t “can/can’t”, but whether the model turns into a regular class that stores data, talks to the console, reads files, and accepts user commands.

For our Expense, for example, it makes sense to add a simple computed property to display the amount in dollars and cents (as a data format, without I/O):

data class Expense(val amountCents: Long, val category: String, val note: String) {
    val amountLabel: String
        get() = "${amountCents / 100}.${(amountCents % 100).toString().padStart(2, '0')}"
}

fun main() {
    val e = Expense(199_05, "food", "Pizza")
    println(e.amountLabel) // 199.05
}

Notice: this is still “data plus a small convenience”. We don’t print to the console from inside the model, we don’t read readln(), we don’t mutate global lists — we just make it easier to work with the data.

5. Common mistakes when working with data class

The first days with a data class usually look like this: you happily reduce code, then somewhat less happily get “weird equality”, and then fully happily understand the rules of the game. The mistakes below are not because you’re a “bad programmer”, but because your brain honestly tries to apply old habits from “regular classes”.

Mistake #1: expecting a data class to be just “less typing”, without understanding what exactly gets generated.
As a result, a person starts using copy() or destructuring “on autopilot”, and then is surprised by the behavior. The fix is simple: keep the list in mind — equals/hashCode/toString/copy/componentN. These are not “extra features”; they are the core of the data-class contract.

Mistake #2: moving an important field into the class body and expecting it to participate in equals() and toString().
This is one of the most common traps: “But I added var age… why doesn’t println(user) show it?” Because auto-generation considers only properties from the primary constructor. If a field affects the meaning of the object, keep it in the constructor; if it’s “temporary/auxiliary”, then it makes sense to keep it in the body.

Mistake #3: making a data class with var fields “just in case”.
Technically you can, but psychologically it often leads to chaos: the object changes somewhere along the way, and you don’t understand where. A beginner-friendly starting rule: if you’re not sure, start with val. It’s easier to add mutability than to hunt later for “who changed the field”.

Mistake #4: trying to make a data class a “universal container for everything” and stuffing half the app into it.
If your data model suddenly grows methods that read input, print menus, and mutate global collections — it’s no longer a data model; it’s a combine harvester. Yes, Kotlin will allow it, but maintaining it later is hard. A data class is strongest when it describes data and at most small pure computations next to it.

Mistake #5: arguing with data class requirements instead of accepting them as a design hint.
When the compiler forbids a parameterless data class or requires val/var in the constructor, it’s not being picky. It protects you from classes where it’s unclear what should be considered “data”. These limitations exist so the generated code is meaningful and internally consistent.

1
Task
Kotlin SELF,  33 level,  0 lecture
Locked
Product catalog
Product catalog
1
Task
Kotlin SELF,  33 level,  0 lecture
Locked
Two profiles
Two profiles
1
Task
Kotlin SELF,  33 level,  0 lecture
Locked
Ticket checking
Ticket checking
1
Task
Kotlin SELF,  33 level,  0 lecture
Locked
Session token
Session token
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet