Наследование, интерфейсы и абстракция

В Kotlin классы по умолчанию финальные: чтобы наследоваться, класс и его методы нужно явно открыть словом open, а интерфейсы описывают контракт поведения.
Суть: продуманная иерархия через наследование и интерфейсы помогает переиспользовать код, но Kotlin намеренно делает наследование явным выбором, а не умолчанием.

Важное отличие от Java: в Kotlin все классы по умолчанию final — от них нельзя наследоваться. Чтобы разрешить наследование, класс помечают словом open. То же касается методов и свойств: переопределять можно только open-члены, а переопределение помечают словом override. Это сознательное решение: наследование — мощный, но опасный инструмент, и язык требует осознанного согласия на него.

open class Animal(val name: String) {
    open fun sound(): String = "..."
}

class Dog(name: String) : Animal(name) {
    override fun sound(): String = "Гав"
}

class Cat(name: String) : Animal(name) {
    override fun sound(): String = "Мяу"
}

val animals: List<Animal> = listOf(Dog("Рекс"), Cat("Барсик"))
animals.forEach { println("${it.name}: ${it.sound()}") }

Абстрактные классы и интерфейсы

Абстрактный класс (abstract) нельзя инстанцировать; он задаёт частичную реализацию и абстрактные члены, которые наследник обязан реализовать. Интерфейс описывает контракт: набор методов и свойств, которые класс обязуется предоставить. Класс может реализовать много интерфейсов, но наследовать только один класс.

interface Clickable {
    fun onClick()
    fun onLongClick() { println("долгое нажатие") } // реализация по умолчанию
}

interface Focusable {
    fun onFocus()
}

class Button : Clickable, Focusable {  // несколько интерфейсов
    override fun onClick() = println("клик")
    override fun onFocus() = println("фокус")
}

Как работает под капотом

При вызове it.sound() для объекта типа Animal JVM в момент исполнения определяет реальный класс (Dog или Cat) и вызывает его версию метода. Это полиморфизм через позднее связывание. Интерфейсы с реализацией по умолчанию работают похоже: если класс не переопределил метод, используется реализация из интерфейса. Когда два интерфейса дают конфликтующие методы по умолчанию, компилятор требует явно разрешить конфликт.

  Полиморфизм

  Animal (open)
     |  sound()
   +-+-----+
   v       v
  Dog     Cat
  Гав     Мяу

  Список из Animal --> каждый отвечает по-своему

Частые ошибки

Забывать open. Попытка наследоваться от обычного класса даёт ошибку: классы финальны по умолчанию.

Строить глубокие иерархии наследования. Многоуровневое наследование хрупко; часто лучше композиция или интерфейсы.

Путать абстрактный класс и интерфейс. Класс наследуется один, интерфейсов — много; выбирайте интерфейс, когда нужна множественная реализация контракта.

Best practices

  • Предпочитайте композицию и интерфейсы глубокому наследованию.
  • Открывайте для наследования только то, что действительно проектировалось как расширяемое.
  • Используйте интерфейсы для описания поведения, которое реализуют разные классы.
  • Реализации по умолчанию в интерфейсах применяйте умеренно, чтобы не запутать иерархию.

Полиморфизм удобно увидеть на Python: список разных объектов, каждый отвечает по-своему. Запустите врезку.

# Аналог полиморфизма из Kotlin
class Animal:
    def __init__(self, name): self.name = name
    def sound(self): return '...'

class Dog(Animal):
    def sound(self): return 'Гав'

class Cat(Animal):
    def sound(self): return 'Мяу'

animals = [Dog('Рекс'), Cat('Барсик')]
for a in animals:
    print(a.name + ':', a.sound())

Попробуй сам ▶ — добавьте новый класс животного со своим звуком. В Kotlin он наследовался бы от open-класса Animal и переопределял sound().

Закрепим главное

Главная мысль урока — наследование в Kotlin осознанно сделано неудобным по умолчанию. Финальность классов заставляет вас каждый раз спрашивать: действительно ли здесь нужно наследование, или достаточно интерфейса либо композиции? Этот вопрос полезен сам по себе. Глубокие иерархии хрупки: изменение в базовом классе незаметно ломает наследников. Поэтому опытные разработчики предпочитают плоские структуры, где поведение собирается из интерфейсов, а не наследуется по цепочке.

Второй ориентир — интерфейс как контракт, а не как реализация. Интерфейс отвечает на вопрос «что объект умеет», не диктуя «как именно». Это развязывает руки: одно поведение (например, кликабельность) можно подмешать в любой класс, и таких контрактов класс может реализовать сколько угодно. В Android этот подход встречается постоянно — от слушателей событий до абстракций над источниками данных, которые мы будем прятать за интерфейсом репозитория.

Итог: Kotlin делает наследование осознанным выбором через open и override, а интерфейсы позволяют описывать поведение и реализовывать его в нескольких классах. На этом завершается фундамент языка — дальше мы переходим к самому Android.

Проверьте себя
1. Почему в Kotlin для наследования нужно слово open?
Aopen ускоряет компиляцию
BКлассы по умолчанию финальные, и open явно разрешает наследование
Copen делает класс абстрактным
DБез open класс становится интерфейсом
2. Чем интерфейс отличается от класса в плане наследования?
AИнтерфейс нельзя реализовать
BКласс можно наследовать только один, а интерфейсов реализовать сколько угодно
CИнтерфейсы работают только с числами
DИнтерфейс обязательно содержит конструктор