Files
obsidian/Kotlin/ООП/Интерфейсы.md
T

15 KiB
Raw Blame History

title, description, tags
title description tags
Интерфейсы Реализация по умолчанию, свойства без состояния, ромбовидное наследование и fun interface
kotlin
oop

Интерфейс описывает контракт. В Kotlin он может содержать и реализацию.

interface Drawable2 {
    fun draw(): String                        // только объявление
    fun show() = "рисую: ${draw()}"           // реализация по умолчанию
}

class Circle4 : Drawable2 {
    override fun draw() = "круг"
}

fun main() {
    println(Circle4().show())      // рисую: круг
}

Скобок после имени интерфейса в списке супертипов нет - конструктора у него не бывает.

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

Свойство объявить можно, но хранить значение - нет:

interface Named2 {
    val name: String                     // абстрактное
    val upper: String get() = name.uppercase()   // через геттер — можно
    // val cached: String = "x"          // ошибка: поля в интерфейсе запрещены
}

class User5(override val name: String) : Named2

class Admin2 : Named2 {
    override val name get() = "админ"    // геттер вместо поля
}

fun main() {
    println(User5("иван").upper)      // ИВАН
    println(Admin2().name)             // админ
}

Реализовать свойство можно параметром конструктора, хранимым свойством или геттером - интерфейсу это безразлично.

Множественная реализация

interface Readable2 { fun read(): String }
interface Writable { fun write(data: String): String }
interface Closeable2 { fun close() = "закрыто" }

class File2(private val name: String) : Readable2, Writable, Closeable2 {
    override fun read() = "чтение $name"
    override fun write(data: String) = "запись '$data' в $name"
}

fun main() {
    val f = File2("a.txt")
    println(f.read())            // чтение a.txt
    println(f.write("текст"))    // запись 'текст' в a.txt
    println(f.close())           // закрыто
}

Ограничений на число интерфейсов нет - в отличие от классов.

Ромбовидное наследование

Если два интерфейса дают реализацию одного метода, компилятор требует разрешить конфликт:

interface A7 { fun f() = "A" }
interface B7 { fun f() = "B" }

class C7 : A7, B7 {
    override fun f() = "${super<A7>.f()}${super<B7>.f()}"
}

fun main() { println(C7().f()) }      // AB

Без override класс не скомпилируется: class C7 : A7, B7 даст ошибку inherits multiple interface methods.

Требование строгое: override нужен, даже если реализацию даёт только один из интерфейсов:

interface A8 { fun f(): String }          // абстрактный
interface B8 { fun f() = "B" }            // с реализацией

// class C8 : A8, B8                       // ошибка: inherits multiple interface methods
class C8 : A8, B8 {
    override fun f() = super<B8>.f()       // выбираем явно
}

fun main() { println(C8().f()) }           // B

А вот реализация из класса абстрактный метод интерфейса покрывает - здесь писать override не нужно:

interface Named3 { fun name(): String }

abstract class Base3 : Named3 {
    override fun name() = "из класса"
}

class Impl10 : Base3(), Named3             // реализация унаследована от Base3

fun main() { println(Impl10().name()) }    // из класса

Для свойств правило то же:

interface A9 { val name: String get() = "A" }
interface B9 { val name: String get() = "B" }

class C9 : A9, B9 {
    override val name get() = super<A9>.name + super<B9>.name
}

fun main() { println(C9().name) }    // AB

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

interface Entity2 { val id: Int }

interface Timestamped : Entity2 {
    val createdAt: Long
    fun age(now: Long) = now - createdAt
}

class Record2(override val id: Int, override val createdAt: Long) : Timestamped

fun main() {
    val r = Record2(1, 1000)
    println("${r.id} ${r.age(1500)}")      // 1 500
}

Интерфейс может наследовать несколько интерфейсов - ограничения на один супертип здесь нет.

private-члены

interface Validator2 {
    fun validate(value: String): Boolean

    private fun isBlank2(value: String) = value.trim().isEmpty()

    fun check(value: String) = when {
        isBlank2(value) -> "пусто"
        validate(value) -> "ок"
        else -> "неверно"
    }
}

class EmailValidator : Validator2 {
    override fun validate(value: String) = "@" in value
}

fun main() {
    val v = EmailValidator()
    println(v.check("  "))             // пусто
    println(v.check("a@b.ru"))         // ок
    println(v.check("нет"))            // неверно
}

private в интерфейсе - способ вынести вспомогательную логику, не включая её в контракт. protected в интерфейсе запрещён: все члены либо публичны, либо приватны.

fun interface - SAM

Интерфейс с единственным абстрактным методом можно пометить fun, и тогда вместо объекта допустима лямбда:

fun interface Transformer {
    fun transform(input: String): String
}

fun apply3(value: String, t: Transformer) = t.transform(value)

fun main() {
    println(apply3("abc", Transformer { it.uppercase() }))     // ABC
    println(apply3("abc") { it.reversed() })                    // cba

    val obj = object : Transformer {                            // можно и так
        override fun transform(input: String) = "[$input]"
    }
    println(apply3("abc", obj))                                 // [abc]
}

Для своего кода обычно лучше функциональный тип (String) -> String - он не требует объявления интерфейса. fun interface нужен, когда:

  • интерфейсу нужны дополнительные методы с реализацией
  • API должен быть удобным из Java
  • имя контракта важно для читаемости
fun interface Predicate2<T> {
    fun test(value: T): Boolean
    fun negate(): Predicate2<T> = Predicate2 { !test(it) }      // метод по умолчанию
}

fun main() {
    val positive = Predicate2<Int> { it > 0 }
    println(positive.test(5))              // true
    println(positive.negate().test(5))     // false
}

Интерфейсы в делегировании

interface Logger4 { fun log(msg: String): String }

class ConsoleLogger2 : Logger4 {
    override fun log(msg: String) = "[LOG] $msg"
}

class Service14(logger: Logger4) : Logger4 by logger {
    fun work() = log("работаю")
}

fun main() {
    println(Service14(ConsoleLogger2()).work())    // [LOG] работаю
}

Подробнее: Делегирование

Интерфейс как граница модуля

interface PaymentGateway {
    fun charge(amount: Long): String
}

internal class StripeGateway : PaymentGateway {
    override fun charge(amount: Long) = "Stripe: списано $amount"
}

internal class TestGateway : PaymentGateway {
    override fun charge(amount: Long) = "Тест: якобы списано $amount"
}

fun createGateway(test: Boolean): PaymentGateway =
    if (test) TestGateway() else StripeGateway()

fun main() {
    println(createGateway(test = true).charge(100))     // Тест: якобы списано 100
    println(createGateway(test = false).charge(100))    // Stripe: списано 100
}

Это основное практическое применение: клиент зависит от интерфейса, реализации скрыты за internal и подменяемы. Подробнее: Модификаторы видимости

[!info] Отличия от Java

Java Kotlin
interface A { } то же
default void f() { } fun f() { } - слово не нужно
static методы в интерфейсе companion object в интерфейсе
private методы (Java 9+) то же
поля public static final только const в companion object
A.super.f() super<A>.f()
@FunctionalInterface fun interface
нет свойств val/var в интерфейсе

Реализация по умолчанию не требует ключевого слова. В Java нужно default, потому что до версии 8 интерфейсы реализации не имели и слово понадобилось для совместимости. В Kotlin такого ограничения не было.

Свойства в интерфейсе - то, чего в Java нет. Java-интерфейс объявляет геттер как метод, и реализация обязана быть методом:

interface Named { String getName(); }

Kotlin позволяет реализовать val name: String параметром конструктора - короче и без ручного геттера.

Константы в интерфейсе работают иначе. В Java поле интерфейса неявно public static final, что породило антипаттерн «интерфейс-константа». В Kotlin поля в интерфейсе запрещены; константы живут в companion object:

interface Config6 {
    companion object { const val MAX = 100 }
}

fun main() { println(Config6.MAX) }      // 100

Реализация по умолчанию и совместимость с Java. По умолчанию Kotlin компилирует её в статический метод плюс DefaultImpls-класс, а не в Java default-метод. Java-классу, реализующему такой интерфейс, придётся реализовать метод самому. Флаг -Xjvm-default=all меняет это поведение. Подробнее: Kotlin из Java

Сравнить: Интерфейсы в Java, Полиморфизм подтипов

[!warning] Подводные камни Свойство в интерфейсе не хранит значение. Попытка задать значение - ошибка:

interface Counter2 {
    // var count: Int = 0           // ошибка: property initializers are not allowed
    var count: Int                   // только объявление
}

class Impl8 : Counter2 { override var count = 0 }

fun main() {
    val c = Impl8()
    c.count++
    println(c.count)      // 1
}

Геттер в интерфейсе вычисляется у каждой реализации заново - это не кеш:

interface Sized {
    val items: List<Int>
    val total: Int get() = items.sum().also { println("считаю") }
}

class Impl9 : Sized { override val items = listOf(1, 2) }

fun main() {
    val i = Impl9()
    println(i.total)
    println(i.total)      // "считаю" дважды
}

Кешировать можно только в классе-реализации через by lazy.

Ромбовидный конфликт не решается автоматически - нужен явный override с super<T>. Это строгость, а не недостаток: язык не выбирает реализацию за вас.

protected в интерфейсе запрещён. Если нужна защищённая логика для наследников, нужен абстрактный класс. Подробнее: Абстрактные классы

Добавление метода в интерфейс - ломающее изменение для всех реализаций, если у него нет реализации по умолчанию. Для публичного API это означает, что интерфейс стоит проектировать узким.

fun interface позволяет только один абстрактный метод. Добавив второй, вы сломаете все вызовы с лямбдой:

fun interface Handler2 { fun handle(x: Int): String }

fun main() {
    val h = Handler2 { "получено $it" }
    println(h.handle(1))      // получено 1
}

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

Из Java реализация по умолчанию не видна без -Xjvm-default=all. Java-код будет требовать реализовать метод, который в Kotlin считается необязательным.

Дополнительно: Абстрактные классы Далее: Делегирование