--- 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 get() = listOf(1, 2) } class Derived20 : Base20() { override val items: List 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.f() + super.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.f()} + ${super.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) : 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.method()` | > | поля скрываются | свойства переопределяются | > | ковариантный результат (Java 5+) | то же | > > **Обязательный `override`** - главное практическое улучшение. В Java `@Override` необязательна, поэтому опечатка в имени тихо создаёт новый метод, который никогда не вызовется. > > **Конфликт реализаций интерфейсов** решается похоже, но синтаксис другой: `A.super.f()` в Java против `super.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 = 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)