Files
obsidian/Kotlin/ООП/Переопределение.md
T

342 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: override и final, вызов super, конфликт реализаций и ковариантность результата
tags:
- kotlin
- oop
---
`override` обязательно. Без него член не переопределяет, а конфликтует с родительским.
```kotlin
open class Animal2 {
open fun sound() = "..."
}
class Cat : Animal2() {
override fun sound() = "Мяу"
// fun sound() = "Мяу" // ошибка: 'sound' hides member and needs 'override'
}
fun main() {
val a: Animal2 = Cat()
println(a.sound()) // Мяу — вызов по фактическому типу
}
```
Это закрывает два класса ошибок: опечатка в имени создала бы новый метод, а добавление метода в базовый класс случайно «переопределилось» бы существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции.
## final override
Переопределённый член остаётся открытым для дальнейшего переопределения. Закрыть ветку - `final`:
```kotlin
open class A5 { open fun f() = "A" }
open class B5 : A5() { final override fun f() = "B" }
// class C5 : B5() { override fun f() = "C" } // ошибка: f закрыт
fun main() { println(B5().f()) } // B
```
## super
```kotlin
open class Logger3 {
open fun log(msg: String) = "[базовый] $msg"
}
class TimestampLogger : Logger3() {
override fun log(msg: String) = super.log("[время] $msg")
}
fun main() {
println(TimestampLogger().log("привет")) // [базовый] [время] привет
}
```
`super` работает и для свойств - через их геттеры:
```kotlin
open class Base20 {
open val items: List<Int> get() = listOf(1, 2)
}
class Derived20 : Base20() {
override val items: List<Int> get() = super.items + 3
}
fun main() { println(Derived20().items) } // [1, 2, 3]
```
Из вложенного класса к `super` внешнего обращаются через `super@Outer`:
```kotlin
open class Base21 {
open fun name() = "база"
}
class Outer4 : Base21() {
override fun name() = "внешний"
inner class Inner4 {
fun bothNames() = "${name()} / ${super@Outer4.name()}"
}
}
fun main() { println(Outer4().Inner4().bothNames()) } // внешний / база
```
## Конфликт реализаций
Если два супертипа дают реализацию одного метода, компилятор требует разрешить конфликт явно:
```kotlin
interface A6 { fun f() = "A" }
interface B6 { fun f() = "B" }
class C6 : A6, B6 {
override fun f() = super<A6>.f() + super<B6>.f() // указываем источник
}
fun main() { println(C6().f()) } // AB
```
Без `override` такой класс не скомпилируется - Kotlin не выбирает реализацию за вас.
То же с классом и интерфейсом:
```kotlin
open class Base22 { open fun f() = "класс" }
interface Mixin { fun f() = "интерфейс" }
class Impl4 : Base22(), Mixin {
override fun f() = "${super<Base22>.f()} + ${super<Mixin>.f()}"
}
fun main() { println(Impl4().f()) } // класс + интерфейс
```
Подробнее: [Интерфейсы](Интерфейсы.md)
## Что можно переопределять
| Член | Условие |
|---|---|
| метод | `open` или `abstract` |
| `val` | `open` или `abstract`; можно расширить до `var` |
| `var` | `open` или `abstract`; сузить до `val` нельзя |
| свойство в конструкторе | `override val` в параметре |
```kotlin
abstract class Base23 {
abstract val size: Int
abstract var label: String
abstract fun compute(): Int
}
class Impl5(override var label: String, private val items: List<Int>) : Base23() {
override val size get() = items.size
override fun compute() = items.sum()
}
fun main() {
val i = Impl5("данные", listOf(1, 2, 3))
println("${i.label} ${i.size} ${i.compute()}") // данные 3 6
}
```
## Ковариантность результата
Переопределение может сузить тип результата:
```kotlin
open class Animal3
class Dog2 : Animal3()
open class Shelter {
open fun adopt(): Animal3 = Animal3()
}
class DogShelter : Shelter() {
override fun adopt(): Dog2 = Dog2() // Dog2 — подтип Animal3
}
fun main() {
val s: Shelter = DogShelter()
println(s.adopt()::class.simpleName) // Dog2
}
```
Это безопасно: клиент ожидает `Animal3`, а получает `Dog2`, который им является.
Параметры сузить нельзя - это сломало бы контракт:
```kotlin
open class Base24 {
open fun process(x: Any) = "any: $x"
}
class Derived24 : Base24() {
override fun process(x: Any) = "переопределено: $x"
fun process(x: String) = "перегрузка: $x" // это перегрузка, не override
}
fun main() {
val d = Derived24()
println(d.process("текст")) // перегрузка: текст
println((d as Base24).process("текст")) // переопределено: текст
}
```
Вызов зависит от статического типа ссылки - классическая ловушка смешения перегрузки и переопределения.
## Nullability при переопределении
```kotlin
open class Base25 {
open fun find(id: Int): String? = null
}
class Derived25 : Base25() {
override fun find(id: Int): String = "найдено $id" // сузили String? до String
}
fun main() {
val b: Base25 = Derived25()
println(b.find(1)) // найдено 1
}
```
Результат можно сделать non-null - это сужение. А параметр, наоборот, можно расширить до nullable:
```kotlin
open class Base26 {
open fun handle(x: String) = "строка"
}
class Derived26 : Base26() {
override fun handle(x: String) = "подкласс: $x"
}
fun main() { println(Derived26().handle("a")) } // подкласс: a
```
## Переопределение equals, hashCode, toString
Члены `Any` открыты всегда:
```kotlin
class Point4(val x: Int, val y: Int) {
override fun equals(other: Any?) = other is Point4 && other.x == x && other.y == y
override fun hashCode() = 31 * x + y
override fun toString() = "($x, $y)"
}
fun main() {
println(Point4(1, 2)) // (1, 2)
println(Point4(1, 2) == Point4(1, 2)) // true
}
```
Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
## Переопределение и расширения
Функция-расширение **не** переопределяется - она разрешается статически, по типу ссылки:
```kotlin
open class Base27
class Derived27 : Base27()
fun Base27.describe() = "база"
fun Derived27.describe() = "подкласс"
fun main() {
val d: Base27 = Derived27()
println(d.describe()) // база — по типу переменной!
println(Derived27().describe()) // подкласс
}
```
Это принципиальное отличие от методов. Если нужен полиморфизм - нужен метод класса. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
Расширение не может скрыть метод с той же сигнатурой: метод всегда побеждает.
```kotlin
class Service13 {
fun run2() = "метод"
}
fun Service13.run2() = "расширение" // никогда не вызовется
fun main() { println(Service13().run2()) } // метод
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `@Override` опциональна | `override` обязательно |
> | метод виртуален, `final` закрывает | нужен `open`, иначе финален |
> | переопределённый метод остаётся виртуальным | то же, закрывается `final override` |
> | `super.method()` | то же |
> | `A.super.method()` при конфликте | `super<A>.method()` |
> | поля скрываются | свойства переопределяются |
> | ковариантный результат (Java 5+) | то же |
>
> **Обязательный `override`** - главное практическое улучшение. В Java `@Override` необязательна, поэтому опечатка в имени тихо создаёт новый метод, который никогда не вызовется.
>
> **Конфликт реализаций интерфейсов** решается похоже, но синтаксис другой: `A.super.f()` в Java против `super<A>.f()` в Kotlin. Требование явно разрешать конфликт одинаково строгое в обоих языках.
>
> **Сокрытие полей** - проблема Java, которой в Kotlin нет. Поле подкласса с тем же именем скрывает поле базового класса, и какое из них увидит код, зависит от типа ссылки:
> ```java
> class Base { int value = 1; }
> class Derived extends Base { int value = 2; }
> Base b = new Derived();
> // b.value == 1, ((Derived) b).value == 2
> ```
> В Kotlin свойства - это методы доступа, поэтому `override` работает полиморфно.
>
> **Статические методы в Java не переопределяются, а скрываются.** В Kotlin статики нет; члены `companion object` тоже не переопределяются, но путаницы меньше, потому что компаньон не наследуется.
>
> Сравнить: [Переопределение в Java](../../Программирование%20на%20Java/ООП/Наследование/Переопределение.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md)
> [!warning] Подводные камни
> **Расширение не переопределяется** - выбор идёт по статическому типу. Это самая частая ошибка ожиданий:
> ```kotlin
> open class Base28
> class Derived28 : Base28()
>
> fun Base28.name() = "база"
> fun Derived28.name() = "подкласс"
>
> fun main() {
> val list: List<Base28> = listOf(Derived28())
> println(list[0].name()) // база — не "подкласс"
> }
> ```
>
> **Перегрузка вместо переопределения компилируется молча:**
> ```kotlin
> open class Base29 { open fun f(x: Any) = "any" }
> class Derived29 : Base29() {
> override fun f(x: Any) = "override"
> fun f(x: Int) = "overload"
> }
>
> fun main() {
> val d = Derived29()
> println(d.f(1)) // overload — точнее по типу
> println((d as Base29).f(1)) // override — по типу ссылки
> }
> ```
>
> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md)
>
> **`super` в лямбде внутри метода работает, а в объекте-выражении - относится к нему:**
> ```kotlin
> open class Base30 { open fun f() = "база" }
>
> class Derived30 : Base30() {
> override fun f() = "подкласс"
> fun test(): String {
> val fromLambda = listOf(1).map { super.f() }.first() // база
> return fromLambda
> }
> }
>
> fun main() { println(Derived30().test()) } // база
> ```
>
> **Переопределение `equals` без `hashCode` ломает хеш-коллекции.** Компилятор не предупреждает.
>
> **Сужение видимости при переопределении запрещено**, а расширение - разрешено и легко делается случайно: `public override` у `protected`-метода публикует внутреннюю деталь.
>
> **Делегат свойства не наследуется.** Переопределяя `val x by lazy { }`, подкласс должен задать своё хранилище - делегат базового класса не переиспользуется:
> ```kotlin
> open class Base31 {
> open val value: String by lazy { "база" }
> }
>
> class Derived31 : Base31() {
> override val value: String by lazy { "подкласс" } // свой делегат
> }
>
> fun main() { println(Derived31().value) } // подкласс
> ```
Дополнительно: [Полиморфизм](Полиморфизм.md)
Далее: [Абстрактные классы](Абстрактные%20классы.md)