Как работает наследование классов и интерфейсов в 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 в плане композиции поведения и предотвращения несанкционированного переопределения.