Урок 10. Наследование, open и интерфейсы
Классы часто похожи друг на друга: у собаки и кошки есть имя, обе издают звуки, но разные. Чтобы не писать одно и то же дважды, общее выносят в родительский класс, а наследники берут его и добавляют своё. Это и есть наследование.
open и override
В Kotlin классы по умолчанию закрыты для наследования: от обычного class унаследоваться нельзя. Родительский класс помечают словом open, и так же — каждый метод, который наследникам разрешено переделать:
open class Animal(val name: String) {
open fun sound() = "..."
fun describe() = "$name says ${sound()}"
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof"
}
class Cat(name: String) : Animal(name) {
override fun sound() = "Meow"
}
fun main() {
val animals = listOf(Dog("Rex"), Cat("Murka"), Animal("Somebody"))
for (a in animals) {
println(a.describe())
}
}Rex says Woof
Murka says Meow
Somebody says ...Как это устроено:
class Dog(name: String) : Animal(name)—DogнаследуетAnimal. После двоеточия — родитель и его конструктор: имя, полученноеDog, передаётся дальше вAnimal;override fun sound()— своя версия метода родителя. Словоoverrideобязательно: без него программа не соберётся;describeобъявлен только вAnimal, но достаётся всем наследникам. Внутри он вызываетsound()— и для собаки срабатывает версия собаки.
Список animals имеет тип List<Animal>: собака и кошка — тоже животные. Один и тот же вызов a.describe() работает по-разному в зависимости от того, какой объект на самом деле лежит в переменной. Это называется полиморфизмом.
Если забыть open у класса или метода, компилятор сообщит, что тип закрыт (final) и наследовать его нельзя, или что метод нельзя переопределить. Так задумано: класс, который не готовили к наследованию, случайно не сломают.
super: вызвать версию родителя
Наследник может не заменить метод родителя целиком, а дополнить его. Версия родителя вызывается через super:
open class Greeter {
open fun hello() = "Hello"
}
class PoliteGreeter : Greeter() {
override fun hello() = super.hello() + ", nice to meet you"
}
fun main() {
println(Greeter().hello())
println(PoliteGreeter().hello())
}Hello
Hello, nice to meet youЕсли у родителя нет параметров в конструкторе, после его имени всё равно ставят пустые скобки: : Greeter().
Абстрактные классы
Бывает, что у родителя нет осмысленной своей версии метода — «звук животного вообще» не существует. Тогда класс и метод объявляют abstract: метод без тела, а объект такого класса создать нельзя — только объекты наследников. Наследник обязан написать все абстрактные методы:
abstract class Employee(val name: String) {
abstract fun salary(): Int
fun report() = "$name earns ${salary()}"
}
class Manager(name: String) : Employee(name) {
override fun salary() = 3000
}
class Intern(name: String) : Employee(name) {
override fun salary() = 800
}
fun main() {
println(Manager("Olga").report())
println(Intern("Pavel").report())
}Olga earns 3000
Pavel earns 800open у абстрактного класса и метода не пишут — они открыты сами собой.
Интерфейсы
Интерфейс — набор методов, которые класс обещает уметь. Он похож на абстрактный класс, но у интерфейса нет конструктора, а один класс может реализовать несколько интерфейсов (родительский класс — только один). В интерфейсе можно дать методу готовое тело — реализацию по умолчанию:
interface Shape {
fun area(): Double
fun describe() = "Area: ${area()}"
}
class Rect(val w: Double, val h: Double) : Shape {
override fun area() = w * h
}
class Circle(val r: Double) : Shape {
override fun area() = 3.14 * r * r
}
fun main() {
val shapes = listOf(Rect(2.0, 3.5), Circle(1.0))
for (s in shapes) {
println(s.describe())
}
}Area: 7.0
Area: 3.14: Shapeпосле конструктора — класс реализует интерфейс. Скобок после имени интерфейса нет: конструктора у него нет;area()без тела класс обязан написать сам, аdescribe()достаётся готовым;- несколько интерфейсов пишут через запятую:
class Duck : Animal("Duck"), Swimmer, Flyer.
is и умное приведение
is проверяет, к какому классу относится объект. После проверки компилятор разрешает пользоваться свойствами этого класса без дополнительных преобразований — то же умное приведение, что и при проверке на null:
interface Shape
class Rect(val w: Int, val h: Int) : Shape
class Circle(val r: Int) : Shape
fun info(s: Shape) = when (s) {
is Rect -> "rectangle ${s.w}x${s.h}"
is Circle -> "circle r=${s.r}"
else -> "unknown shape"
}
fun main() {
println(info(Rect(2, 3)))
println(info(Circle(5)))
}rectangle 2x3
circle r=5Внутри ветки is Rect переменная s уже считается Rect, и s.w доступно. Явное приведение типа пишется через as: s as Rect. Если объект другого класса, as роняет программу с ошибкой ClassCastException, а безопасный вариант as? даёт null.
Коротко
- Классы и методы закрыты по умолчанию; для наследования —
open, для своей версии метода —override. - Наследование:
class Dog(name: String) : Animal(name); версия родителя —super.метод(). abstract— класс без своих объектов и методы без тела, которые обязан написать наследник.- Интерфейс:
interface Shape { fun area(): Double }; класс может реализовать несколько интерфейсов. isпроверяет тип и включает умное приведение;as?— безопасное приведение.