--- title: Инициализация description: Порядок выполнения init и инициализаторов, инициализация при наследовании и ловушка open-метода tags: - kotlin - oop --- Инициализаторы свойств и блоки `init` выполняются **в порядке их объявления** в теле класса - как единая последовательность. ```kotlin class Order4 { val a = println("1: свойство a").let { "a" } init { println("2: первый init") } val b = println("3: свойство b").let { "b" } init { println("4: второй init") } constructor() { println("5: тело конструктора") } } fun main() { Order4() } ``` Вывод: `1`, `2`, `3`, `4`, `5`. Тело вторичного конструктора идёт **последним**, после всех инициализаторов. Блоков `init` может быть несколько; их используют, когда инициализация не сводится к одному выражению. ## Параметры конструктора доступны сразу ```kotlin class User3(name: String, age: Int) { val displayName = name.uppercase() val category: String init { category = when { age < 18 -> "несовершеннолетний" age < 65 -> "взрослый" else -> "пенсионер" } } } fun main() { val u = User3("иван", 20) println("${u.displayName}: ${u.category}") // ИВАН: взрослый } ``` ## Порядок при наследовании Базовый класс инициализируется **полностью** до начала инициализации подкласса: ```kotlin open class Base4 { val baseField = println("1: свойство Base").let { "base" } init { println("2: init Base") } } class Derived4 : Base4() { val derivedField = println("3: свойство Derived").let { "derived" } init { println("4: init Derived") } } fun main() { Derived4() } ``` Вывод: `1`, `2`, `3`, `4`. Внутри каждого класса - строго порядок объявления; базовый класс целиком раньше подкласса. Переставьте `init` выше свойства - и номера пойдут как `2`, `1`, `4`, `3`. Аргументы для конструктора базового класса вычисляются **до** всего остального: ```kotlin open class Base5(val value: String) { init { println("2: init Base, value=$value") } } class Derived5(input: String) : Base5(input.uppercase().also { println("1: аргумент=$it") }) { init { println("3: init Derived") } } fun main() { Derived5("иван") } ``` ## Ловушка: open-метод в конструкторе Это главная ошибка инициализации. Вызов `open`-метода из конструктора базового класса попадёт в переопределённую версию - **до** того, как подкласс инициализирован. ```kotlin open class Base6 { init { show() } // опасно open fun show() = println("Base") } class Derived6 : Base6() { val name: String = "Иван" override fun show() = println("Derived, name='$name'") } fun main() { Derived6() } ``` Вывод: `Derived, name='null'`. Свойство `name` объявлено non-null, но в этот момент ещё не присвоено - и мы наблюдаем `null` в типе `String`. Если свойство подкласса имеет примитивный тип, вместо `null` будет `0` - и это ещё труднее заметить, потому что исключения не возникает: ```kotlin open class Base7 { init { println("размер при инициализации: ${size()}") } open fun size() = 0 } class Derived7 : Base7() { private val count: Int = 3 override fun size() = count } fun main() { val d = Derived7() println("размер после: ${d.size()}") } ``` Вывод: `0`, затем `3`. Ошибки нет, просто неверное значение. А вот если переопределённый метод обращается к объекту, результат - `NullPointerException` в коде, где ни одного nullable-типа не видно: ```kotlin open class Base7b { init { println("размер: ${size()}") } open fun size() = 0 } class Derived7b : Base7b() { private val items = listOf(1, 2, 3) override fun size() = items.size // items ещё null } fun main() { try { Derived7b() } catch (e: NullPointerException) { println("NPE: ${e.message}") // NPE: Cannot invoke "java.util.List.size()" because "this.items" is null } } ``` Та же проблема существует в Java; разница в том, что Kotlin позволяет её избежать, закрывая классы по умолчанию. Как не попадать: - не вызывать `open`-члены из конструкторов и `init` - делать такие методы `private` или `final` - если поведение подкласса нужно при создании - передавать его как параметр конструктора ```kotlin open class Base8(private val label: String) { init { println("метка: $label") } // параметр уже готов } class Derived8 : Base8("Derived") { val name = "Иван" } fun main() { Derived8() } // метка: Derived ``` ## Порядок объявления имеет значение Свойство нельзя использовать до его объявления: ```kotlin class Bad3 { // val b = a + 1 // ошибка: variable 'a' must be initialized val a = 1 val b = a + 1 } fun main() { println(Bad3().b) } // 2 ``` Компилятор ловит это для прямых обращений. Но через метод проверка обходится: ```kotlin class Sneaky { val b = compute() // вызывается до инициализации a val a = 10 private fun compute() = a * 2 override fun toString() = "a=$a b=$b" } fun main() { println(Sneaky()) } // a=10 b=0 ``` `b` равно `0`, потому что `a` на момент вызова ещё не присвоено. Ошибки компиляции нет - компилятор не отслеживает порядок через вызовы методов. ## Ленивая инициализация как решение Если порядок становится проблемой, `by lazy` снимает её: значение вычисляется при первом обращении, когда объект уже собран. ```kotlin class Fixed { val b: Int by lazy { a * 2 } // вычислится после создания val a = 10 override fun toString() = "a=$a b=$b" } fun main() { println(Fixed()) } // a=10 b=20 ``` Подробнее: [Свойства](Свойства.md) ## Исключение в инициализации ```kotlin class Validated(val age: Int) { init { require(age >= 0) { "возраст отрицательный: $age" } println("объект создан") } } fun main() { try { Validated(-1) } catch (e: IllegalArgumentException) { println("не создан: ${e.message}") // не создан: возраст отрицательный: -1 } println(Validated(20).age) // объект создан, 20 } ``` Ссылка на объект не вернётся, но побочные эффекты, выполненные до исключения, останутся. Поэтому в `init` держат только проверки и присваивания - без регистрации в реестрах, открытия файлов и запуска потоков. ## Инициализация object и companion object ```kotlin object Registry2 { init { println("инициализация Registry") } val items = mutableListOf() } class Service9 { companion object { init { println("инициализация companion") } val created = 0 } } fun main() { println("старт") println(Registry2.items.size) // инициализация Registry, затем 0 println(Service9.created) // инициализация companion, затем 0 } ``` Оба инициализируются лениво - при первом обращении, потокобезопасно средствами JVM. `companion object` инициализируется при первом обращении к нему или при первом создании экземпляра класса. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | блок `{ }` в теле класса | `init { }` | > | `static { }` | `init` в `companion object` | > | присваивание в конструкторе | инициализатор свойства | > | `super(args)` первой строкой | `: Base(args)` в заголовке | > | нет проверки порядка | ошибка при прямом обращении вперёд | > > **Порядок инициализации одинаков**: базовый класс, потом подкласс; внутри класса - по порядку объявления. Проблема вызова переопределяемого метода из конструктора тоже одинакова. > > **Kotlin частично защищает от неинициализированных свойств.** Прямое обращение к свойству до его объявления - ошибка компиляции, тогда как Java молча даёт значение по умолчанию: > ```java > class Sneaky { > int b = a + 1; // компилируется, b = 1, потому что a == 0 > int a = 10; > } > ``` > Через вызов метода обход возможен в обоих языках. > > **Главная защита - закрытые классы.** Ловушка с `open`-методом в конструкторе требует, чтобы класс был наследуемым. Поскольку в Kotlin класс закрыт по умолчанию, большинство классов в эту ловушку попасть не может в принципе. > > Сравнить: [Блоки инициализации в Java](../../Программирование%20на%20Java/ООП/Блоки%20инициализации.md), [Конструкторы](../../Программирование%20на%20Java/ООП/Конструкторы.md) > [!warning] Подводные камни > **Вызов `open`-метода из `init` видит неинициализированный подкласс** - значение будет `null` даже у non-null свойства: > ```kotlin > open class Base9 { > init { check() } > open fun check() {} > } > > class Derived9 : Base9() { > private val name: String = "Иван" > override fun check() { > @Suppress("SENSELESS_COMPARISON") > println("name инициализирован: " + (name != null)) > } > } > > fun main() { Derived9() } // name инициализирован: false > ``` > Это единственный штатный способ увидеть `null` в переменной типа `String`. > > **Порядок объявления свойств меняет поведение**, если между ними есть зависимость через методы. Перестановка двух строк может дать `0` вместо правильного значения - без ошибки компиляции. > > **Тело вторичного конструктора выполняется последним**, уже после всех `init`. Код, который ожидает обратного порядка, получит неожиданный результат. > > **`lateinit` не инициализируется автоматически** - обращение до присваивания даёт `UninitializedPropertyAccessException`. > > **Круговая зависимость между `object` даёт `null` или `0` без ошибки:** > ```kotlin > object First { val value: String = Second.value + "-first" } > object Second { val value: String = "second" } > > fun main() { println(First.value) } // second-first > ``` > Если поменять зависимость местами, получится `null-second`: один из объектов будет читать поле другого во время его инициализации. > > **Исключение в `init` оставляет побочные эффекты.** Частая ошибка - регистрация `this` в глобальном реестре до валидации: объект создан не будет, а ссылка на него останется. > > **Утечка `this` из конструктора.** Передача `this` куда-либо в `init` открывает доступ к недосозданному объекту: > ```kotlin > class Leaky { > val items = mutableListOf() > init { register(this) } // объект ещё не готов > val name: String = "готов" > > companion object { > val all = mutableListOf() > fun register(l: Leaky) = all.add(l) > } > } > > fun main() { > Leaky() > println("в реестре: ${Leaky.all.size}") // в реестре: 1 > } > ``` > Другой поток, получивший такую ссылку, увидит `name` как `null`. Дополнительно: [Классы и конструкторы](Классы%20и%20конструкторы.md) Далее: [Модификаторы видимости](Модификаторы%20видимости.md)