321 lines
15 KiB
Markdown
321 lines
15 KiB
Markdown
---
|
|
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<String>()
|
|
}
|
|
|
|
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<Leaky>()
|
|
> init { register(this) } // объект ещё не готов
|
|
> val name: String = "готов"
|
|
>
|
|
> companion object {
|
|
> val all = mutableListOf<Leaky>()
|
|
> fun register(l: Leaky) = all.add(l)
|
|
> }
|
|
> }
|
|
>
|
|
> fun main() {
|
|
> Leaky()
|
|
> println("в реестре: ${Leaky.all.size}") // в реестре: 1
|
|
> }
|
|
> ```
|
|
> Другой поток, получивший такую ссылку, увидит `name` как `null`.
|
|
|
|
Дополнительно: [Классы и конструкторы](Классы%20и%20конструкторы.md)
|
|
Далее: [Модификаторы видимости](Модификаторы%20видимости.md)
|