14 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Переопределение | override и final, вызов super, конфликт реализаций и ковариантность результата |
|
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 overridesuper.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) } // подкласс
Дополнительно: Полиморфизм Далее: Абстрактные классы