Какой порядок инициализации переменных и выполнения блоков кода при создании экземпляра класса: companion object, init блок, переменная в конструкторе, обычная переменная?
Комментарии (3)
Ответ сгенерирован нейросетью и может содержать ошибки
Порядок инициализации в Kotlin при создании экземпляра класса
При создании экземпляра класса в Kotlin существует строго определённый порядок выполнения различных блоков кода и инициализации переменных. Это важно понимать для предотвращения ошибок, связанных с зависимостью от состояния объекта. Давайте разберём всё по шагам.
Общая последовательность
- Инициализация companion object (если он есть в классе) — происходит до создания любого экземпляра класса.
- Выполнение блоков
initи инициализация свойств (полей) класса в порядке их объявления в теле класса. - Выполнение кода конструктора класса (тела primary или secondary конструктора).
Подробное объяснение каждого этапа
1. Companion Object
Companion object инициализируется первым, но это происходит ещё до создания первого экземпляра класса. Companion object является синглтоном, связанным с классом, и его блоки init и свойства инициализируются при первом обращении к классу (или при загрузке класса JVM). Порядок внутри companion object аналогичен порядку в обычном классе: сначала инициализируются свойства, затем выполняются блоки init в порядке объявления.
class MyClass {
companion object {
val companionProperty = "Companion Property".also { println(it) }
init {
println("Companion init block")
}
}
}
// При первом обращении к MyClass (или MyClass.Companion) выведет:
// Companion Property
// Companion init block
2. Инициализация свойств и блоков init в теле класса
После вызова конструктора начинается процесс инициализации самого экземпляра. Kotlin выполняет в порядке их объявления в классе:
- Инициализацию обычных переменных (свойств), определённых в теле класса.
- Выполнение всех блоков
init.
Это ключевой момент: блоки init не выполняются все сразу перед конструктором, а перемежаются с инициализацией свойств в том порядке, как они написаны в коде.
class Example(val constructorParam: String) {
// 1. Первое свойство в классе
val firstProperty = "First".also { println(it) }
// 2. Первый блок init
init {
println("First init block: $constructorParam")
}
// 3. Второе свойство
val secondProperty = "Second".also { println(it) }
// 4. Второй блок init
init {
println("Second init block")
}
}
// При создании Example("Hello") выведет:
// First
// First init block: Hello
// Second
// Second init block
3. Выполнение кода конструктора
Тело конструктора выполняется последним, после того как все свойства инициализированы и все блоки init выполнены. В primary конструкторе код выполняется в секции init, которая является частью primary конструктора. В secondary конструкторах код выполняется после вызова this() (для primary конструктора) и после всей инициализации свойств и блоков init.
class Person(val name: String) {
val greeting = "Hello, $name".also { println(it) }
init {
println("Init block for $name")
}
// Это часть primary конструктора, выполняется после свойств и init блоков
// Но логически это "тело конструктора"
// В данном примере у primary конструктора нет дополнительного кода кроме инициализации свойств и init блоков.
}
Для secondary конструктора:
class Person {
val property = "Property".also { println(it) }
init {
println("Init block")
}
constructor(name: String) {
// Этот код выполняется ПОСЛЕ инициализации property и init блоков
println("Secondary constructor body: $name")
}
}
// При Person("John") выведет:
// Property
// Init block
// Secondary constructor body: John
Особые случаи и важные замечания
- Зависимости между свойствами: поскольку инициализация происходит последовательно, вы можете использовать ранее объявленные свойства для инициализации последующих. Однако попытка использовать свойство, объявленное позже, приведёт к ошибке или неожиданному поведению (например, значению
nullдляlateinit var). lateinit var: эти переменные не инициализируются в данном порядке. Их присвоение значения происходит позже, вручную. Проверка на инициализацию (isInitialized) возможна только в пределах, где доступен backing field.- Вычисляемые свойства (
get()): их "инициализация" (вызов геттера) происходит при каждом обращении, а не в процессе создания объекта. - Делегированные свойства (
by): инициализация делегированного свойства происходит при первом обращении к геттеру/сеттеру (в зависимости от делегата), но не во время общей инициализации экземпляра. Однако сам делегат может быть создан во время инициализации.
Практический пример с полной последовательностью
class Order(val id: Int) {
companion object {
val MAX_ID = 1000.also { println("Companion property") }
init { println("Companion init") }
}
val item = "Item $id".also { println("Property item") }
init { println("Init 1: $item") }
val price = 100.also { println("Property price") }
init { println("Init 2: Total ${price * 1}") }
// Secondary constructor
constructor(id: Int, discount: Int) : this(id) {
println("Secondary constructor body with discount $discount")
}
}
// Создание экземпляра: Order(1, 10)
// Предварительно, при первом обращении к классу Order:
// Companion property
// Companion init
// Затем при создании экземпляра:
// Property item
// Init 1: Item 1
// Property price
// Init 2: Total 100
// Secondary constructor body with discount 10
Заключение: Порядок инициализации в Kotlin строгий и логичный. Companion object живёт отдельно от экземпляров. При создании объекта сначала инициализируются свойства и блоки init в порядке их написания в классе, а затем выполняется тело конструктора. Это обеспечивает контроль над процессом построения объекта и помогает избегать ошибок, связанных с неинициализированными состояниями.