Переопределение в Kotlin
Наследование в Kotlin начинается не с объявления программистом какого-то класса родительским. Уже самый первый родительский класс является наследником общего для всех суперкласса Any
. Это наследование происходит по-умолчанию, поэтому его можно не указывать. В случае явного указания заголовок класса выглядит примерно так:
class NumInc(n: Int, gap: Int): Any() { ...
Поскольку дочерние классы наследуют свойства и функции родительского, то все, что есть у Any
, есть у всех остальных классов. Не важно, непосредственно это класс-наследник или наследник наследника.
Класс Any
включает три функции-члена – equals()
, toString()
, hashСode()
. Если мы создадим пустой класс, у его объектов все-равно будут методы, унаследованные от Any
:
fun main() { val a = Days() val b = Days() println(a.equals(b)) // или a == b println(a.hashCode()) println(b.hashCode()) println(a.toString()) println(a) } class Days {}
Пример выполнения программы:
false 1018547642 1456208737 Days@3cb5cdba Days@3cb5cdba
Метод toString()
в данном случае можно не вызывать, так как функция println()
делает это сама. Этот метод отвечает за преобразование объекта, каких-то данных о нем, к строке. Он должен возвращать строку. В классе Any
запрограммирован возврат строки, содержащей имя класса и через "собаку" по всей видимости адрес объекта в памяти. Нас может не устраивать такое положение дел, лучше бы, чтобы toString()
выводит что-нибудь более полезное.
Для этого нам потребуется переопределить в нашем классе функцию-член родительского класса. Это значит, что мы создаем в своем классе метод с таким же именем, что и в родительском, а тело метода пишем свое. И когда объект будет вызывать этот метод, будет использована функция-член своего класса, а не родительского.
Если в классе начать писать имя метода родительского класса, то IntelliJ IDEA сама предложит его для переопределения.
После подтверждения код класса будет выглядеть так:
open class NumInc(n: Int, gap: Int) { var number = n var step = gap fun inc() {number += step} fun dec() {number -= step} override fun toString(): String { return super.toString() } }
Слово override
говорит, что данная функция переопределяет родительскую. String
после двоеточия – тип возвращаемых функцией данных, должна возвращаться строка.
В теле автоматически сгенерированной функции происходит обращение к одноименной функции-члену родительского класса. Происходит это через ключевое слово super
, которое обозначает родительский класс. Из него мы получаем строку, строка возвращается в функцию toString()
класса NumInc, а уже отсюда с помощью return
она передается в место, откуда вызывался данный метод.
В таком варианте, несмотря на то, что мы переопределили функцию в дочернем классе, ничего не изменилось. Как по большому счету выполнялось тело функции родительского класса, так оно и будет выполняться. Просто вызов идет не напрямую, а опосредовано через дочерний класс.
Пусть мы хотим, чтобы метод toString()
возвращал информацию о текущих значениях свойств объектов. Для этого в теле метода удалим вызов аналогичного метода родительского класса и напишем свое тело:
open class NumInc(n: Int, gap: Int) { var number = n var step = gap fun inc() {number += step} fun dec() {number -= step} override fun toString(): String { val n = "number = $number" val s = "step = $step" return "$n \n $s" } }
Если main
будет таким,
fun main() { val a = NumInc(0, 1) val b = NumInc(12, 2) println(a.toString()) println(b) }
результат выполнения таким.
number = 0 step = 1 number = 12 step = 2
Мы можем переопределять не только методы класса Any
, но и своего родительского в его дочернем. Пусть нам нужен класс подобный NumInc, но функции inc()
и dec()
которого уменьшают и увеличивают number на двойной шаг. В этом случае, если этот класс будет дочерним от NumInc, нам придется переопределить функции inc()
и dec()
:
class NumDouble(n: Int, gap: Int): NumInc(n, gap) { override fun inc() { number += step * 2 } override fun dec() { number -= step * 2 } }
Однако мы не сможем этого сделать до тех пор, пока в родительском классе не разрешим переопределять методы с этими именами. Для этого следует объявить как открытые с помощью слова open
:
open class NumInc(n: Int, gap: Int) { var number = n var step = gap open fun inc() {number += step} open fun dec() {number -= step} ... }
Другими словами, в Kotlin, чтобы иметь возможность переопределять методы, недостаточно, чтобы их класс был открытым. Необходимо дополнительно указывать такую возможность для каждого метода. По-умолчанию методы вместо модификатора open
имеют модификатор final
. Мы его не пишем.
Однако, если дочерний класс переопределяет метод родительского, то в нем open
не пишется. Вместо этого стоит override
. При этом метод остается открытым. Если у дочернего будет свой дочерний, то он сможет переопределить такой метод. Если требуется исключить возможность дальнейшего переопределения, то перед override
пишут final
.
В примере на скрине мы объявили класс NumDouble открытым и создали от него дочерний NumD2. В нем мы можем переопределить метод inc()
и не можем сделать это с dec()
, потому что в NumDouble объявили метод финальным.
Бывает так, что код переопределяемого метода не сильно отличается от того, что используется в родительском классе. Нам нужно его лишь дополнить. В этом случае из тела метода дочернего класса, вызывается родительский метод через слово super
. После того, как он возвращает поток выполнения в метод дочернего класса, здесь выполняется дополнительный код. Подобное мы уже видели, переопределяя toString()
.
Давайте сделаем это в нашем классе NumDouble, хотя в данном случае большого смысла в этом нет. Тело класса очень простое.
open class NumDouble(n: Int, gap: Int): NumInc(n, gap) { override fun inc() { super.inc() number += step } override fun dec() { super.dec() number -= step } }
Здесь мы изменяем number на один шаг через родительский метод, потом делаем это еще раз уже в дочернем.
Следует отметить, что переопределять можно не только функции-члены класса, но и свойства. Однако нельзя изменить их тип. Поэтому такое переопределение обычно имеет место, когда используются инициирующие значения для полей, и в дочернем классе эти значения должны быть не такими, каковы они в родительском.
open class NumInc { open var number = 0 open var step = 1 open fun inc() {number += step} open fun dec() {number -= step} } open class NumDouble: NumInc() { override var number = 1 override var step = 2 }
Практическая работа:
Пусть свойство step родительского класса NumInc имеет сеттер, проверяющий число на неравенство нулю (нулевой шаг недопустим). В дочернем классе NumDouble такая проверка не нужна. Наследуется ли сеттер, следует ли его переопределять?
PDF-версия курса с ответами к практическим работам