Наследование в объектно-ориентированном программировании

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

  • общую функциональность нескольких классов выносить в один общий суперкласс,

  • совместно обрабатывать объекты, созданные от разных, но родственных, классов.

Вспомним наш класс:

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

Допустим, в программе должны быть объекты, поле number которых можно только увеличивать и уменьшать на величину шага. Также в программе нужны объекты, у которых number может изменяться не только добавлением/вычитанием шага, но также умножением на шаг. Конечно, мы можем написать еще один класс:

class NumMult(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    fun inc() {number += step}
    fun dec() {number -= step}    
    fun mult() {number *= step}
}

Однако он во многом повторяет предыдущий класс, поэтому имеет смысл сделать его дочерним по отношению к NumInt, который выступит в роли родительского. В ООП дочерний класс наследует свойства и метода родительского. Таким образом, в NumMult нам придется описывать только дополнительную функциональность. Другими словами, дочерний наследует особенности родительского, а также расширяет, дополняет их.

Чтобы класс мог быть родительским перед его объявлением должно стоять ключевое слово open.

open class NumInc(n: Int, gap: Int) {
...

В свою очередь класс-наследник должен в своем заголовке иметь запись о родительском классе. В нашем случае определение класса NumMult будет выглядеть так:

class NumMult(num: Int, coef: Int): NumInc(num, coef) {
    fun mult() {number *= step}
}

В заголовке после параметров первичного конструктора (если он есть) ставится двоеточие, после которого идет имя родительского класса. Поскольку конструктор родительского класса предусматривает два параметра, мы должны их туда передать.

Наследование в Kotlin - пример родительского и дочернего классов

После этого объекты NumMult будут обладать теми же свойствами и методами, что и объекты NumInc. У них тоже появятся свойства number и step, методы inc() и dec(). Однако помимо этого у них есть метод mult(), которого нет у объектов родительского класса.

Наследование может быть сложнее. Дочерний класс может стать родительским по отношению к другому дочернему. Для этого перед его объявлением также должно стоять слово open.

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

Давайте усложним наш пример, введя в дочерний класс третье свойство.

class NumMult(num: Int, gap: Int, coef: Int): NumInc(num, gap) {
    var coefficient = coef
 
    fun mult() {number *= coefficient}
}

Теперь дочерний класс обладает не только дополнительным методом, но и дополнительным полем. Конструктору родительского мы по-прежнему передаем два аргумента. Больше он и не принимает.

При создании объекта от класса NumMult надо передавать три аргумента:

val b = NumMult(1, 3, 2)

Первые два будут присвоены полям number и step и использоваться в функциях inc() и dec(). Третий будет присвоен свойству coefficient и использоваться только в методе mult().

Теперь представим, что класс NumMult имеет два конструктора, а у NumInc он по прежнему один. В это случае вторичный конструктор NumMult должен делегировать к первичному своего же класса, а уже тот будет обращаться к конструктору родительского класса.

class NumMult(num: Int, gap: Int, coef: Int): NumInc(num, gap) {
    var coefficient = coef
 
    constructor() : this(0, 1, 2)
 
    fun mult() {number *= coefficient}
}

Пример создания объекта через вторичный конструктор:

val c = NumMult()

Если у дочернего класса есть первичный конструктор, то все вторичные должны делегировать к нему. И только через него – к конструктору родительского класса. Однако если первичного конструктора нет, вторичные должны напрямую вызывать конструкторы родительского класса через ключевое слово super. Пример с двумя конструкторами как в основном, так и в дочернем классе при том, что в дочернем нет первичного:

open class NumInc(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    constructor(): this(0, 1)
 
    fun inc() {number += step}
    fun dec() {number -= step}
}
class NumMult: NumInc {
    var coefficient = 2
 
    constructor(num: Int, gap: Int, coef: Int): super(num, gap) {
        coefficient = coef
    }
 
    constructor(): super()
 
    fun mult() {number *= coefficient}
}

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

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

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

Однако, поскольку переменная b имеет тип NumInc через нее нельзя получить доступ к свойствам и методам, которых нет в NumInc. Объект NumMult приводится к типу NumInc с потерей своих дополнительных свойств и методов.

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

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

fun main() {
    val a: List<NumInc> = listOf(
        NumMult(),NumMult(3,4,3),
        NumInc(10, 3), NumInc(5, 1))
 
    for(i in a) {
        i.inc()
        println(i.number)
    }
}

Результат выполнения программы:

1
7
13
6

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

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

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

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

Комментарии

open class NumInc(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    fun inc() {
        number += step
    }
    fun dec() {
        number -= step
    }
}
 
class NumMul(n: Int, gap: Int, coef: Int): NumInc(n, gap) {
    var coefficient = coef
 
    constructor(): this(0, 1, 2)
 
    fun mul() {
        number *= coefficient
    }
}