15 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Инициализация | Порядок выполнения init и инициализаторов, инициализация при наследовании и ловушка open-метода |
|
Инициализаторы свойств и блоки init выполняются в порядке их объявления в теле класса - как единая последовательность.
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 может быть несколько; их используют, когда инициализация не сводится к одному выражению.
Параметры конструктора доступны сразу
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}") // ИВАН: взрослый
}
Порядок при наследовании
Базовый класс инициализируется полностью до начала инициализации подкласса:
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.
Аргументы для конструктора базового класса вычисляются до всего остального:
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-метода из конструктора базового класса попадёт в переопределённую версию - до того, как подкласс инициализирован.
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 - и это ещё труднее заметить, потому что исключения не возникает:
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-типа не видно:
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 - если поведение подкласса нужно при создании - передавать его как параметр конструктора
open class Base8(private val label: String) {
init { println("метка: $label") } // параметр уже готов
}
class Derived8 : Base8("Derived") {
val name = "Иван"
}
fun main() { Derived8() } // метка: Derived
Порядок объявления имеет значение
Свойство нельзя использовать до его объявления:
class Bad3 {
// val b = a + 1 // ошибка: variable 'a' must be initialized
val a = 1
val b = a + 1
}
fun main() { println(Bad3().b) } // 2
Компилятор ловит это для прямых обращений. Но через метод проверка обходится:
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 снимает её: значение вычисляется при первом обращении, когда объект уже собран.
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
Подробнее: Свойства
Исключение в инициализации
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
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
[!info] Отличия от Java
Java Kotlin блок { }в теле классаinit { }static { }initвcompanion objectприсваивание в конструкторе инициализатор свойства super(args)первой строкой: Base(args)в заголовкенет проверки порядка ошибка при прямом обращении вперёд Порядок инициализации одинаков: базовый класс, потом подкласс; внутри класса - по порядку объявления. Проблема вызова переопределяемого метода из конструктора тоже одинакова.
Kotlin частично защищает от неинициализированных свойств. Прямое обращение к свойству до его объявления - ошибка компиляции, тогда как Java молча даёт значение по умолчанию:
class Sneaky { int b = a + 1; // компилируется, b = 1, потому что a == 0 int a = 10; }Через вызов метода обход возможен в обоих языках.
Главная защита - закрытые классы. Ловушка с
open-методом в конструкторе требует, чтобы класс был наследуемым. Поскольку в Kotlin класс закрыт по умолчанию, большинство классов в эту ловушку попасть не может в принципе.Сравнить: Блоки инициализации в Java, Конструкторы
[!warning] Подводные камни Вызов
open-метода изinitвидит неинициализированный подкласс - значение будетnullдаже у non-null свойства: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без ошибки: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открывает доступ к недосозданному объекту: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.
Дополнительно: Классы и конструкторы Далее: Модификаторы видимости