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

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

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

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}
}

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

После этого объекты 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. Курс"

Введение в объектно-ориентированное программирование на Kotlin