JavaRush/Courses/Kotlin SELF/init-block: normali...

init-block: normalizing and preparing state

Open

1. Why you need init and what it does

When you write a class for the first time, it often feels like a constructor is just “parameters in parentheses”, and everything else happens magically. In practice there is some magic there, but it is controllable: Kotlin provides a place where you can put code that will run at the moment an object is created. That place is called an init-block — a small “kitchen” inside the class where you tidy up input data and prepare state.

init { ... } is an initialization block that runs when a class instance is created, i.e., during execution of the primary constructor. In Kotlin documentation, this is explicitly described as initializer blocks that are executed as part of the primary constructor.

The most useful thing about init for a beginner: it helps ensure the object does not store “dirty” input (spaces, empty strings, wrong format), and that the inside is always neat and predictable.

A tiny “hands-on” example:

class User(val name: String) {
    init {
        println("Created user: $name")
    }
}

fun main() {
    User("Ann") // Created user: Ann
}

Yes, println inside init is not production style, but for learning it clearly shows: the block really does execute on creation.

2. Normalization: why “fix” input when creating an object

Normalization is when you take input data and convert it to a “canonical” form. In programming it is a bit like washing your hands before cooking: sure, you can skip it... but then you will be surprised why everything starts to smell weird.

If you are building a console application (and we are), input often comes from readln(). And readln() reads the world as it is: with spaces, extra line breaks, unexpected “ ” instead of a name, and the classic “I definitely typed a number” (nope).

Continuing our learning app

Imagine we are building a small console expense tracker (a conditional BudgetBuddy). We have an entity Expense — an expense: category, amount, and note. For now, without a data class (we will get to it later), just a regular class.

Let’s make Expense itself clean up the data:

class Expense(
    categoryInput: String,
    amountInput: Int,
    noteInput: String?
) {
    val category: String
    val amount: Int
    val note: String?

    init {
        category = categoryInput.trim().lowercase()
        amount = amountInput
        val n = noteInput?.trim()
        note = if (n == null || n.isBlank()) null else n
    }
}

Here is an important point: the parameters categoryInput, amountInput, noteInput are just input. But the properties category, amount, note are what will actually be stored in the object. And it is exactly in init that we turn input into state.

Let’s test it with a small main:

fun main() {
    val e = Expense(categoryInput = "  Food  ", amountInput = 250, noteInput = "  coffee ")
    println(e.category) // food
    println(e.note)     // coffee
}

Now any part of the program can rely on category already being trimmed and in a consistent case. That reduces the number of “just in case” checks elsewhere.

3. Preparing state: not only “clean up”, but also “assemble”

init is useful not only for trim(). Often an object needs to prepare some additional “internal” values: for example, a string for display, a short identifier, a normalized version for search, or a neatly assembled title.

It is important not to turn init into a “12-season TV show”: it should be responsible for creating correct state, not for a business scenario like “go to the internet, fetch the USD exchange rate, and make a life-changing decision”.

Let’s add to Expense a prepared display text (call it summary). It is computed once at creation time and stored as a val:

class Expense(
    categoryInput: String,
    amountInput: Int,
    noteInput: String?
) {
    val category: String
    val amount: Int
    val note: String?
    val summary: String

    init {
        category = categoryInput.trim().lowercase()
        amount = amountInput
        val n = noteInput?.trim()
        note = if (n == null || n.isBlank()) null else n

        summary = if (note == null) {
            "$category: $amount"
        } else {
            "$category: $amount ($note)"
        }
    }
}

fun main() {
    val e = Expense("  Transport ", 120, " metro ")
    println(e.summary) // transport: 120 (metro)
}

Notice: we are not doing anything overly complex here, but we gain readability in the rest of the code. Later, when we print a list of expenses, we will be able to print summary without rebuilding the string in ten different places.

4. Two normalization strategies: “fix” vs “reject”

In real projects, you almost always choose one of two philosophies.

The first philosophy is “let’s try to gently fix the input”. For example, trim() for a string is a soft fix: the user typed “ Food ”, you turned it into “food”, and everyone is happy.

The second philosophy is “if the input is bad, do not create the object”. This is a fail-fast approach: “better to crash immediately than to live with garbage inside”.

Kotlin directly recommends using init as the place for validation and shows an example with require(...).

Let’s do a soft variant (fix spaces) and a strict variant (disallow an empty category).

class Expense(categoryInput: String, val amount: Int) {
    val category: String

    init {
        val fixed = categoryInput.trim().lowercase()
        require(fixed.isNotBlank()) { "Category must not be blank" }
        category = fixed
    }
}

fun main() {
    val e = Expense("  Food  ", 100)
    println(e.category) // food
}

The function require() is a “precondition”: if the condition is false, it throws IllegalArgumentException. We will cover require/check in detail in a separate lecture that day, but at the init level the key idea is: input validation logically belongs where the object is created, otherwise you will constantly be searching for who exactly forgot to validate the arguments.

A small table to get a feel for the boundaries:

Situation Usually done in init Why it is convenient
Spaces around a string
trim()
Remove “noise” once instead of at every usage site
Inconsistent letter case
lowercase()
/
uppercase()
Search/comparison becomes more stable
Empty string where it is not allowed
require(...)
Better not to create an object than to store broken state
An empty note that is optional convert to null “No note” and “note = spaces” stop being different worlds

5. Multiple init-blocks and execution order

Sometimes you want to split initialization logic into parts: normalization separately, derived-field computation separately, checks separately. Kotlin allows this: you can have multiple init-blocks, and they execute in declaration order (top to bottom).

This is like cooking dinner: first you take out ingredients, then wash them, then cut them, then cook. You can do everything in one giant “mega-step”, but the brain finds it easier when the steps are separated.

Let’s show the order with an example:

class Demo(categoryInput: String) {
    val category: String = categoryInput.trim()

    init {
        println("init #1: category='$category'")
    }

    init {
        println("init #2: length=${category.length}")
    }
}

fun main() {
    Demo("  Food  ")
    // init #1: category='Food'
    // init #2: length=4
}

The key idea to take away: init is not “one magical thing”, but a sequence of blocks. This helps when you read someone else’s code (or your own code three weeks later, which is almost the same thing).

For visualizing the object creation process, it is useful to keep this scheme in mind:

flowchart TD
    A["Called the constructor
Demo(' Food ')"] --> B["Property initializers
val category = ..."] B --> C["init #1"] C --> D["init #2"] D --> E["Object is ready to use"]

We will discuss more subtle nuances of initialization order in a separate lecture that day. For now, it is enough to confidently understand that init runs “during creation” and in order.

6. Design: normalization inside the class and invariants

Normalization should live inside the class

A very common “beginner pain” looks like this: you read a string somewhere, you do trim() somewhere, you forget somewhere else, and then you catch a bug: “why is category food not equal to category food ”.

If normalization lives outside, you must remember it at every place where you create an object. And over time there will be many places: the add command, file import, test code, demo-data generator. And every time someone will forget.

That is why good style sounds like this: the class itself is responsible for keeping its inside clean.

Bad variant (normalization outside):

fun main() {
    val rawCategory = "  Food  "
    val e = Expense(categoryInput = rawCategory.trim(), amountInput = 100, noteInput = null)
    println(e.category)
}

The problem here is not that it “does not work”. The problem is that in a week someone will create Expense(rawCategory, ...) without trim() and will be genuinely surprised.

Good variant (normalization inside):

fun main() {
    val rawCategory = "  Food  "
    val e = Expense(categoryInput = rawCategory, amountInput = 100, noteInput = null)
    println(e.category) // food
}

Now the object guarantees that category is fine. External code can be simple, and simplicity is an underrated superpower.

init as an “invariant assembly point”

When you design a class, it is useful to agree with yourself on a few rules that must always be true for the object. For example: “category is not empty”, “amount is not negative”, “note is either null or a non-empty string without leading/trailing spaces”.

Such rules are often called state invariants. You do not have to memorize the term; it is enough to remember the meaning: the object should not exist in a ridiculous form.

init is a great place to “lock in” these rules. Kotlin even hints at this as a typical scenario: init + require(...) to check that data is valid.

In our Expense, let’s enforce two simple ideas: category is not empty, amount is not negative (for now, we will just forbid negative values).

class Expense(categoryInput: String, amountInput: Int) {
    val category: String
    val amount: Int

    init {
        val fixedCategory = categoryInput.trim().lowercase()
        require(fixedCategory.isNotBlank()) { "Category must not be blank" }
        require(amountInput >= 0) { "Amount must be >= 0, got $amountInput" }

        category = fixedCategory
        amount = amountInput
    }
}

If someone tries to create Expense(" ", -10), the object simply will not be created. That is much better than creating a “broken” object and then trying to compute reports from it.

7. Typical mistakes when working with init

Mistake #1: normalization lives outside the class and ends up duplicated.
Usually it starts innocently: you did trim() in one place and everything works. Then a second place that creates the object appears, trim() is forgotten there, and search/comparison starts behaving “as if Kotlin is joking”. Better to normalize once in init than to hope for human discipline ten times.

Mistake #2: turning init into the “main application scenario”.
Sometimes you want to print a lot in init, read readln(), compute something big, and do “half the program”. This is bad style: construction should be fast and predictable. init is about correct state, not about “doing everything at once”.

Mistake #3: mixing “fix input” and “reject input” strategies without an explicit decision.
If you silently fix some fields (for example, trim()), but other fields suddenly crash with require, the class user gets a strange experience. At the design level, it is important to choose at least for yourself: “we normalize gently” or “we reject strictly”, and be consistent.

Mistake #4: making properties var only to change them in init.
Technically it works, but it often leads to unnecessary mutability: if a property is a var, it can be changed after creation — even if that is not part of the intended meaning. Often it is better to store normalization results in separate val and not allow the object to “drift”.

Mistake #5: writing checks, but with uninformative messages.
require(x > 0) { "bad" } is almost like a note saying “broken” on a broken bridge. A good message should explain the expectation and the actual value. Kotlin and exception documentation explicitly emphasize that require throws IllegalArgumentException, and the message quality affects how quickly you understand the problem.

1
Task
Kotlin SELF,  31 level,  1 lecture
Locked
User registration
User registration
1
Task
Kotlin SELF,  31 level,  1 lecture
Locked
Section label
Section label
1
Task
Kotlin SELF,  31 level,  1 lecture
Locked
Smart comment
Smart comment
1
Task
Kotlin SELF,  31 level,  1 lecture
Locked
Two init steps
Two init steps
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet