Интерфейсы

Последнее обновление: 03.03.2024

Интерфейсы представляют контракт - набор функциональности, который должен реализовать класс. Интерфейсы могут содержать объявления свойств и функций, а также могут содержать их реализацию по умолчанию. Интерфейсы позволяют реализовать в программе концепцию полиморфизма и решить проблему множественного наследования, поскольку класс может унаследовать только один класс, зато интерфейсов он может реализовать множество.

Для определения интерфейса применяется ключевое слово 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 вызывается эта реализация.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850