Files
obsidian/Kotlin/ООП/Наследование.md
T

308 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Int>) : 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<T> : ArrayList<T>() {
var added = 0
override fun add(element: T): Boolean {
added++
return super.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
added += elements.size
return super.addAll(elements) // внутри может вызвать add()!
}
}
fun main() {
val list = CountingList<Int>()
list.addAll(listOf(1, 2, 3))
println("добавлено: ${list.added}, реально: ${list.size}")
}
```
На текущем JDK выведется `добавлено: 3, реально: 3` - потому что `ArrayList.addAll` копирует массив напрямую и `add` не вызывает. Но это не гарантия: реализация, которая реализует `addAll` через цикл с `add`, удвоит счётчик. Корректность класса зависит от внутренностей базового, а они не входят в его контракт и могут измениться между версиями JDK.
Композиция такой зависимости не создаёт:
```kotlin
class CountingList2<T> {
private val items = mutableListOf<T>()
var added = 0
private set
fun add(element: T) { added++; items.add(element) }
fun addAll(elements: Collection<T>) { elements.forEach { add(it) } }
val size: Int get() = items.size
}
fun main() {
val list = CountingList2<Int>()
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)