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

14 KiB
Raw Blame History

title, description, tags
title description tags
Переопределение override и final, вызов super, конфликт реализаций и ковариантность результата
kotlin
oop

override обязательно. Без него член не переопределяет, а конфликтует с родительским.

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:

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

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 работает и для свойств - через их геттеры:

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:

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()) }   // внешний / база

Конфликт реализаций

Если два супертипа дают реализацию одного метода, компилятор требует разрешить конфликт явно:

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 не выбирает реализацию за вас.

То же с классом и интерфейсом:

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()) }     // класс + интерфейс

Подробнее: Интерфейсы

Что можно переопределять

Член Условие
метод open или abstract
val open или abstract; можно расширить до var
var open или abstract; сузить до val нельзя
свойство в конструкторе override val в параметре
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
}

Ковариантность результата

Переопределение может сузить тип результата:

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, который им является.

Параметры сузить нельзя - это сломало бы контракт:

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 при переопределении

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:

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 открыты всегда:

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

Переопределение и расширения

Функция-расширение не переопределяется - она разрешается статически, по типу ссылки:

open class Base27
class Derived27 : Base27()

fun Base27.describe() = "база"
fun Derived27.describe() = "подкласс"

fun main() {
    val d: Base27 = Derived27()
    println(d.describe())             // база — по типу переменной!
    println(Derived27().describe())    // подкласс
}

Это принципиальное отличие от методов. Если нужен полиморфизм - нужен метод класса. Подробнее: Функции-расширения

Расширение не может скрыть метод с той же сигнатурой: метод всегда побеждает.

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 нет. Поле подкласса с тем же именем скрывает поле базового класса, и какое из них увидит код, зависит от типа ссылки:

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, Сокрытие, Ad-hoc полиморфизм

[!warning] Подводные камни Расширение не переопределяется - выбор идёт по статическому типу. Это самая частая ошибка ожиданий:

open class Base28
class Derived28 : Base28()

fun Base28.name() = "база"
fun Derived28.name() = "подкласс"

fun main() {
    val list: List<Base28> = listOf(Derived28())
    println(list[0].name())      // база — не "подкласс"
}

Перегрузка вместо переопределения компилируется молча:

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-метода из конструктора попадает в подкласс до его инициализации. Подробнее: Инициализация

super в лямбде внутри метода работает, а в объекте-выражении - относится к нему:

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 { }, подкласс должен задать своё хранилище - делегат базового класса не переиспользуется:

open class Base31 {
    open val value: String by lazy { "база" }
}

class Derived31 : Base31() {
    override val value: String by lazy { "подкласс" }     // свой делегат
}

fun main() { println(Derived31().value) }    // подкласс

Дополнительно: Полиморфизм Далее: Абстрактные классы