Как работает наследование классов и интерфейсов в Kotlin?
В Kotlin наследование работает по принципу одиночного наследования классов и множественного наследования интерфейсов. Это означает, что класс может наследовать только один другой класс, но может реализовывать несколько интерфейсов. Kotlin устраняет проблемы, связанные с множественным наследованием классов, сохранив при этом гибкость через интерфейсы с реализацией по умолчанию.
Наследование классов
По умолчанию все классы в Kotlin final — их нельзя наследовать. Чтобы класс можно было расширять, он должен быть помечен модификатором open.
open class Animal {
open fun speak() {
println("Some sound")
}
}
class Dog : Animal() {
override fun speak() {
println("Bark")
}
}
-
Animal — родительский класс (суперкласс), помечен open.
-
Dog — подкласс, наследует Animal.
-
Метод speak переопределяется с помощью override.
Наследование конструкторов
Подкласс обязан вызвать конструктор суперкласса:
open class Person(val name: String)
class Student(name: String, val grade: Int) : Person(name)
Если в суперклассе есть первичный конструктор с параметрами, то они обязательно должны быть переданы из конструктора подкласса.
Множественное наследование через интерфейсы
Интерфейсы объявляются с помощью ключевого слова interface, и класс может реализовывать любое количество интерфейсов.
interface Walker {
fun walk() {
println("Walking")
}
}
interface Swimmer {
fun swim() {
println("Swimming")
}
}
class Amphibian : Walker, Swimmer
-
Интерфейсы могут содержать реализацию по умолчанию.
-
Класс Amphibian реализует оба интерфейса.
Если в двух интерфейсах есть методы с одинаковыми сигнатурами и реализациями, Kotlin требует явного разрешения конфликта:
interface A {
fun greet() = println("Hello from A")
}
interface B {
fun greet() = println("Hello from B")
}
class C : A, B {
override fun greet() {
super<A>.greet()
super<B>.greet()
}
}
-
Метод greet() определяется в обоих интерфейсах.
-
В классе C необходимо явно указать, какую реализацию использовать.
Абстрактные классы и интерфейсы
Оба механизма могут определять абстрактные члены (без тела), которые нужно реализовать:
abstract class Shape {
abstract fun area(): Double
}
interface Drawable {
fun draw()
}
class Circle(val r: Double) : Shape(), Drawable {
override fun area(): Double = Math.PI \* r \* r
override fun draw() = println("Drawing a circle")
}
-
Shape — абстрактный класс с абстрактной функцией area().
-
Drawable — интерфейс с методом draw().
-
Circle реализует оба: наследует Shape и реализует Drawable.
Отличие интерфейса от абстрактного класса
Характеристика | Интерфейс | Абстрактный класс |
---|---|---|
Множественная реализация | ✅ | ❌ (только один) |
--- | --- | --- |
Состояние (свойства с backing field) | ❌ | ✅ |
--- | --- | --- |
Конструктор | ❌ | ✅ |
--- | --- | --- |
Реализация методов по умолчанию | ✅ | ✅ |
--- | --- | --- |
Интерфейсы не могут иметь состояния (например, val x = 5 с backing field), но могут иметь свойства без хранения:
interface Named {
val name: String
}
Наследование от интерфейса с реализацией и без
interface Logger {
fun log(message: String) {
println("Log: $message")
}
}
interface Printable {
fun print()
}
-
log() имеет реализацию — не обязательно переопределять.
-
print() — абстрактный метод, его нужно реализовать.
Инициализация и вызов суперкласса
Класс может вызывать методы или свойства суперкласса через super:
open class Animal {
open fun speak() = println("Generic animal sound")
}
class Dog : Animal() {
override fun speak() {
super.speak()
println("Dog bark")
}
}
Итоговая модель наследования Kotlin
-
Наследование классов — строго одиночное (open).
-
Интерфейсы позволяют множественное наследование, в т.ч. с дефолтной реализацией.
-
Конфликты методов из разных интерфейсов должны разрешаться явно.
-
Абстрактные классы дают возможность совместить абстракции и состояния.
-
Все методы и классы по умолчанию final, нужно явно делать open.
Эта модель делает Kotlin безопаснее, чем Java в плане композиции поведения и предотвращения несанкционированного переопределения.