333 lines
15 KiB
Markdown
333 lines
15 KiB
Markdown
---
|
||
title: Интерфейсы
|
||
description: Реализация по умолчанию, свойства без состояния, ромбовидное наследование и fun interface
|
||
tags:
|
||
- kotlin
|
||
- oop
|
||
---
|
||
|
||
Интерфейс описывает контракт. В Kotlin он может содержать и реализацию.
|
||
|
||
```kotlin
|
||
interface Drawable2 {
|
||
fun draw(): String // только объявление
|
||
fun show() = "рисую: ${draw()}" // реализация по умолчанию
|
||
}
|
||
|
||
class Circle4 : Drawable2 {
|
||
override fun draw() = "круг"
|
||
}
|
||
|
||
fun main() {
|
||
println(Circle4().show()) // рисую: круг
|
||
}
|
||
```
|
||
Скобок после имени интерфейса в списке супертипов нет - конструктора у него не бывает.
|
||
|
||
## Свойства в интерфейсе
|
||
Свойство объявить можно, но **хранить значение - нет**:
|
||
```kotlin
|
||
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) // админ
|
||
}
|
||
```
|
||
Реализовать свойство можно параметром конструктора, хранимым свойством или геттером - интерфейсу это безразлично.
|
||
|
||
## Множественная реализация
|
||
```kotlin
|
||
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()) // закрыто
|
||
}
|
||
```
|
||
Ограничений на число интерфейсов нет - в отличие от классов.
|
||
|
||
## Ромбовидное наследование
|
||
Если два интерфейса дают реализацию одного метода, компилятор требует разрешить конфликт:
|
||
```kotlin
|
||
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` нужен, даже если реализацию даёт только **один** из интерфейсов:
|
||
```kotlin
|
||
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` не нужно:
|
||
```kotlin
|
||
interface Named3 { fun name(): String }
|
||
|
||
abstract class Base3 : Named3 {
|
||
override fun name() = "из класса"
|
||
}
|
||
|
||
class Impl10 : Base3(), Named3 // реализация унаследована от Base3
|
||
|
||
fun main() { println(Impl10().name()) } // из класса
|
||
```
|
||
|
||
Для свойств правило то же:
|
||
```kotlin
|
||
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
|
||
```
|
||
|
||
## Наследование интерфейсов
|
||
```kotlin
|
||
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-члены
|
||
```kotlin
|
||
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`, и тогда вместо объекта допустима лямбда:
|
||
```kotlin
|
||
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
|
||
- имя контракта важно для читаемости
|
||
|
||
```kotlin
|
||
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
|
||
}
|
||
```
|
||
|
||
## Интерфейсы в делегировании
|
||
```kotlin
|
||
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] работаю
|
||
}
|
||
```
|
||
Подробнее: [Делегирование](Делегирование.md)
|
||
|
||
## Интерфейс как граница модуля
|
||
```kotlin
|
||
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` и подменяемы. Подробнее: [Модификаторы видимости](Модификаторы%20видимости.md)
|
||
|
||
> [!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-интерфейс объявляет геттер как метод, и реализация обязана быть методом:
|
||
> ```java
|
||
> interface Named { String getName(); }
|
||
> ```
|
||
> Kotlin позволяет реализовать `val name: String` параметром конструктора - короче и без ручного геттера.
|
||
>
|
||
> **Константы в интерфейсе работают иначе.** В Java поле интерфейса неявно `public static final`, что породило антипаттерн «интерфейс-константа». В Kotlin поля в интерфейсе запрещены; константы живут в `companion object`:
|
||
> ```kotlin
|
||
> 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](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||
>
|
||
> Сравнить: [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **Свойство в интерфейсе не хранит значение.** Попытка задать значение - ошибка:
|
||
> ```kotlin
|
||
> 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
|
||
> }
|
||
> ```
|
||
>
|
||
> **Геттер в интерфейсе вычисляется у каждой реализации заново** - это не кеш:
|
||
> ```kotlin
|
||
> 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` в интерфейсе запрещён.** Если нужна защищённая логика для наследников, нужен абстрактный класс. Подробнее: [Абстрактные классы](Абстрактные%20классы.md)
|
||
>
|
||
> **Добавление метода в интерфейс - ломающее изменение** для всех реализаций, если у него нет реализации по умолчанию. Для публичного API это означает, что интерфейс стоит проектировать узким.
|
||
>
|
||
> **`fun interface` позволяет только один абстрактный метод.** Добавив второй, вы сломаете все вызовы с лямбдой:
|
||
> ```kotlin
|
||
> 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 считается необязательным.
|
||
|
||
Дополнительно: [Абстрактные классы](Абстрактные%20классы.md)
|
||
Далее: [Делегирование](Делегирование.md)
|