--- title: Наследование description: open, вызов конструктора базового класса, наследование свойств и выбор в пользу композиции tags: - kotlin - oop --- Класс закрыт для наследования, пока не объявлен `open`. ```kotlin open class Animal(val name: String) { open fun sound() = "..." fun describe() = "$name говорит ${sound()}" // final: переопределить нельзя } class Dog(name: String) : Animal(name) { override fun sound() = "Гав" } fun main() { println(Dog("Рекс").describe()) // Рекс говорит Гав } ``` `describe()` не `open`, но вызывает `sound()`, который переопределён - это и есть полиморфизм: шаблон поведения в базовом классе, детали в подклассе. ## Вызов конструктора базового класса Указывается в заголовке: ```kotlin open class Base12(val id: Int) class Derived12(id: Int, val name: String) : Base12(id) fun main() { val d = Derived12(1, "Иван") println("${d.id} ${d.name}") // 1 Иван } ``` Аргументы можно вычислять: ```kotlin open class Shape2(val area: Double) class Circle2(val radius: Double) : Shape2(Math.PI * radius * radius) fun main() { println("%.2f".format(java.util.Locale.US, Circle2(1.0).area)) // 3.14 } ``` Если у подкласса нет основного конструктора, базовый вызывается из каждого вторичного через `super`: ```kotlin open class Base13(val source: String) class Derived13 : Base13 { constructor(file: java.io.File) : super("файл ${file.name}") constructor(url: String, port: Int) : super("$url:$port") } fun main() { println(Derived13("http://x.ru", 80).source) // http://x.ru:80 } ``` Скобки в заголовке обязательны, даже если конструктор без параметров: ```kotlin open class Base14 class Derived14 : Base14() // скобки = вызов конструктора interface Marker2 class Impl3 : Marker2 // у интерфейса скобок нет fun main() { println(Derived14()::class.simpleName) } // Derived14 ``` Наличие скобок различает класс и интерфейс в списке супертипов. ## Что можно наследовать ```kotlin open class Base15 { open val version: Int = 1 open var state: String = "новый" open fun run2() = "база" val fixed = "не переопределить" // не open } class Derived15 : Base15() { override val version = 2 override var state: String = "изменённый" override fun run2() = "подкласс" } fun main() { val d: Base15 = Derived15() println("${d.version} ${d.state} ${d.run2()} ${d.fixed}") // 2 изменённый подкласс не переопределить } ``` `open` нужен каждому члену отдельно: открытый класс не делает своих членов открытыми. ### val можно переопределить как var ```kotlin open class Base16 { open val value: Int = 1 } class Derived16 : Base16() { override var value: Int = 2 // добавили сеттер } fun main() { val d = Derived16() d.value = 5 println(d.value) // 5 } ``` Обратное запрещено: `var` нельзя сузить до `val` - это сломало бы код, который присваивает значение. ### Переопределение свойства геттером ```kotlin open class Base17 { open val size: Int = 0 } class Derived17(private val items: List) : Base17() { override val size: Int get() = items.size // вычисляемое вместо хранимого } fun main() { println(Derived17(listOf(1, 2, 3)).size) // 3 } ``` ## Свойство в конструкторе подкласса ```kotlin open class Base18 { open val name: String = "базовое" } class Derived18(override val name: String) : Base18() fun main() { println(Derived18("своё").name) // своё } ``` `override` прямо в параметре конструктора - короткая и частая форма. ## Абстрактный член не требует open ```kotlin abstract class Shape3 { abstract val area: Double // open подразумевается abstract fun describe(): String open fun show() = "фигура: ${describe()}" } class Square2(val side: Double) : Shape3() { override val area get() = side * side override fun describe() = "квадрат со стороной $side" } fun main() { println(Square2(2.0).show()) // фигура: квадрат со стороной 2.0 } ``` Подробнее: [Абстрактные классы](Абстрактные%20классы.md) ## Одиночное наследование, множественные интерфейсы ```kotlin open class Entity(val id: Int) interface Serializable2 { fun serialize(): String } interface Validatable { fun validate(): Boolean } class User4(id: Int, val name: String) : Entity(id), Serializable2, Validatable { override fun serialize() = """{"id":$id,"name":"$name"}""" override fun validate() = name.isNotBlank() } fun main() { val u = User4(1, "Иван") println(u.serialize()) // {"id":1,"name":"Иван"} println(u.validate()) // true } ``` Класс в списке супертипов идёт первым - это соглашение, а не требование компилятора. ## Что наследовать нельзя ```kotlin class Final2 // закрыт по умолчанию data class Point3(val x: Int) // data class не может быть open @JvmInline value class Id2(val value: Int) // value class тоже object Singleton2 // object enum class Status2 { OK } // enum sealed class Limited // только в своём модуле и пакете fun main() { println("перечислены закрытые формы") } ``` Для `enum` есть исключение: отдельная константа может переопределить метод, образуя анонимный подкласс. Подробнее: [Enum class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md) ## Композиция вместо наследования Наследование связывает подкласс с реализацией базового класса. Изменение базового класса может сломать подкласс, даже если публичный контракт не менялся - это проблема хрупкого базового класса. Классический пример - подсчёт добавленных элементов: ```kotlin // плохо: наследование от реализации class CountingList : ArrayList() { var added = 0 override fun add(element: T): Boolean { added++ return super.add(element) } override fun addAll(elements: Collection): Boolean { added += elements.size return super.addAll(elements) // внутри может вызвать add()! } } fun main() { val list = CountingList() list.addAll(listOf(1, 2, 3)) println("добавлено: ${list.added}, реально: ${list.size}") } ``` На текущем JDK выведется `добавлено: 3, реально: 3` - потому что `ArrayList.addAll` копирует массив напрямую и `add` не вызывает. Но это не гарантия: реализация, которая реализует `addAll` через цикл с `add`, удвоит счётчик. Корректность класса зависит от внутренностей базового, а они не входят в его контракт и могут измениться между версиями JDK. Композиция такой зависимости не создаёт: ```kotlin class CountingList2 { private val items = mutableListOf() var added = 0 private set fun add(element: T) { added++; items.add(element) } fun addAll(elements: Collection) { elements.forEach { add(it) } } val size: Int get() = items.size } fun main() { val list = CountingList2() list.addAll(listOf(1, 2, 3)) println("добавлено: ${list.added}, реально: ${list.size}") // 3, 3 } ``` Kotlin делает композицию такой же короткой, как наследование, - через делегирование `by`. Подробнее: [Делегирование](Делегирование.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `class A extends B` | `class A : B()` | > | `implements I` | та же запись через запятую | > | класс открыт, `final` закрывает | закрыт, `open` открывает | > | метод виртуален по умолчанию | финален по умолчанию | > | `super(args)` первой строкой тела | `: Base(args)` в заголовке | > | `@Override` опциональна | `override` обязательно | > | нельзя переопределить поле | свойство переопределяется | > > **Закрытость по умолчанию - осознанный выбор.** Проектировать класс под наследование дорого: нужно документировать, какие методы вызывают какие, и поддерживать это между версиями. Java по умолчанию делает всё наследуемым, из-за чего библиотечные классы либо получают `final`, либо навсегда остаются связанными своими внутренностями. > > **Переопределение свойств** в Java невозможно - поля скрываются, а не переопределяются: > ```java > class Base { int value = 1; } > class Derived extends Base { int value = 2; } // сокрытие, не переопределение > // ((Base) new Derived()).value == 1 > ``` > Обращение по ссылке базового типа даст поле базового класса. В Kotlin свойство - это методы доступа, поэтому `override` работает полиморфно и такой ловушки нет. > > Сравнить: [Наследование в Java](../../Программирование%20на%20Java/ООП/Наследование/Наследование.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md) > [!warning] Подводные камни > **`open` у класса не открывает его члены** - каждый метод и свойство нужно помечать отдельно. > > **Переопределённый метод остаётся `open`.** Чтобы закрыть ветку иерархии, нужен `final override`: > ```kotlin > open class A4 { open fun f() = "A" } > open class B4 : A4() { final override fun f() = "B" } > // class C4 : B4() { override fun f() = "C" } // ошибка: f закрыт > > fun main() { println(B4().f()) } // B > ``` > > **Вызов `open`-метода из конструктора видит неинициализированный подкласс:** > ```kotlin > open class Base19 { > init { println("из init: ${describe()}") } > open fun describe() = "база" > } > > class Derived19 : Base19() { > private val name: String = "Иван" > override fun describe() = "подкласс, name=$name" > } > > fun main() { Derived19() } // из init: подкласс, name=null > ``` > Подробнее: [Инициализация](Инициализация.md) > > **Скобки обязательны при наследовании класса**: `: Base()`, а не `: Base`. Без скобок компилятор ищет интерфейс. > > **`protected` в Kotlin не даёт доступа из пакета** - только из подклассов. Код, перенесённый из Java, может перестать компилироваться. > > **`var` нельзя переопределить как `val`** - только наоборот. > > **Наследование от классов стандартной библиотеки хрупко.** `ArrayList`, `HashMap` и подобные не документируют, какие методы вызывают друг друга внутри. Используйте делегирование. > > **Глубокие иерархии трудно читать.** Чтобы понять поведение класса в иерархии из пяти уровней, нужно просмотреть все пять. Практическое правило: наследование - для «является» и когда базовый класс спроектирован под него; во всех остальных случаях - композиция. > > **Нельзя наследоваться от `data class`**, поэтому иерархии носителей данных строят на `sealed`-интерфейсах с `data class`-реализациями. Дополнительно: [Переопределение](Переопределение.md) Далее: [Переопределение](Переопределение.md)