Абстрактные классы

В программировании абстрактные классы – это такие классы, от которых нельзя создавать объекты. Они используются лишь в качестве суперкласса, в который вынесено все общее из дочерних классов.

Например, в программе есть различные классы юнитов – пехотинцы, всадники, герои. Их общие свойства и методы можно вынести в один общий класс "юнит". Поскольку в программе не может быть просто юнита, такой класс имеет смысл сделать абстрактным.

В Kotlin абстрактные классы имеют модификатор abstract вместо open, то есть абстрактные классы всегда открыты для наследования, иначе в них не было бы смысла. Сделаем наш класс NumInc абстрактным:

abstract class NumInc(n: Int, s: Int) {
    var number = n
    var step = s
    fun inc() {number += step}
    fun dec() {number -= step}
}

Теперь мы не сможем создать объект от NumInc, хотя переменную такого типа создать можем. Ее можно связать с объектами дочерних от NumInc неабстрактных классов.

val a = NumMult(3,4)
val b: NumInc = NumDouble(2, 2)

Здесь переменная a будет иметь тип дочернего класса, b хоть и связана с объектом дочернего класса, будет иметь тип абстрактного родительского класса. Следующее выражение недопустимо:

val c = NumInc(1, 4)

Оно содержит ошибку, так как мы пытаемся создать объект абстрактного класса.

Если класс NumInc определен так, как представлено выше, в дочерних классах мы не можем переопределять его свойства и методы, только добавлять новые. Чтобы иметь возможность переопределения, их по-прежнему надо делать открытыми.

abstract class NumInc(n: Int, s: Int) {
    var number = n
    var step = s
    open fun inc() {number += step}
    open fun dec() {number -= step}
}
class NumDouble(n: Int, s: Int): NumInc(n, s) {
    override fun inc() {
        super.inc()
        super.inc()
    }
}

Однако кроме такого варианта, методы можно объявлять абстрактными. В этом случае они не должны содержать тела, то есть должны быть без реализации. В свою очередь дочерний класс обязан реализовать эти методы. То есть абстрактный метод родителя в дочернем классе всегда должен быть переопределен.

abstract class NumInc(n: Int, s: Int) {
    var number = n
    var step = s
 
    abstract fun inc()
    abstract fun dec()
}
class NumDouble(n: Int, s: Int): NumInc(n, s) {
 
    override fun inc() {
        number += 2 * step
    }
 
    override fun dec() {
        number -= 2 * step
    }
}

В другом дочернем классе реализация функций inc() и dec() может быть совсем другой.

class NumMult(n: Int, s: Int, q: Int): NumInc(n, s) {
    val coefficient = q
 
    override fun inc() {
        number += step * coefficient
    }
 
    override fun dec() {
        number -= step * coefficient
    }
}

Одинаковыми являются лишь параметры (в данном случае их нет) и тип возвращаемого значения. Все это определено в родительском абстрактном классе.

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

В чем выгода от этого? Мы могли бы вообще не описывать эти абстрактные методы в абстрактном классе и реализовывать только в дочерних при необходимости. Выгода – в одинаковой сигнатуре дочерних классов. Гарантируется, что у всех них есть что-то общее – одинаковые методы, что позволит выполнять групповую обработку объектов, созданных от разных классов.

В IntelliJ IDEA, когда вы создаете класс и хотите (или это требуется в случае абстрактных) переопределить свойства и методы родительского класса, можно нажать Ctrl + O, появится окно, где следует выбрать то, что вам требуется. IDEA сама сформирует заголовок.

Окно переопределения членов класса в IntelliJ IDEA

Абстрактными могут быть не только функции-члены, но и свойства. Однако в этом случае их нельзя инициировать значениями. Пример полностью абстрактного класса:

abstract class NumInc {
    abstract var number: Int
    abstract var step: Int
    abstract fun inc()
    abstract fun dec()
}

Обратите внимание, что у класса нет собственного конструктора. Пример его дочернего класса:

class NumMult(n: Int, s: Int, q: Int): NumInc() {
    override var number = n
    override var step = s
    val coefficient = q
 
    override fun inc() {
        number += step * coefficient
    }
    override fun dec() {
        number -= step * coefficient
    }
}

Практическая работа:

  1. Проверьте, можно ли в неабстрактном классе определить абстрактный метод. Объясните результат.

  2. Проверьте, может ли абстрактный класс сам быть наследником другого класса. Обязан ли последний быть также абстрактным. Результат объясните.

PDF-версия курса с ответами к практическим работам

Приложение для Android "Kotlin. Курс"