15 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Интерфейсы | Реализация по умолчанию, свойства без состояния, ромбовидное наследование и fun interface |
|
Интерфейс описывает контракт. В 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 objectA.super.f()super<A>.f()@FunctionalInterfacefun 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-класс, а не в Javadefault-метод. 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 считается необязательным.
Дополнительно: Абстрактные классы Далее: Делегирование