Прямые ссылки на публичные уроки для быстрого старта и стабильной индексации lesson-страниц.
Наследование — один из принципов ООП. Суть: можно создать основной класс с базовыми свойствами и методами, а от него отнаследовать более специализированные подклассы, которые переиспользуют общую функциональность и добавляют свою.
Представим приложение для классификации вымышленных космических кораблей. У всех кораблей есть общие свойства: название, скорость, признак беспилотного корабля. И общие методы: переход в варп-режим и запуск диагностики. Корабли подразделяются на типы: разведчик (Scout) и индустриальный (Industrial). Без наследования базовые свойства и методы пришлось бы дублировать в каждом типе.
Наследование позволяет создать один базовый класс-родитель (суперкласс) с общими данными, а затем дочерние классы, которые наследуют его функциональность, расширяют и модифицируют её. Это исключает дублирование кода и поддерживает порядок в проекте.
Распределим классы по отдельным файлам в пакете текущего урока.
Создаём базовый класс Spaceship с общими полями и методами. Поле unmanned получает значение false по умолчанию — большинство кораблей пилотируемые.
class Spaceship( val name: String, val speed: Int, val unmanned: Boolean = false, ) { fun switchToWarpMode() { println("Переход в варп-режим") } fun runSystemDiagnostics() { println("Запущена диагностика системы корабля") } }
При инициализации экземпляра свойство unmanned не обязательно — объект хранит false по умолчанию.
fun main() { val ship1 = Spaceship("ship1", 500) ship1.runSystemDiagnostics() ship1.switchToWarpMode() }
Создадим классы Scout и Industrial. Чтобы от класса можно было наследоваться, родитель должен быть помечен ключевым словом open — это явное разрешение на расширение.
open class Spaceship( val name: String, val speed: Int, val unmanned: Boolean = false, ) {
В классе-наследнике после двоеточия указывается родительский класс. Если у родителя непустой первичный конструктор с объявленными свойствами, его необходимо вызвать. Свойства передаются без val/var, так как они уже объявлены в суперклассе. Поле unmanned не пишем — оно передастся со значением по умолчанию.
class Scout( name: String, speed: Int, ) : Spaceship(name, speed) { }
Для Industrial устанавливаем unmanned = true прямо при вызове родительского конструктора — все индустриальные корабли беспилотные.
class Industrial( name: String, speed: Int, ) : Spaceship(name, speed, unmanned = true) { }
В Kotlin можно наследоваться только от одного класса, но от множества интерфейсов (о них позже).
После создания подклассов можно создавать их экземпляры и вызывать методы суперкласса — они доступны автоматически, без повторного объявления.
fun main() { val ship1 = Spaceship("ship1", 500) ship1.runSystemDiagnostics() ship1.switchToWarpMode() println() val scout1 = Scout("scout1", 750) scout1.runSystemDiagnostics() scout1.switchToWarpMode() println(scout1.unmanned) println() val industrial1 = Scout("industrial1", 250) industrial1.runSystemDiagnostics() industrial1.switchToWarpMode() println(industrial1.unmanned) }
Если экземпляр базового класса не имеет смысла создавать — он служит только шаблоном — его объявляют абстрактным, заменяя open на abstract. Попытка создать экземпляр такого класса вызовет ошибку компиляции.
Подробнее об абстрактных классах и интерфейсах — в следующем уроке.
Расширим подклассы специализированными полями и методами. Для Scout добавим radarRange и afterburnerSpeed, а также методы handleDataFromRadar() и runAfterburner(). Для Industrial — numberOfMiners и метод launchScanningDrones(). Новые поля объявляются с val, так как они уникальны для каждого подкласса.
class Scout( name: String, speed: Int, val radarRange: Int, val afterburnerSpeed: Int, ) : Spaceship(name, speed) { fun handleDataFromRadar() { println("$name: Обработка данных с радара") } fun runAfterburner() { println("$name: Форсаж запущен") } }
class Industrial( name: String, speed: Int, val numberOfMiners: Int, ) : Spaceship(name, speed, unmanned = true) { fun launchScanningDrones() { println("$name: Сканирующие дроны запущены") } }
fun main() { val ship1 = Spaceship("ship1", 500) ship1.runSystemDiagnostics() ship1.switchToWarpMode() println() val scout1 = Scout("scout1", 750, 100, 1000) scout1.runSystemDiagnostics() scout1.switchToWarpMode() scout1.runAfterburner() scout1.handleDataFromRadar() println(scout1.unmanned) println() val industrial1 = Industrial("industrial1", 250, 8) industrial1.runSystemDiagnostics() industrial1.switchToWarpMode() industrial1.launchScanningDrones() println(industrial1.unmanned) }
Переопределение позволяет изменить поведение метода суперкласса в подклассе. Например, runSystemDiagnostics() у Industrial должен запускать диагностику дронов и майнеров вместо общей системной.
Для переопределения:
open;override.class Industrial( name: String, speed: Int, val numberOfMiners: Int, ) : Spaceship(name, speed, unmanned = true) { fun launchScanningDrones() { println("$name: Сканирующие дроны запущены") } override fun runSystemDiagnostics() { println("$name: Запущена диагностика дронов и майнеров") } }
Ключевое слово super позволяет обратиться к методам и свойствам суперкласса из переопределённого метода. Это удобно, когда нужно сначала выполнить базовую логику родителя, а затем добавить специализированную.
class Industrial( name: String, speed: Int, val numberOfMiners: Int, ) : Spaceship(name, speed, unmanned = true) { fun launchScanningDrones() { println("$name: Сканирующие дроны запущены") } override fun runSystemDiagnostics() { super.runSystemDiagnostics() println("$name: Запущена диагностика дронов и майнеров") } }
При вызове runSystemDiagnostics() для объекта Industrial сначала выполнится общая диагностика из суперкласса, затем — специализированная из подкласса.