Глиф Учебники

Урок 10. Наследование, open и интерфейсы

Курс «Kotlin» · урок 10 из 10 · 3 минуты чтения

Классы часто похожи друг на друга: у собаки и кошки есть имя, обе издают звуки, но разные. Чтобы не писать одно и то же дважды, общее выносят в родительский класс, а наследники берут его и добавляют своё. Это и есть наследование.

open и override

В Kotlin классы по умолчанию закрыты для наследования: от обычного class унаследоваться нельзя. Родительский класс помечают словом open, и так же — каждый метод, который наследникам разрешено переделать:

Kotlin
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 ...

Как это устроено:

Список animals имеет тип List<Animal>: собака и кошка — тоже животные. Один и тот же вызов a.describe() работает по-разному в зависимости от того, какой объект на самом деле лежит в переменной. Это называется полиморфизмом.

open class Animal(val name: String) open fun sound() = "..." fun describe() — общий для всех внутри describe() вызывает sound() наследуют Dog — наследник Animal override fun sound() = "Woof" describe() — достался от Animal Rex says Woof Cat — наследник Animal override fun sound() = "Meow" describe() — достался от Animal Murka says Meow
Dog и Cat наследуют open class Animal: describe() достаётся им готовым, а sound() каждый переопределяет через override, поэтому вывод разный.

Если забыть open у класса или метода, компилятор сообщит, что тип закрыт (final) и наследовать его нельзя, или что метод нельзя переопределить. Так задумано: класс, который не готовили к наследованию, случайно не сломают.

super: вызвать версию родителя

Наследник может не заменить метод родителя целиком, а дополнить его. Версия родителя вызывается через super:

Kotlin
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: метод без тела, а объект такого класса создать нельзя — только объекты наследников. Наследник обязан написать все абстрактные методы:

Kotlin
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 800

open у абстрактного класса и метода не пишут — они открыты сами собой.

Интерфейсы

Интерфейс — набор методов, которые класс обещает уметь. Он похож на абстрактный класс, но у интерфейса нет конструктора, а один класс может реализовать несколько интерфейсов (родительский класс — только один). В интерфейсе можно дать методу готовое тело — реализацию по умолчанию:

Kotlin
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
interface Shape fun area(): Double без тела — каждый класс пишет сам fun describe() = "Area: ${area()}" готов для всех реализуют Rect — реализует Shape override fun area() = w * h describe() → Area: 7.0 Circle — реализует Shape override fun area() = 3.14 * r * r describe() → Area: 3.14
Интерфейс Shape: метод area() без тела каждый класс пишет сам, а describe() с готовым телом достаётся Rect и Circle и вызывает их area().
Animal класс Swimmer интерфейс Flyer интерфейс родитель — один класс интерфейсов — сколько угодно class Duck : Animal("Duck"), Swimmer, Flyer у класса — скобки конструктора, у интерфейсов — нет
Класс Duck наследует один класс Animal и реализует сколько угодно интерфейсов — здесь Swimmer и Flyer, через запятую.

is и умное приведение

is проверяет, к какому классу относится объект. После проверки компилятор разрешает пользоваться свойствами этого класса без дополнительных преобразований — то же умное приведение, что и при проверке на null:

Kotlin
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.

Коротко

Проверьте себя

1. Почему не собирается class Dog : Animal(), если Animal объявлен как class Animal?
2. Что выведет программа?
Kotlin
open class A {
    open fun name() = "A"
}

class B : A() {
    override fun name() = "B" + super.name()
}

fun main() {
    println(B().name())
}
3. Сколько интерфейсов может реализовать один класс?
4. Можно ли создать объект абстрактного класса: Employee("Olga")?