Наследование
Для всех классов в Kotlin родительским суперклассом является класс Any
. Он также является родительским классом для любого класса,
в котором не указан какой-либо другой родительский класс.
class Example // Неявно наследуется от Any
У Any
есть три метода: equals()
, hashCode()
и toString()
. Эти методы определены для всех классов в Kotlin.
По умолчанию все классы в Kotlin имеют статус final, который блокирует возможность наследования.
Чтобы сделать класс наследуемым, его нужно пометить ключевым словом open
.
open class Base // Класс открыт для наследования
Для явного объявления суперкласса мы помещаем его имя за знаком двоеточия в оглавлении класса:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
Если у класса есть основной конструктор, базовый тип может (и должен) быть проинициализирован там же, с использованием параметров основного конструктора.
Если у класса нет основного конструктора, тогда каждый последующий дополнительный конструктор должен включать в себя инициализацию базового типа
с помощью ключевого слова super
или давать отсылку на другой конструктор, который это делает.
Примечательно, что любые дополнительные конструкторы могут ссылаться на разные конструкторы базового типа.
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
Переопределение методов класса
Kotlin требует явно указывать модификаторы и для членов, которые могут быть переопределены, и для самого переопределения.
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
Для Circle.draw()
необходим модификатор override
. В случае её отсутствия компилятор выдаст ошибку.
Если у функции типа Shape.fill()
нет модификатора open
, объявление метода с такой же сигнатурой в производном классе невозможно,
с override
или без. Модификатор open
не действует при добавлении к членам final класса (т.е. класса без модификатора open
).
Член класса, помеченный override
, является сам по себе open, т.е. он может быть переопределён в производных классах.
Если вы хотите запретить возможность переопределения такого члена, используйте final
.
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
Переопределение свойств класса
Переопределение свойств работает также, как и переопределение методов; все свойства, унаследованные от суперкласса, должны быть помечены ключевым словом override
,
а также должны иметь совместимый тип.
Каждое объявленное свойство может быть переопределено свойством с инициализацией или свойством с get
-методом.
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
Вы также можете переопределить свойство val
свойством var
, но не наоборот.
Это разрешено, поскольку свойство val
объявляет get
-метод, а при переопределении его как var
дополнительно объявляется set
-метод в производном классе.
Обратите внимание, что ключевое слово override
может быть использовано в основном конструкторе класса как часть объявления свойства.
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // Всегда имеет 4 вершины
class Polygon : Shape {
override var vertexCount: Int = 0 // Может быть установлено любое количество
}
Порядок инициализации производного класса
При создании нового экземпляра класса в первую очередь выполняется инициализация базового класса (этому шагу предшествует только оценка аргументов, передаваемых в конструктор базового класса) и, таким образом, происходит до запуска логики инициализации производного класса.
open class Base(val name: String) {
init { println("Инициализация класса Base") }
open val size: Int =
name.length.also { println("Инициализация свойства size в класса Base: $it") }
}
class Derived(
name: String,
val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Аргументы, переданные в конструктор класса Base: $it") }) {
init { println("Инициализация класса Derived") }
override val size: Int =
(super.size + lastName.length).also { println("Инициализация свойства size в классе Derived: $it") }
}
fun main() {
println("Построение класса Derived(\"hello\", \"world\")")
Derived("hello", "world")
}
Это означает, что свойства, объявленные или переопределенные в производном классе, не инициализированы к моменту вызова конструктора базового класса.
Если какое-либо из этих свойств используется в логике инициализации базового класса (прямо или косвенно через другую переопределенную open
реализацию члена класса),
это может привести к некорректному поведению или сбою во время выполнения. Поэтому при разработке базового класса следует избегать использования членов
с ключевым словом open
в конструкторах, инициализации свойств и блоков инициализации (init
).
Вызов функций и свойств суперкласса
Производный класс может вызывать реализацию функций и свойств своего суперкласса, используя ключевое слово super
.
open class Rectangle {
open fun draw() { println("Рисование прямоугольника") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Заполнение прямоугольника")
}
val fillColor: String get() = super.borderColor
}
Во внутреннем классе доступ к суперклассу внешнего класса осуществляется при помощи ключевого слова super
, за которым следует имя внешнего класса: super@Outer
.
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}
inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // Вызывает реализацию функции draw() класса Rectangle
fill()
println("Нарисованный прямоугольник заполнен ${super@FilledRectangle.borderColor} цветом") // Используется реализация get()-метода свойства borderColor в классе
}
}
}
Правила переопределения
В Kotlin правила наследования реализации определены следующим образом: если класс наследует многочисленные реализации одного и того члена от ближайших родительских классов, он должен переопределить этот член и обеспечить свою собственную реализацию (возможно, используя одну из унаследованных).
Для того чтобы отметить конкретный супертип (родительский класс), от которого мы наследуем данную реализацию,
используйте ключевое слово super
. Для задания имени родительского супертипа используются треугольные скобки, например super<Base>
.
open class Rectangle {
open fun draw() { /* ... */ }
}
interface Polygon {
fun draw() { /* ... */ } // члены интерфейса открыты ('open') по умолчанию
}
class Square() : Rectangle(), Polygon {
// Компилятор требует, чтобы функция draw() была переопределена:
override fun draw() {
super<Rectangle>.draw() // вызов Rectangle.draw()
super<Polygon>.draw() // вызов Polygon.draw()
}
}
Это нормально, наследоваться одновременно от Rectangle
и Polygon
,
но так как у каждого из них есть своя реализация функции draw()
,
мы должны переопределить draw()
в Square
и обеспечить нашу собственную реализацию этого метода для устранения получившейся неоднозначности.