Урок 14: ООП. Наследование в Kotlin. open/super class, override

. Наследование. open class. Переопределение override - Android [Kotlin] для начинающих

Суперкласс

Наследование считается одним из принципов ООП. Суть заключается в том, что можно создать один основной класс с базовыми свойствами и методами и отнаследовать от него более специализированные подклассы. Которые будут переиспользовать свойства и методы из основного класса, а также добавлять свои, более узконаправленные.

Представим, что мы проектируем гипотетическое приложение для классификации вымышленных космических кораблей. Нам нужно описать сущности в виде классов.

У абсолютно всех кораблей пусть будут такие свойства, как название, скорость, беспилотный корабль или нет. Есть методы перейти в варп режим, запустить диагностику системы. Назовем это базовыми свойствами и методами. Корабли могут подразделяться на типы, пусть они будут такими: разведчик и индустриальный.

У каждого типа есть свои дополнительные свойства и действия. И если пойдя классическим путем, мы бы создали дополнительные классы с этими типами. Внутри пришлось бы дублировать базовые свойства и методы для каждого типа.

Классы наследники

Прелесть наследования в том, что сначала мы можем создать один базовый класс-родитель с общими данными. Еще его называют суперклассом. Затем создать несколько дочерних классов, которые будут наследовать функциональность базового класса, а также расширять и модифицировать ее. Это правильный подход, который позволяет избегать дублирования кода и соблюдать порядок в проекте.

К примерам. Классы можно создавать в одном файле (несколько штук), но так как мы преследуем условия обучения, приближенные к околореальным, распределим классы по отдельным документам. Для этого сначала создадим пакет для текущего урока. Также переименуем предыдущие пакеты для их красивого отображения в алфавитном порядке.

Создание базового класса — родителя

Теперь приступаем к написанию классов. Первым создаем базовый класс Spaceship. Запишем поля, которые мы определили как общие для всех сущностей. Название, скорость, беcпилотник. Поле 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. В свойствах пока укажем только название и скорость. Созданные классы с названиями типов космических кораблей уже можно использовать для создания объектов, но мы договорились сделать их дочерними от базового класса. Чтобы не дублировать поля и методы по несколько раз. Теперь свяжем их между собой.

Первое, что нужно сделать. В Kotlin по умолчанию нельзя наследоваться от чего угодно. Чтобы класс можно было расширять, необходимо указать это явно. Для этого нужно перед ключевым словом class у родителя написать ключевое слово open. Таким образом мы говорим, что мы в курсе, что у этого класса будут наследники, которые получат доступ к данным внутри для их использования и расширения.

open class Spaceship(
   val name: String,
   val speed: Int,
   val unmanned: Boolean = false,
) {

Окей, второй шаг – это указать родителя в классе наследнике. Для этого после названия класса и круглых скобок основного конструктора (если он есть) через двоеточие указывается название класса от которого будем наследоваться. В Kotlin наследоваться можно только от одного класса и от множества интерфейсов, но о них позже.

Шаг третий, опциональный. Тут касаемся конструкторов, поэтому постараюсь рассказывать чуть подробнее. Если в базовом классе уже есть не пустой первичный конструктор, который запускается при создании объекта (напоминаю, под капотом он занимается выделением памяти, присваивает значения полям класса и так далее). И если в базовом классе присутствуют объявленные свойства. Как в данном случае это val name, speed и так далее. Этот конструктор необходимо вызвать в наследуемом классе, иначе ничего не заработает. Таким образом мы передаем информацию об объявленных полях, которые будут использованы при создании подкласса. Так как переменные уже объявлены в суперклассе (при помощи val) и хранятся там, здесь не используется повторное объявление.

Мы говорим, что конструктор подкласса Scout будет только принимать данные name и speed. Кроме того надо еще перечислить эти свойства в скобках базового класса, которые будут переданы. Вновь обращу внимание, что поле unmanned мы не пишем как в классе, так и в конструкторе, но держим в уме, что с наследованием оно также передастся создаваемому объекту со значением по умолчанию false.

class Scout(
   name: String,
   speed: Int,
) : Spaceship(name, speed) {


}

Сделаем то же самое для второго подкласса Industrial. Но пусть наши индустриальные корабли будут беспилотными, то есть unmanned для них должно быть всегда true. Для этого нужно изменить один штрих в передаваемом конструкторе. Добавляем свойство unmanned и присваиваем ему true. Так мы взяли свойство из класса-родителя и применили его с новым значением для класса-наследника.

class Industrial(
   name: String,
   speed: Int,
) : Spaceship(name, speed, unmanned = true) {

}

Наследование методов суперкласса

Окей. Наследование реализовано, теперь можно создавать экземпляры подклассов и использовать методы суперкласса. Сделаем это для обоих типов объектов коралей. Заполняем название, скорость и вызываем методы. Также для демонстрации распечатаем информацию об отсутствии пилотов для каждого класса. Для Скаута будет false, для Индустриального true.

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() – обработать данные радара и запустить форcажные двигатели.

Для другого типа корабля 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(). Метод сообщает, что запущена диагностика системы корабля. Но, например, для промышленного судна, необходимо запустить только диагностику дронов и майнеров. Не будем создавать для этого новую функцию, а модифицируем (переопределим) существующую.

Чтобы переопределить функцию, нужно сделать следующее. Объявить метод в дочернем классе, обязательно с таким же названием. В теле функции прописываем новые действия для этого специализированного класса. Помните, что по умолчанию от классов нельзя наследоваться? От методов тоже. Поэтому еще добавим два штриха. У метода, который хотим переопределить (в базовом класса) прописываем уже знакомое ключевое слово open. И второе – у метода, который переопределяем в подклассе добавляем новое ключевое слово override перед fun.

class Industrial(
   name: String,
   speed: Int,
   val numberOfMiners: Int,
) : Spaceship(name, speed, unmanned = true) {

   fun launchScanningDrones() {
		println("$name: Сканирующие дроны запущены")
   }

   override fun runSystemDiagnostics() {
		println("$name: Запущена диагностика дронов и майнеров"
			)
   }

}

Теперь можно запустить программу из рабочего файла. Обратите внимание на результат команды runSystemDiagnostics() у объекта industrial1 – теперь система запустит диагностику конкретно для дронов и майнеров.

Обращение к методам класса-родителя

Окей. И рассмотрим еще одно ключевое слово – super. Одна из его возможностей – это обращение к методам и свойствам суперкласса. То есть, если по каким то причинам нужна реализация из класса-родителя, перед вызовом метода или перед обращением к переменной прописываем super с точкой. Для небольшой демонстрации сделаем это тут же в классе Industrial. Пусть при вызове переопределенного метода runSystemDiagnostics() внутри него сначала вызывается родительский метод runSystemDiagnostics().

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: Запущена диагностика дронов и майнеров"
      )
   }

}

Отлично, запустим. Теперь сначала запустится общая диагностика, а затем диагностика переферийных систем в специализированном классе.

Для тех, кто собрался стать Android-разработчиком

Пошаговаясхема
Пошаговая
схема

Описание процесса обучения от основ Kotlin до Android-разработчика

Бесплатныеуроки
Бесплатные
уроки

Авторский бесплатный курс по основам языка программирования Kotlin

Обучающийбот
Обучающий
бот

Тренажер и самоучитель по Котлин – бесплатные тесты и практика

Поделиться уроком

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *