Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user