Интерфейсы

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

Интерфейс определяется ключевым словом interface:

interface MyInterface {
    fun bar()
    fun foo() {
      // необязательное тело
    }
}

Реализация интерфейсов

Класс или объект могут реализовать один или несколько интерфейсов.

class Child : MyInterface {
    override fun bar() {
        // тело
    }
}

Свойства в интерфейсах

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

interface MyInterface {
    val prop: Int // абстрактное свойство

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

Наследование интерфейсов

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

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String

    override val name: String get() = "$firstName $lastName"
}

data class Employee(
    // реализовывать 'name' не требуется
    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person

Устранение противоречий при переопределении

Когда вы объявляете много типов в списке супертипов, вы можете унаследовать более одной реализации одного и того же метода.

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

Оба интерфейса A и B объявляют функции foo() и bar(). Оба реализуют foo(), но только B содержит реализацию bar() (bar() не отмечен как абстрактный метод в интерфейсе A, потому что в интерфейсах это подразумевается по умолчанию, если у функции нет тела). Теперь, если вы наследуете конкретный класс C от интерфейса A, вам нужно переопределить bar() и предоставить реализацию.

Однако если вы унаследуете класс D от интерфейсов A и B, вам надо будет переопределять все методы, унаследованные от этих интерфейсов, и вам нужно указать, как именно D должен их реализовать. Это правило касается как тех методов, у которых имеется только одна реализация (bar()), так и тех, у которых есть несколько реализаций (foo()).

Генерация JVM-методов по умолчанию для функций интерфейсов

На JVM функции, объявленные в интерфейсах, компилируются в методы по умолчанию. Вы можете управлять этим поведением с помощью параметра компилятора -jvm-default со следующими значениями:

  • enable (по умолчанию): генерирует реализации по умолчанию в интерфейсах и включает мостовые функции в подклассах и классах DefaultImpls. Используйте этот режим, чтобы сохранить бинарную совместимость со старыми версиями Kotlin.
  • no-compatibility: генерирует только реализации по умолчанию в интерфейсах. Этот режим пропускает мосты совместимости и классы DefaultImpls, поэтому подходит для нового кода на Kotlin.
  • disable: пропускает методы по умолчанию и генерирует только мосты совместимости и классы DefaultImpls.

Чтобы настроить параметр компилятора -jvm-default, задайте свойство jvmDefault в Kotlin DSL для Gradle:

kotlin {
    compilerOptions {
        jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
    }
}