JavaRush /Курсы /Kotlin SELF /init-блок: нормализ...

init-блок: нормализация и подготовка состояния

Kotlin SELF
31 уровень , 1 лекция
Открыта

1. Зачем нужен init и что он делает

Когда вы впервые пишете класс, часто кажется, что конструктор — это просто «параметры в скобках», а всё остальное происходит магически. На практике магия там есть, но управляемая: Kotlin даёт место, куда можно положить код, который выполнится в момент создания объекта. Это место и называется init‑блоком — небольшая «кухня» внутри класса, где вы приводите входные данные в порядок и готовите состояние.

init { ... } — это блок инициализации, который выполняется при создании экземпляра класса, то есть во время выполнения primary constructor. В Kotlin документации это прямо описано как initializer blocks, которые запускаются при выполнении primary constructor.

Самое полезное в init для новичка: он помогает сделать так, чтобы объект не хранил “грязный” ввод (пробелы, пустые строки, неправильный формат), а внутри всегда был аккуратный и предсказуемый.

Мини‑пример “на пальцах”:

class User(val name: String) {
    init {
        println("Создали пользователя: $name")
    }
}

fun main() {
    User("Ann") // Создали пользователя: Ann
}

Да, println в init — это не стиль для продакшена, но для обучения отлично показывает: блок реально выполняется при создании.

2. Нормализация: зачем “чинить” вход при создании объекта

Нормализация — это когда вы берёте входные данные и приводите их к “канонической” форме. В программировании это почти как мыть руки перед готовкой: можно, конечно, не мыть… но потом вы удивитесь, почему всё стало странно пахнуть.

Если вы делаете консольное приложение (а мы как раз делаем), то вход к вам часто приходит из readln(). А readln() читает мир таким, какой он есть: с пробелами, лишними переносами, неожиданными “ ” вместо имени и классическим “я точно ввёл число” (нет).

Продолжаем наше учебное приложение

Представим, что мы делаем маленький консольный трекер расходов (условный BudgetBuddy). У нас есть сущность Expense — расход: категория, сумма и заметка. Пока без data class (до неё ещё дойдём), обычный класс.

Сделаем так, чтобы Expense сам приводил данные в порядок:

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
    }
}

Здесь важный момент: параметры categoryInput, amountInput, noteInput — это просто вход. А свойства category, amount, note — это то, что реально будет храниться в объекте. И именно в init мы превращаем вход в состояние.

Проверим на маленьком main:

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

Теперь любая часть программы может рассчитывать, что category уже без пробелов и в одном регистре. Это снижает количество проверок “на всякий случай” в остальных местах.

3. Подготовка состояния: не только “почистить”, но и “дособрать”

init полезен не только для trim(). Часто объекту нужно подготовить ещё какие-то “внутренние” значения. Например, строку для печати, короткий идентификатор, нормализованную версию для поиска, или аккуратно собранный заголовок.

Важно не превращать init в “сериал на 12 сезонов”: он должен отвечать за создание корректного состояния, а не за бизнес‑сценарий “сходить в интернет, спросить курс доллара и принять судьбоносное решение”.

Добавим в Expense подготовленный текст для отображения (условно summary). Он вычисляется один раз при создании и хранится как 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)
}

Обратите внимание: мы не делаем тут ничего сверхсложного, но выигрываем читабельность в остальном коде. Позже, когда мы будем выводить список расходов, мы сможем печатать summary, не собирая строку заново в десяти местах.

4. Две стратегии нормализации: “исправить” или “запретить”

В реальных проектах вы почти всегда выбираете одну из двух философий.

Первая философия — “попробуем аккуратно исправить вход”. Например, trim() для строки — это мягкое исправление: пользователь ввёл “ Food ”, а вы превратили это в “food”, и всем хорошо.

Вторая философия — “если вход плохой, объект не создаём”. Это подход fail-fast: “лучше упасть сразу, чем жить с мусором внутри”.

Kotlin прямо рекомендует init как место для валидации и показывает пример с require(...).

Сделаем мягкий вариант (чинить пробелы) и строгий вариант (запрещать пустую категорию).

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
}

Функция require() — это “предусловие”: если условие ложно, она выбрасывает IllegalArgumentException. Мы подробно разберём require/check в отдельной лекции этого дня, но на уровне init важно запомнить идею: валидация входа логично живёт там же, где объект создаётся, иначе вы вечно будете искать, кто именно забыл проверить аргументы.

Небольшая табличка для ощущения границ:

Ситуация Обычно делаем в init Почему это удобно
Пробелы вокруг строки
trim()
Убираем “шум” один раз, а не в каждом месте использования
Разный регистр
lowercase()
/
uppercase()
Поиск/сравнение становятся стабильнее
Пустая строка там, где нельзя
require(...)
Лучше не создавать объект, чем хранить поломанное состояние
Пустая заметка, которая не обязательна превращаем в null “Нет заметки” и “заметка = пробелы” перестают быть разными мирами

5. Несколько init‑блоков и порядок выполнения

Иногда хочется разделить логику инициализации на части: отдельно нормализация, отдельно вычисление derived‑полей, отдельно проверки. Kotlin это разрешает: init‑блоков может быть несколько, и они выполняются в порядке объявления (сверху вниз).

Это похоже на то, как вы готовите ужин: сначала вы достаёте продукты, потом моете, потом режете, потом готовите. Можно всё сделать в одном гигантском “мега‑шаге”, но мозгу проще, когда шаги отделены.

Покажем порядок на примере:

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
}

Идея, которую важно унести: init — не “одна волшебная штука”, а последовательность блоков. Это помогает, когда вы читаете чужой код (или свой код через три недели, что почти то же самое).

Для визуализации процесса создания объекта удобно держать в голове такую схему:

flowchart TD
    A["Вызвали конструктор
Demo(' Food ')"] --> B["Инициализаторы свойств
val category = ..."] B --> C["init #1"] C --> D["init #2"] D --> E["Объект готов к использованию"]

Про более тонкие нюансы порядка инициализации мы поговорим в отдельной лекции этого дня. Сейчас нам достаточно уверенно понимать, что init идёт “при создании” и по порядку.

6. Дизайн: нормализация внутри класса и инварианты

Нормализация должна жить внутри класса

Очень частая “боль новичка” выглядит так: вы где-то прочитали строку, где-то сделали trim(), где-то забыли, а потом ловите баг “почему категория food не равна категории food ”.

Если нормализация лежит снаружи, вы обязаны помнить о ней в каждом месте, где создаёте объект. А мест со временем станет много: команда add, импорт из файла, тестовый код, генератор демо‑данных. И каждый раз кто‑то забудет.

Поэтому хороший стиль звучит так: класс сам отвечает за то, чтобы внутри него было чисто.

Плохой вариант (нормализация снаружи):

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

Проблема здесь не в том, что это “не работает”. Проблема в том, что через неделю кто-то создаст Expense(rawCategory, ...) без trim() и будет искренне удивлён.

Хороший вариант (нормализация внутри):

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

Теперь объект гарантирует, что category в норме. Внешний код может быть простым, а простота — это недооценённая суперсила.

init как “место сборки инвариантов”

Когда вы проектируете класс, полезно договориться с самим собой о небольших правилах, которые всегда должны быть верны для объекта. Например: “категория не пустая”, “сумма не отрицательная”, “заметка либо null, либо непустая строка без пробелов по краям”.

Такие правила часто называют инвариантами состояния. Не обязательно запоминать термин, достаточно запомнить смысл: объект не должен существовать в нелепом виде.

init — отличное место, чтобы эти правила “закрепить”. Kotlin даже подсказывает это как типичный сценарий: init + require(...) для проверки корректности данных.

В нашем Expense зафиксируем две простые идеи: категория не пустая, сумма не отрицательная (пока просто запретим отрицательную).

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
    }
}

Если кто-то попытается создать Expense(" ", -10), объект просто не появится. Это намного лучше, чем создать “сломанный” объект и потом пытаться вычислять по нему отчёты.

7. Типичные ошибки при работе с init

Ошибка №1: нормализация живёт снаружи класса, и в итоге дублируется.
Обычно это начинается безобидно: вы сделали trim() в одном месте и всё работает. Потом появляется второе место создания объекта, там trim() забыли, и поиск/сравнение начинают вести себя “как будто Kotlin шутит”. Лучше один раз нормализовать в init, чем десять раз надеяться на дисциплину человека.

Ошибка №2: init превращают в “главный сценарий приложения”.
Иногда в init хочется и печатать много, и читать readln(), и считать что-то большое, и делать “половину программы”. Это плохой стиль: конструирование должно быть быстрым и предсказуемым. init — это про корректность состояния, а не про то, чтобы “сделать всё сразу”.

Ошибка №3: смешивают стратегию “чинить вход” и “запрещать вход” без явного решения.
Если часть полей вы тихо исправляете (например, trim()), а часть внезапно валится с require, пользователь класса получает странный опыт. На уровне дизайна важно хотя бы для себя выбрать: “мы мягко нормализуем” или “мы строго запрещаем”, и быть последовательным.

Ошибка №4: делают свойства var только ради того, чтобы поменять их в init.
Технически это работает, но часто ведёт к лишней мутабельности: раз свойство var, значит его можно менять и после создания — даже если это не часть смысла. Нередко лучше хранить результат нормализации в отдельных val и не давать объекту “дрейфовать”.

Ошибка №5: пишут проверки, но с неинформативными сообщениями.
require(x > 0) { "bad" } — это почти как записка “сломалось” на сломанном мосту. Хорошее сообщение должно объяснять ожидание и фактическое значение. Kotlin и документация по исключениям прямо подчеркивают, что require бросает IllegalArgumentException, и от качества сообщения зависит, насколько быстро вы поймёте проблему.

1
Задача
Kotlin SELF, 31 уровень, 1 лекция
Недоступна
Регистрация пользователя
Регистрация пользователя
1
Задача
Kotlin SELF, 31 уровень, 1 лекция
Недоступна
Ярлык раздела
Ярлык раздела
1
Задача
Kotlin SELF, 31 уровень, 1 лекция
Недоступна
Умный комментарий
Умный комментарий
1
Задача
Kotlin SELF, 31 уровень, 1 лекция
Недоступна
Два шага init
Два шага init
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ