14 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Наследование | open, вызов конструктора базового класса, наследование свойств и выбор в пользу композиции |
|
Класс закрыт для наследования, пока не объявлен open.
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(), который переопределён - это и есть полиморфизм: шаблон поведения в базовом классе, детали в подклассе.
Вызов конструктора базового класса
Указывается в заголовке:
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 Иван
}
Аргументы можно вычислять:
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:
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
}
Скобки в заголовке обязательны, даже если конструктор без параметров:
open class Base14
class Derived14 : Base14() // скобки = вызов конструктора
interface Marker2
class Impl3 : Marker2 // у интерфейса скобок нет
fun main() { println(Derived14()::class.simpleName) } // Derived14
Наличие скобок различает класс и интерфейс в списке супертипов.
Что можно наследовать
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
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 - это сломало бы код, который присваивает значение.
Переопределение свойства геттером
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
}
Свойство в конструкторе подкласса
open class Base18 {
open val name: String = "базовое"
}
class Derived18(override val name: String) : Base18()
fun main() {
println(Derived18("своё").name) // своё
}
override прямо в параметре конструктора - короткая и частая форма.
Абстрактный член не требует open
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
}
Подробнее: Абстрактные классы
Одиночное наследование, множественные интерфейсы
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
}
Класс в списке супертипов идёт первым - это соглашение, а не требование компилятора.
Что наследовать нельзя
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
Композиция вместо наследования
Наследование связывает подкласс с реализацией базового класса. Изменение базового класса может сломать подкласс, даже если публичный контракт не менялся - это проблема хрупкого базового класса.
Классический пример - подсчёт добавленных элементов:
// плохо: наследование от реализации
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.
Композиция такой зависимости не создаёт:
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. Подробнее: Делегирование
[!info] Отличия от Java
Java Kotlin class A extends Bclass A : B()implements Iта же запись через запятую класс открыт, finalзакрываетзакрыт, openоткрываетметод виртуален по умолчанию финален по умолчанию super(args)первой строкой тела: Base(args)в заголовке@Overrideопциональнаoverrideобязательнонельзя переопределить поле свойство переопределяется Закрытость по умолчанию - осознанный выбор. Проектировать класс под наследование дорого: нужно документировать, какие методы вызывают какие, и поддерживать это между версиями. Java по умолчанию делает всё наследуемым, из-за чего библиотечные классы либо получают
final, либо навсегда остаются связанными своими внутренностями.Переопределение свойств в Java невозможно - поля скрываются, а не переопределяются:
class Base { int value = 1; } class Derived extends Base { int value = 2; } // сокрытие, не переопределение // ((Base) new Derived()).value == 1Обращение по ссылке базового типа даст поле базового класса. В Kotlin свойство - это методы доступа, поэтому
overrideработает полиморфно и такой ловушки нет.Сравнить: Наследование в Java, Сокрытие
[!warning] Подводные камни
openу класса не открывает его члены - каждый метод и свойство нужно помечать отдельно.Переопределённый метод остаётся
open. Чтобы закрыть ветку иерархии, нуженfinal override: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-метода из конструктора видит неинициализированный подкласс: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Подробнее: Инициализация
Скобки обязательны при наследовании класса:
: Base(), а не: Base. Без скобок компилятор ищет интерфейс.
protectedв Kotlin не даёт доступа из пакета - только из подклассов. Код, перенесённый из Java, может перестать компилироваться.
varнельзя переопределить какval- только наоборот.Наследование от классов стандартной библиотеки хрупко.
ArrayList,HashMapи подобные не документируют, какие методы вызывают друг друга внутри. Используйте делегирование.Глубокие иерархии трудно читать. Чтобы понять поведение класса в иерархии из пяти уровней, нужно просмотреть все пять. Практическое правило: наследование - для «является» и когда базовый класс спроектирован под него; во всех остальных случаях - композиция.
Нельзя наследоваться от
data class, поэтому иерархии носителей данных строят наsealed-интерфейсах сdata class-реализациями.
Дополнительно: Переопределение Далее: Переопределение