JavaRush/Courses/Kotlin SELF/Why classes matter and how to think in objects: your own ...

Why classes matter and how to think in objects: your own type and references

Open

1. Introduction

When we are just starting to program, it is natural to store everything in separate variables: it is simple and looks like math. But as soon as variables become “parts of one meaning,” everyday chaos begins: values are easy to mix up, easy to forget to update, easy to pass in the wrong order. The language does not know these pieces are supposed to live together, so it cannot help you.

Imagine we are writing a small expense tracker (we will come back to it today and later). We have an “expense name” and an “amount.” The naive version looks like this:

fun main() {
    val title = "Coffee"
    val amount = 300

    println("$title: $amount") // Coffee: 300
}

While it is just one expense, it seems fine. But now imagine there are many expenses. The approach “I will make title1, amount1, title2, amount2” quickly turns into a museum of pain, where the exhibits are labeled with a marker as “approximately this.”

A slightly more “advanced” beginner approach is two parallel lists:

fun main() {
    val titles = mutableListOf("Coffee", "Taxi")
    val amounts = mutableListOf(300, 1200)

    println("${titles[0]}: ${amounts[0]}") // Coffee: 300
}

And this is where the magic begins: if you remove an element from one list and forget to remove it from the other, the data becomes “out of sync.” The program will keep running — it will just print coffee for 1200 and taxi for 300. This is not a compiler error; it is a reality error.

At this point you want to say: “But I know for sure that title and amount are one whole. Why doesn’t Kotlin know?” The answer: because you have not told Kotlin explicitly yet. Classes are exactly the way to say: “This is a single entity.”

Pair as a temporary solution

When it becomes clear that “two variables are one whole,” many people make a logical move: they glue the values into a Pair. This already resembles a normal data structure: one value that contains two fields inside.

An expense scenario using Pair:

fun main() {
    val expense: Pair<String, Int> = "Coffee" to 300

    println(expense.first)  // Coffee
    println(expense.second) // 300
}

Pair is genuinely useful: it lets you quickly combine two values and pass them around as one. It is convenient when you are making a small one-off bundle, for example “name and counter” or “result and message.”

But Pair has a fundamental problem: the names first and second carry no domain meaning. A week later you will see pair.second and try to remember: “Is the second one the amount? age? rating? number of kittens?” Yes, you can agree that “second is always amount,” but agreements like that usually last exactly until the first tired evening.

To see the difference, it helps to compare the approaches in a small table:

Approach What it looks like What is good What starts to hurt
Separate variables
title, amount
simple no “single whole,” easy to mix up
Pair
"Coffee" to 300
one value
first/second
without meaning
Class
Expense(title, amount)
clear field names you have to declare the type once

A class is the next step: you create your own data type with normal names, and the code starts reading like text.

Class and object

When you see Int, you perceive it as a ready-made type: a number you can do things with. A class is a way to create your own type in roughly the same style: “these are not two values; this is one object — an expense.”

Technically, a class is declared with the keyword class. For example, an “empty” class might look like this:

class Expense

But an “empty” class is useless: it is like a box with nothing inside. So we will add fields (properties). Right now we will do it in the most straightforward way — via properties in the class body — so we do not jump ahead into constructor syntax (that will be a separate topic later).

class Expense {
    var title: String = ""
    var amount: Int = 0
}

Now you can create an object of this class. In Kotlin, unlike some other languages, you do not write new when creating an object — you just call the class name “like a function.”

fun main() {
    val e = Expense()
    e.title = "Coffee"
    e.amount = 300

    println("${e.title}: ${e.amount}") // Coffee: 300
}

What matters here at the “understand it and stop being afraid” level:

  • Expense is a type.
  • e is a variable that holds a reference to an object of type Expense.
  • Expense() is the moment a new object (an instance of the class) is created.
  • e.title and e.amount are access to the object’s data via dot notation.

And here we have made a key step: instead of “two independent variables you have to keep in your head,” we now have “one value that has meaningful parts.”

Connecting the idea to an expense app

Now we will carefully start translating the idea of classes into our learning context: an app that has expenses. We are not building an architecture or inventing the “right” model for the ages — we need to feel how a class turns a set of values into a meaningful entity.

Let’s make a simple scaffold: the Expense class, creating an object, and a print function. Note: the function takes one parameter (an expense), not two (name and amount). That is already a readability win.

class Expense {
    var title: String = ""
    var amount: Int = 0
}

fun printExpense(e: Expense) {
    println("${e.title}: ${e.amount}")
}

fun main() {
    val e = Expense()
    e.title = "Coffee"
    e.amount = 300

    printExpense(e) // Coffee: 300
}

Now compare this with the Pair version. There, printExpense would look like: “pass me a pair, and I will remember that first is title.” In other words, part of the meaning lives only in your head. With a class, the meaning lives in the code.

And one more nice thing: an object can be “passed along” as a single value. For example, you can make a “create expense” function and return an object:

class Expense {
    var title: String = ""
    var amount: Int = 0
}

fun createExpense(title: String, amount: Int): Expense {
    val e = Expense()
    e.title = title
    e.amount = amount
    return e
}

fun main() {
    val lunch = createExpense("Lunch", 900)
    println("${lunch.title}: ${lunch.amount}") // Lunch: 900
}

So far this looks a bit verbose, and that is normal: we are intentionally using the most explicit style so it is clear what exactly is happening. Kotlin lets you write it shorter, but “shorter” makes sense only after it is “clear.”

2. Object references and variable behavior

A variable stores a reference to an object

The word “reference” often sounds like the start of a lecture after which you want to jump out a window (or at least into the hallway). But we do not need theory about memory, addresses, and bytes here. We need a practical behavior model so you do not catch “weird bugs” and think Kotlin is messing with you.

Let’s simplify to an everyday analogy. An object is like a folder on your desk. A variable is like a label with a name that you stick onto the folder. If you stick a second label onto the same folder, you do not get two folders — the folder stays one, it just has two names now.

Let’s look at the code:

class Box {
    var value: Int = 0
}

fun main() {
    val a = Box()
    a.value = 10

    val b = a
    b.value = 99

    println(a.value) // 99
}

The key line here is val b = a. It does not create a new object. It makes b point to the same object as a. Therefore changing b.value changes the state of the one and only object, and when reading a.value you will see 99.

It is useful to lock this in with a diagram. This is not “real memory,” just a picture so your brain does not resist:

flowchart LR
    a[a] --> obj["Box(value=10)"]
    b[b] --> obj

After b.value = 99 the object becomes Box(value=99), and both variables see the same thing.

This behavior is not a “Kotlin quirk,” but a basic idea of object types: a variable stores a reference, not “the whole object itself.” This will be the most important foundation for all the next steps.

val and var: you cannot reassign, but you can mutate

A very common confusion among beginners: “if I have val, then the value is immutable.” That is true for numbers and strings in the everyday sense, but for objects you need a clarification: val forbids reassigning the variable, but it does not necessarily forbid changing the object it points to.

Let’s make an example that shows the difference between “reassign the reference” and “mutate the object”:

class Counter {
    var value: Int = 0
}

fun main() {
    val c = Counter()
    c.value = 5
    c.value += 1

    println(c.value) // 6
}

Here c is a val, but c.value is a var. That means: the variable c will always point to the same object, but inside the object the field value can be changed.

But this is not allowed:

class Counter {
    var value: Int = 0
}

fun main() {
    val c = Counter()
    // c = Counter() // not allowed: val cannot be reassigned
}

In human terms: val says “you cannot move the label onto a different folder,” but it does not say “you cannot change the documents inside the folder.”

Why is this important today? Because as soon as you start storing objects in collections, or passing them into functions, or assigning one variable to another, the effect “I changed it here, but it changed over there” will suddenly show up. And that is almost always about references, not mysticism.

“Same data” vs “same object”

Another point where beginners usually look honestly confused: “But I created two identical objects — why are they not the same thing?” Because “same fields” and “same object” are different concepts.

Look:

class Box {
    var value: Int = 0
}

fun main() {
    val a = Box()
    a.value = 10

    val b = Box()
    b.value = 10

    println(a === b) // false
}

The operator === checks whether two variables point to the very same object. Here they do not, because we wrote Box() twice, i.e., we created two different objects.

Now compare that to “two names for one object”:

class Box {
    var value: Int = 0
}

fun main() {
    val a = Box()
    a.value = 10

    val b = a
    println(a === b) // true
}

This topic is closely related to == (“value equality”) and === (“reference equality”). You have seen this idea before, and today it is important to keep only the practical part: === helps you check whether it is “one folder with two labels” or “two separate folders.”

We are not drawing far-reaching conclusions about “data equality” yet: for a regular class without additional logic, == is not required to compare fields the way you might want. So today we keep the focus on references and on how they affect state changes.

3. Typical mistakes when working with objects and references

Mistake #1: thinking that b = a creates a copy of the object.
This expectation comes from the habit of working with numbers and strings: “I assigned it — so I got my own independent value.” But with objects, assignment copies the reference, not a new object. If you need an independent entity, you must create a new object explicitly (for example, call Expense() again or another constructor).

Mistake #2: believing that val makes an object “immutable.”
val protects the variable from reassignment, but it does not forbid changing the object’s var properties. This is useful, but sometimes surprising: “why is it changing if it is val?” Because it is not the reference that changes, but the object’s state. If you want fewer surprises, a good habit is to start with val references and add mutability inside the object very deliberately.

Mistake #3: overusing Pair where there is already domain meaning.
Pair is great as a temporary glue, but as soon as you catch yourself thinking “first is the expense name,” that is a signal: it is time to name the entity (Expense) and give it proper properties. Otherwise the code eventually turns into a set of riddles: “and what is second here?”

Mistake #4: trying to “stuff everything into a class” immediately without understanding boundaries.
On day one of OOP, it is common to want a class that stores data, prints itself, reads input, sorts a list, and also makes coffee. At the start, keep the class as a data model (fields) — that is already enough to win on readability and reduce the number of mistakes.

Mistake #5: ignoring the “one object or two” check during debugging.
When “it suddenly changes somewhere else,” the best quick test is to check a === b. If it is true, then you really are working with one object, just through two names. This does not fix the bug by itself, but it instantly explains why it is possible.

1
Task
Kotlin SELF,  30 level,  0 lecture
Locked
Storefront price tag
Storefront price tag
1
Task
Kotlin SELF,  30 level,  0 lecture
Locked
Double label
Double label
1
Task
Kotlin SELF,  30 level,  0 lecture
Locked
Tweaking the score counter
Tweaking the score counter
1
Task
Kotlin SELF,  30 level,  0 lecture
Locked
Order line item
Order line item
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet