JavaRush /Courses /Kotlin SELF /Getters and setters in Kotlin: private set, computed prop...

Getters and setters in Kotlin: private set, computed properties, and field

Kotlin SELF
Level 32, Lesson 1
Available

1. A property in Kotlin is a small API

If you’re just starting out, it’s very easy to think like this: “var x is just a variable that lives inside an object.” And in an everyday sense, it almost is... until you find out that reading and writing a property are operations that can be controlled. Kotlin is designed so you build an object not as a “bag of data”, but as a set of safe actions.

It’s important to catch the idea: when you write obj.name, you don’t “pull a field from memory” directly (at least, conceptually). You invoked a property read, i.e., its getter. When you write obj.name = "...", you invoked a property write, i.e., its setter. That’s why Kotlin lets you override read/write behavior: you write a small “contract” around the property.

In the Kotlin documentation, you can even see it “in the compiler’s footprints”: when a property has read/write logic, functions essentially appear: get() and set(value) (this is especially visible in codegen examples for delegated properties, where get()/set(value) are shown explicitly as the accessor form).

To get a feel for the mechanics — a mini diagram:

flowchart LR
    A["Outside code: obj.x"] --> B["get()"]
    C["Outside code: obj.x = v"] --> D["set(v)"]
    B --> E["return value"]
    D --> F["checks / normalization / write"]

2. Controlling access and the property’s representation

private set: readable by everyone, writable only inside the class

Sometimes you want a simple thing: let the property be readable from anywhere, but writable only inside the object. This is a super popular pattern because it supports encapsulation extremely well. For example, an account balance, number of attempts, internal ID counter — all of that typically shouldn’t be freely assignable from the outside.

private set is like a glass display case: everyone can look, but only staff can touch.

A minimal example: “balance can only be topped up”:

class Wallet {
    var balanceCents: Int = 0
        private set

    fun deposit(amountCents: Int) {
        require(amountCents > 0) { "amountCents must be > 0" }
        balanceCents += amountCents
    }
}

fun main() {
    val w = Wallet()
    w.deposit(250)
    println(w.balanceCents) // 250
}

Notice an important thing: we don’t forbid reading balanceCents, but we do forbid direct writes like w.balanceCents = 1_000_000. Now the balance changes only through a clear action deposit(...), which means you always have a control point (checks, logging, limits — anything).

How private set differs from private var:

private var means “from the outside, the property doesn’t exist at all.”
var ... private set means “from the outside, the property exists, but it’s read-only.”

When you design a class API, these feel very different to the user. Most of the time, “visible but not writable” is exactly what you want.

Computed properties: a display property over data

Sometimes you need to store data in one format (for example, in cents), but show it to the user in another (in dollars, with two digits). Or you want a nice “presentation” string, but you don’t want to store it separately so you don’t end up with a second source of truth (and don’t start manually syncing one with the other).

That’s where a computed property comes in: a property that doesn’t store a value, but computes it in get().

By the way, Kotlin even has a style guideline: prefer a property over a function if the computation is cheap, doesn’t throw exceptions, and returns the same result when the object’s state hasn’t changed. That directly reflects the computed property philosophy.

Example: the expense is stored in cents, but displayed as “dollars.cents”. Let’s imagine we’re building a console expense tracking app (we started it earlier with collections, and now moved items into classes). We’ll make an Expense class that stores the amount in cents and computes the output string.

class Expense(
    val id: Int,
    amountCents: Int,
    note: String
) {
    var amountCents: Int = amountCents
        private set

    var note: String = note
        private set

    val amountLabel: String
        get() = "${amountCents / 100}.${(amountCents % 100).toString().padStart(2, '0')}"
}

Here amountLabel is a computed property: each time it’s honestly built from amountCents. No “what if we forgot to update the string after changing the amount.”

Computed property vs “stored field” — a small table:

Approach What it is Pros Cons
Stored property (with a backing field)
var x = ...
fast to read risk of “two sources of truth” if you also store derived values
Computed property
val y get() = ...
always up to date, doesn’t store extra data computed on every read (so it should be reasonably cheap)

3. Backing field and field: where the value lives

When a property has the usual form var name: String = "...", Kotlin stores the value “inside” the object. In language terms, this is called a backing field (an internal storage field). When you write a custom get() or set(value), sometimes you need to refer to that storage specifically.

For that, Kotlin has a special word: field.

If you remember one rule from this part of the lecture, let it be this: inside a setter, you must not assign to the property using its own name — you’ll get recursion. You must assign to field.

An incorrect setter (recursion) — a classic beginner trap:

class User {
    var name: String = "Unknown"
        set(value) {
            name = value.trim() // ❌ recursion: the setter calls itself
        }
}

What will happen? The setter will call the setter, which will call the setter... and so on until the call stack runs out.

A correct setter via field:

class User {
    var name: String = "Unknown"
        set(value) {
            field = value.trim() // ✅ write into the internal storage
        }
}

fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name) // Alice
}

Here field is the “real property value”, while name is “accessing the property via its API”.

Where field exists and where it doesn’t:

field exists only if the property has a backing field, i.e., Kotlin actually stores a value. Computed properties typically don’t have storage: they’re computed “on the fly”, so field simply isn’t needed there.

4. Validation and normalization in set: maintaining invariants

When we say “invariant”, it sounds a bit academic, but the meaning is simple: an object must remain valid at all times, not only “if the class user was careful enough and didn’t touch anything extra.”

A setter is a great place to:

  1. normalize input (for example, trim() for strings),
  2. reject invalid values via require(...),
  3. enforce range constraints (for example, volume level 0..10).

A setter is like a “security guard at the entrance”: if someone tries to bring something weird into the object, the object says “no” immediately, instead of pretending everything is fine.

Normalization: the note doesn’t keep extra spaces.

In Expense, we want the note to be stored neatly. But we don’t want to write .trim() all over the app code. So normalization should be centralized.

class Expense(val id: Int, amountCents: Int, note: String) {
    var note: String = note
        set(value) {
            field = value.trim()
        }
}

Validation: you can’t set a negative amount.

Now let’s add a rule: an expense amount must be greater than zero (or at least non-negative — choose what’s product-correct). A setter is a good place for such a rule. But we also need to decide whether we want to allow changing the amount from the outside at all.

Let’s do this: changing from the outside is not allowed, but inside the class it is (for example, via a changeAmount(...) method).

class Expense(val id: Int, amountCents: Int) {
    var amountCents: Int = amountCents
        private set(value) {
            require(value >= 0) { "amountCents must be >= 0" }
            field = value
        }

    fun changeAmount(newAmountCents: Int) {
        amountCents = newAmountCents
    }
}

Notice how nicely the ideas combine here: private set closes direct writes from the outside, but the setter still exists and protects the invariant. So even inside the class you can’t accidentally assign garbage — the check lives in one place.

5. Example: expense tracking with safe properties

Now comes an important “how this is used in real code” moment, because otherwise getters/setters can feel like magic for the sake of magic. Let’s imagine our console app keeps a list of expenses in memory (we already know MutableList, input/validation, and commands).

We want:

  • an expense to have an id that cannot be changed;
  • the amount to never become negative;
  • the note to be normalized (trim());
  • a convenient computed property for displaying the amount.

Expense class (minimally practical):

class Expense(
    val id: Int,
    amountCents: Int,
    note: String
) {
    var amountCents: Int = amountCents
        private set(value) {
            require(value >= 0) { "amountCents must be >= 0" }
            field = value
        }

    var note: String = note
        set(value) {
            field = value.trim()
        }

    val amountLabel: String
        get() = "${amountCents / 100}.${(amountCents % 100).toString().padStart(2, '0')}"

    fun changeAmount(newAmountCents: Int) {
        amountCents = newAmountCents
    }
}

Everything is short here, but already “grown-up”: external code can’t break the object with an accidental assignment.

The ExpenseBook storage class: a computed property for total expenses.

Now let’s make an object that stores expenses. And add a computed property totalCents, so we don’t store a separate variable and don’t manually sync it after every add/remove.

class ExpenseBook {
    private val expenses: MutableList<Expense> = mutableListOf()

    val totalCents: Int
        get() = expenses.sumOf { it.amountCents }

    fun add(expense: Expense) {
        expenses.add(expense)
    }
}

totalCents here is a computed property. Yes, it’s recomputed on every read. But for a learning app that’s fine, and more importantly it’s logically reliable: the sum always matches the list.

Mini demo in main:

fun main() {
    val book = ExpenseBook()
    book.add(Expense(id = 1, amountCents = 199, note = " coffee "))
    book.add(Expense(id = 2, amountCents = 2500, note = "groceries"))

    println(book.totalCents) // 2699
}

6. Common mistakes when working with getters and setters

Mistake #1: recursion in a setter due to assigning “to itself”.
The most common story looks like this: you write a custom set(value), and out of habit you do prop = value. But prop = value is another setter call. You end up building a perpetual motion machine that runs not on caffeine but on StackOverflow. The fix is one word: inside a setter use field = ...

Mistake #2: turning a computed property into an “expensive function” that gets read a hundred times.
A computed property is good when the computation is cheap and logically looks like a “display property”. If inside get() you start doing heavy data processing, long loops, or complex parsing, reading the property becomes an unexpectedly expensive operation. Kotlin style hints at exactly this: a property is appropriate when the computation is cheap and predictable.

Mistake #3: scattering validation across the code instead of keeping it in one place.
A beginner often does this: checked in one place, forgot in another, and in a third “well it’s surely fine there”. As a result, the object is sometimes valid, sometimes not, and bugs live their own lives. It’s better to enforce invariants either in init, or in set, or in class methods, but in a way that every state change goes through the same control.

Mistake #4: making a public var and then trying to “negotiate” with the class user.
Sometimes it feels like: “I’ll just write a comment saying you must not assign a negative value.” That works about as well as “do not press the red button” signs in movies — your hand just reaches for it. If a value must not be changed from outside, close writes with private set and provide methods that make the change safely.

Mistake #5: confusing “hide the property” with “close writes”.
private var hides the property entirely — from outside it’s as if it doesn’t exist. var ... private set leaves public reads but forbids writes. Beginners sometimes choose private var, and then wonder why they have to write a separate getBalance() and manually maintain the API. Usually it’s simpler and cleaner to allow reads as a property and close writes.

1
Task
Kotlin SELF, level 32, lesson 1
Locked
Profile name
Profile name
1
Task
Kotlin SELF, level 32, lesson 1
Locked
Attempt Counter
Attempt Counter
1
Task
Kotlin SELF, level 32, lesson 1
Locked
Live Rectangle
Live Rectangle
1
Task
Kotlin SELF, level 32, lesson 1
Locked
Thermometer validation
Thermometer validation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION