Освой Kotlin играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Наследование - важная тема в программировании. Наследование предотвращает дублирование кода. Общий код размещается в одном классе, а затем более конкретные специализированные классы наследуют этот класс. Если же код потребуется обновить, достаточно внести изменения в одном месте и эти изменения отразятся во всех классах, наследующих это поведение. Класс, содержащий общий код, называется суперклассом, а классы, наследующие от него, называются подклассами.
В Java классы открыты по умолчанию, а для того, чтобы запретить наследование от классов или переопределение их переменных и методов экземпляров, используется ключевое слово final. В Kotlin используется противоположный подход - можно наследовать от суперклассов и переопределять их свойства и функции только в том случае, если они снабжены префиксом open.
У нас уже был класс Cat, но теперь мы всё поменяем, чтобы показать принцип наследования.
Мы создадим суперкласс Animal, который будет содержать некоторые общие свойства и функции, которые будут наследоваться подклассами. В суперкласс добавим свойства image (имя файла с изображением животного), food (пища, которой питается животное, habitat (среда обитания), hunger (уровень голода животного), а также четыре функции: makeNoise() (животное издаёт свой характерный звук), eat() (что делает животное при обнаружении своего предпочтительного источника пищи), roam() (что делает животное, когда не ест и не спит), sleep() (животное засыпает).
Все животные выглядят по-разному, живут в разных местах и имеют разные гастрономические предпочтения. Это означает, что мы можем переопределить свойства, чтобы они по-разному инициализировались для каждого типа животного. Например, свойство habitat у класса Mouse будет инициализироваться значением «ground», а свойство food у Lion — значением «meat». Тоже самое с функциями. Каждый подкласс животного наследует функции от класса Animal. Какие же из этих функций следует переопределять? Львы рычат, волки воют, мыши пищат. Все животные издают разные звуки, это означает, что функция makeNoise() должна переопределяться в каждом подклассе животного.
Кроме того, можно сгруппировать некоторые виды классов, выстраивая иерархию. Например, волк и лисица относятся к семейству собачьих, и поэтому могут обладать общим поведением, которое можно абстрагировать в класс Canine. С другой стороны, лев, гепард и рысь относятся к семейству кошачьих, поэтому может быть полезно определить новый класс Feline.
Начнём с суперкласса Animal. Чтобы класс можно было использовать в качестве суперкласса, необходимо явно сообщить об этом компилятору. Для этого перед именем класса — и любым свойством и функцией, которые вы собираетесь переопределять, — ставится ключевое слово open. Тем самым вы сообщаете компилятору, что класс проектировался как суперкласс, и согласны с тем, что его свойства и функции, объявленные как открытые, будут переопределяться.
open class Animal {
open val image = ""
open val food = ""
open val habitat = ""
var hunger = 10
open fun makeNoise() {
println("Животное издаёт звук")
}
open fun eat() {
println("Животное ест")
}
open fun roam() {
println("Животное скитается")
}
fun sleep() {
println("Животное спит")
}
}
Мы объявили открытым сам класс, три свойства (кроме hunger) и три метода (кроме sleep).
Чтобы класс наследовал от другого класса, добавьте в заголовок класса двоеточие (:), за которым следует имя суперкласса. Класс становится подклассом и получает все свойства и функции того класса, от которого наследуется. В нашем случае класс Mouse должен наследовать от суперкласса Animal. Чтобы переопределить свойство, унаследованное от суперкласса, добавьте свойство в подкласс и поставьте перед ним ключевое слово override.
Переопределяем свойства image, food и habitat, унаследованные классом Mouse от суперкласса Animal, чтобы они инициализировались значениями, специфическими для Mouse.
class Mouse : Animal() {
override val image = "mouse.jpg"
override val food = "cheese"
override val habitat = "ground
override fun makeNoise() {
println("Пищит!")
}
override fun eat() {
println("Мышка ест $food")
}
}
Animal() после двоеточия (:) — вызов конструктора Animal. Он обеспечивает выполнение всего кода инициализации Animal — например, присваивание значений свойствам. Вызов конструктора суперкласса обязателен: если у суперкласса есть первичный конструктор, вы должны вызвать его в заголовке подкласса, иначе код не будет компилироваться. И даже если вы явно не добавили конструктор в свой суперкласс, помните, что компилятор автоматически создаёт пустой конструктор при компиляции кода.
Если конструктор суперкласса получает параметры, значения этих параметров должны передаваться при вызове конструктора.
При определении свойства в суперклассе с ключевым словом val вы обязаны переопределить его в подклассе, если хотите присвоить ему другое значение.
Если свойство суперкласса определяется с ключевым словом var, то переопределять его для присваивания нового значения не обязательно, так как переменные var могут повторно использоваться для других значений. Можно присвоить ему новое значение в блоке инициализации подкласса, как в следующем примере:
open class Animal {
var image = "" // var инициализируется пустой строкой
...
}
class Hippo : Animal() {
init {
// переопределять не надо, просто присвоим новое значение
image = "hippo.jpg"
}
...
}
Если свойство в суперклассе было определено с ключевым словом val, в подклассе оно может быть переопределено как свойство var. Для этого просто переопределите свойство и объявите его с ключевым словом var. Учтите, что замена работает только в одном направлении: при попытке переопределить свойство var с ключевым словом val компилятор откажется компилировать ваш код.
Функции переопределяются по аналогии со свойствами - добавляется в подкласс с префиксом override. При переопределении функций необходимо соблюдать два правила:
Ранее говорилось, чтобы переопределить функцию или свойство, необходимо объявить их открытыми в суперклассе. При этом функция или свойство остаются открытыми в каждом из подклассов, даже если они были переопределены, так что вам не придётся объявлять их открытыми ниже по дереву. Если вы хотите запретить возможность переопределения функции или свойства ниже в иерархии классов, снабдите их префиксом final.
Добавим новый класс, которые послужит промежуточным классом для некоторых животных - класс Canine (класс собачьих) и класс Wolf (волк), который будет наследоваться уже от него.
open class Canine : Animal() {
override fun roam() {
println("Животное класса волчьих скитается")
}
}
class Wolf : Canine() {
override val image = "wolf.jpg"
override val food = "мясо"
override val habitat = "лес"
override fun makeNoise() {
println("Воет! У-у-у-у!")
}
override fun eat() {
println("Волк ест $food")
}
}
Хотя в классе Wolf мы видим только две функции, на самом деле класс содержит четыре функции. И мы можем вызвать любую из них.
val wolf = Wolf()
wolf.makeNoise() // Wolf
wolf.eat() // Wolf
wolf.roam() // Canine
wolf.sleep() // Animal
Функция sleep наследуется от Animal, roam() от Canine (которая в свою очередь наследуется от Animal) и две функции переопределены в самом классе.
При вызове функции по ссылке на объект будет вызвана самая конкретная версия функции для этого типа объекта: то есть та, которая находится ниже всего в дереве наследования. Система сначала ищет функцию в классе Wolf. Если функция будет найдена в этом классе, то она выполняется. Но если функция не определена в классе Wolf, система идёт вверх по дереву наследования до класса Canine. Если функция определена в этом классе, система выполняет ее, а если нет, то поиск вверх по дереву продолжается. Система продолжает идти вверх по иерархии классов, пока не найдёт совпадение для функции.
Вот почему при вызове функции makeNoise() мы получим строку "Воет! У-у-у-у!", а не "Животное издаёт звук".
Наследование обеспечивает наличие функций и свойств во всех подклассах, определённых в суперклассе.
Когда вы определяете супертип для группы классов, вы можете использовать любой подкласс вместо суперкласса, от которого он наследуется. Можно написать следующее:
// Код создаёт объект Wolf
// и присваивает его переменной типа Animal
val animal: Animal = Wolf()
При вызове функции eat() будет вызвана версия класса Wolf, так как система знает, что по ссылке хранится объект Wolf.
Это даёт возможность создать массив из Animal на основе разных типов животных, но при этом каждый элемент массива будет выполнять свои функции.
// Можно было добавить и других животных
val animals = arrayOf(Mouse(), Wolf())
for(item in animals) {
item.roam()
item.eat()
}
Похожим образом поведёт себя функция, объявленная в другом классе, которая будет использовать в параметре Animal.
// Айболит
class Vet {
// делаем укол
fun giveShot(animal: Animal) {
// животному не нравится, он "говорит"
animal.makeNoise();
}
}
val vet = Vet()
val wolf = Wolf()
vet.giveShot(Wolf())
Айболит может сделать любому животному укол, так как волк и мышка являются разновидностями Animal.
Возможность использования объектов одного типа в месте, в котором явно обозначен другой тип, называется полиморфизмом. По сути, речь идёт о возможности предоставлять разные реализации функций, которые были унаследованы от другого класса.
Стоит заметить, что мы можем создать экземпляры класса Wolf и Mouse, но не должны иметь возможность создать экземпляр класса Animal, так такого абстрактного животного не существует. Для этой цели существуют абстрактные классы.