Наследование, интерфейсы и абстракция
В 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.