Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+307
View File
@@ -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)