Интерфейсы представляют контракт - набор функциональности, который должен реализовать класс. Интерфейсы могут содержать объявления свойств и функций, а также могут содержать их реализацию по умолчанию. Интерфейсы позволяют реализовать в программе концепцию полиморфизма и решить проблему множественного наследования, поскольку класс может унаследовать только один класс, зато интерфейсов он может реализовать множество.
Для определения интерфейса применяется ключевое слово interface:
interface название_интерфейса{ // определения функций и свойств }
Для применения интерфейса после имени класса через двоеточие (как при наследовании) указывается имя применяемого интерфейса:
interface Movable{} class Car : Movable {}
В данном случае класс Car применяет или реализует интерфейс Movable.
Интерфейс может определять функции без реализации. Например:
interface Movable{ fun move() // определение функции без реализации }
Например, в данном случае интерфейс Movable представляет функционал транспортного средства и определяет одну функцию без реализации - функцию
move()
, которая условно предназначена для передвижения транспортного средства.
Таким образом, у нас еть интерфейс Movable, которое представляет непонятно какое транспортное средство, и есть функция move, которая предназначена для перемещения транспортного средства, но как именно это перемещение осуществляется - неизвестно.
Стоит отметить, что мы не можем напрямую создать объект интерфейса, так как интерфейс не поддерживает конструкторы и просто представляет шаблон, которому класс должен соответствовать.
Определим два класса, которые применяют этот интерфейс:
// класс машины class Car : Movable{ override fun move(){ println("Едем на машине") } } // класс самолета class Aircraft : Movable{ override fun move(){ println("Летим на самолете") } }
Здесь определены классы Car и Aircraft, которые условно представляют машину и самолет. При применении интерфейса класс должен реализовать все его абстрактные методы и свойства. При реализации функций и свойств перед ними ставится ключевое слово override.
Так, класс Car
применяет интерфейс Movable
. Так как интерфейс содержит абстрактный метод move()
, то
класс Car
обязательно должен его реализовать. То же самое касается класса Aircraft.
Далее мы можем вызвать реализованный метод move как любую другую функцию класса:
fun main() { val car = Car() val aircraft = Aircraft() car.move() aircraft.move() }
Консольный вывод программы:
Едем на машине Летим на самолете
И реализация интерфейса также означает, что мы можем рассматривать объекты классом Car и Aircraft как объекты Movable. И тут в дело вступает полиморфизм:
fun main() { val car = Car() val aircraft = Aircraft() travel(car) // Едем на машине travel(aircraft) // Летим на самолете } fun travel(obj: Movable) = obj.move() interface Movable{ fun move() } class Car : Movable{ override fun move(){ println("Едем на машине") } } class Aircraft : Movable{ override fun move(){ println("Летим на самолете") } }
В данном случае функция travel (условная функция путешествия на транспорте) в качестве параметра получает объект Movable. Это может быть машина, и самолет, и любой другой объект, класс которого реализует интерфейс Movable
Также стоит отметить, что мы можем напрямую определить объекты типа интерфейса, но для их создания будут применяться конструкторы классов, которые реализуют интерфейс:
val car : Movable = Car() val aircraft : Movable = Aircraft()
Мы не можем наследовать один класс от нескольких классов, зато класс может реализовать множество интерфейсов. Например, у нас есть два интерфейса:
interface Worker{ fun work() } interface Student{ fun study() }
Интерфейс Worker представляет работающего, а интерфейс Student - учащегося. А что если нам надо определить сущность работающего студента? В этом случае мы можем реализовать в классе оба этих интерфейса:
fun main() { val tom = WorkingStudent("Tom") work(tom) // Tom работает study(tom) // Tom учится } fun work(worker:Worker) = worker.work() fun study(student:Student) = student.study() interface Worker{ fun work() } interface Student{ fun study() } class WorkingStudent(val name:String) : Worker, Student{ override fun work() = println("$name работает") override fun study() = println("$name учится") }
Класс WorkingStudent реализует оба интерфейса - Worker и Student. Все реализуемые интерфейсы передаются после двоеточия через запятую.
Интерфейс может также определять реализацию по умолчанию для своих методов. В свою очередь, класс, который реализует этот интерфейс, может принять эти методы как есть, а может и переопределить их. Например:
fun main() { val car = Car() val aircraft = Aircraft() car.move() // Едем на машине car.stop() // Останавливаемся... aircraft.move() // Летим на самолете aircraft.stop() // Приземляемся... } interface Movable{ fun move() // определение функции без реализации fun stop() { // определение функции с реализацией по умолчанию println("Останавливаемся...") } } class Car : Movable{ override fun move(){ println("Едем на машине") } } class Aircraft : Movable{ override fun move(){ println("Летим на самолете") } override fun stop() = println("Приземляемся...") }
Здесь в интерфейсе Movable для функции stop определена реализация по умолчанию. Класс Car не изменяет ее. А класс Aircraft переопределяет эту функцию.
Интерфейс может определять свойства - таким свойствам в интерфейсе им не присваиваются значения. Класс же, который реализует интерфейс, также обязан реализовать эти свойства. Например:
fun main() { val car = Car() val aircraft = Aircraft() car.move() // Едем на машине со скоростью 60 км/ч aircraft.move() // Летим на самолете со скоростью 600 км/ч } interface Movable{ var speed: Int // объявление свойства fun move() // определение функции без реализации } class Car : Movable{ override var speed = 60 override fun move() { println("Едем на машине со скоростью $speed км/ч") } } class Aircraft : Movable{ override var speed = 600 override fun move(){ println("Летим на самолете со скоростью $speed км/ч") } }
В данном случае в интерфейсе Movable определено свойство speed
. Здесь реализация свойства в классах заключается в установке для него начального значения.
Стоит отметить, что реализуемые свойства интерфейса могут устанавливаться через конструктор. Иногда это единственное место, где можно получить значения для свойств. Например:
fun main() { val tesla: Car = Car("Tesla", "2345SDG") println(tesla.model) // Tesla println(tesla.number) // 2345SDG tesla.move() // Едем на машине со скоростью 60 км/ч } interface Movable{ var speed: Int // объявление свойства val model: String val number: String fun move() // определение функции без реализации } // в первичном конструкторе реализуем свойства интерфейса class Car(override val model: String, override var number: String) : Movable{ override var speed = 60 override fun move() { println("Едем на машине со скоростью $speed км/ч") } }
Здесь интерфейс Movable также определяет свойства model (модель) и number (номер транспортного средства). Но эти характеристики различаются для каждой конкретной машины, соответственно их предпочтительнее устанавливать в конструкторе. В примере выше они устанавливаются в первичном конструкторе класса Car.
В Kotlin мы можем одновременно реализовать интерфейсы, которые определяют функцию с одним и тем же именем. То же самое касается ситуации, когда класс одновременно реализует интерфейс и наследует класс, которые имеют одноименную функцию. В программировании подобная проблема известна как diamond problem или проблема "ромба"/"ромбовидного наследования". В этом случае класс, реализующий интерфейсы, может определить одну функцию для всех реализаций:
fun main() { val player = MediaPlayer() player.play() // Play audio and video } interface VideoPlayable { fun play() } interface AudioPlayable { fun play() } class MediaPlayer : VideoPlayable, AudioPlayable { // Функция play для обоих интерфейсов override fun play() = println("Play audio and video") }
Здесь интерфейсы VideoPlayable и AudioPlayable определяют функцию play. В этом случае класс MediaPlayer, который применяет оба интерфейса, обязательно должен определить функцию с тем же именем, то есть play.
Иногда может быть необходимо использовать функцию из интерфейса с реализацией по умолчанию, но при этом добавить к ней еще какой-то функционал. В этом случае нет
смысла дублировать в классе реализацию по умолчанию. И мы можем обратиться к реализации из интерфейса с помощью конструкции super<интерфейс>.имя_функции
:
fun main() { val player = MediaPlayer() player.play() } interface VideoPlayable { fun play() = println("Play video") } interface AudioPlayable { fun play() = println("Play audio") } class MediaPlayer : VideoPlayable, AudioPlayable { // Функцию play обязательно надо переопределить override fun play() { println("Start playing") super<VideoPlayable>.play() // вызываем VideoPlayable.play() super<AudioPlayable>.play() // вызываем AudioPlayable.play() } }
В данном случае интерфейсы VideoPlayable и AudioPlayable определяют для функции play реализацию по умолчанию, а в классе MediaPlayer вызывается эта реализация.