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

333 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)