Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
---
|
||||
title: Абстрактные классы
|
||||
description: abstract против open, шаблонный метод и выбор между абстрактным классом и интерфейсом
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Абстрактный класс нельзя создать - только унаследовать.
|
||||
|
||||
```kotlin
|
||||
abstract class Shape4 {
|
||||
abstract val area: Double // без реализации
|
||||
abstract fun name(): String
|
||||
|
||||
fun describe() = "${name()}: площадь %.2f".format(java.util.Locale.US, area)
|
||||
}
|
||||
|
||||
class Circle3(val radius: Double) : Shape4() {
|
||||
override val area get() = Math.PI * radius * radius
|
||||
override fun name() = "круг"
|
||||
}
|
||||
|
||||
class Square3(val side: Double) : Shape4() {
|
||||
override val area get() = side * side
|
||||
override fun name() = "квадрат"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val shapes = listOf(Circle3(1.0), Square3(2.0))
|
||||
shapes.forEach { println(it.describe()) }
|
||||
// круг: площадь 3.14
|
||||
// квадрат: площадь 4.00
|
||||
// println(Shape4()) // ошибка: нельзя создать абстрактный класс
|
||||
}
|
||||
```
|
||||
|
||||
## abstract и open
|
||||
`abstract` подразумевает `open` - писать его не нужно:
|
||||
```kotlin
|
||||
abstract class Base32 {
|
||||
abstract fun required() // open не нужен, реализации нет
|
||||
open fun optional() = "по умолчанию" // есть реализация, но переопределяема
|
||||
fun fixed() = "не переопределить" // final
|
||||
}
|
||||
|
||||
class Impl6 : Base32() {
|
||||
override fun required() = Unit
|
||||
override fun optional() = "своё"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val i = Impl6()
|
||||
println("${i.optional()} / ${i.fixed()}") // своё / не переопределить
|
||||
}
|
||||
```
|
||||
|
||||
| Модификатор | Реализация | Нужно переопределять |
|
||||
|---|---|---|
|
||||
| `abstract` | нет | обязательно |
|
||||
| `open` | есть | можно |
|
||||
| нет модификатора | есть | нельзя |
|
||||
|
||||
Абстрактный класс может не иметь ни одного абстрактного члена - тогда он просто запрещает создание экземпляра:
|
||||
```kotlin
|
||||
abstract class Utils {
|
||||
fun helper() = "общая логика"
|
||||
}
|
||||
|
||||
class Concrete : Utils()
|
||||
|
||||
fun main() { println(Concrete().helper()) } // общая логика
|
||||
```
|
||||
|
||||
## Абстрактный класс может иметь состояние
|
||||
Это главное отличие от интерфейса:
|
||||
```kotlin
|
||||
abstract class Repository2(protected val tableName: String) {
|
||||
private val cache = mutableMapOf<Int, String>() // состояние
|
||||
|
||||
protected abstract fun loadFromDb(id: Int): String?
|
||||
|
||||
fun find(id: Int): String? = cache.getOrPut(id) {
|
||||
loadFromDb(id) ?: return null
|
||||
}
|
||||
|
||||
fun cacheSize() = cache.size
|
||||
}
|
||||
|
||||
class UserRepository2 : Repository2("users") {
|
||||
override fun loadFromDb(id: Int) = if (id == 1) "Иван" else null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = UserRepository2()
|
||||
println(repo.find(1)) // Иван
|
||||
println(repo.find(1)) // Иван — из кеша
|
||||
println(repo.cacheSize()) // 1
|
||||
println(repo.find(99)) // null
|
||||
}
|
||||
```
|
||||
Конструктор, приватные поля, инициализация - всё доступно. У интерфейса этого нет.
|
||||
|
||||
## Шаблонный метод
|
||||
Самое частое применение: алгоритм зафиксирован в базовом классе, шаги переопределяются.
|
||||
```kotlin
|
||||
abstract class Importer {
|
||||
fun import(data: String): String {
|
||||
val parsed = parse(data)
|
||||
val valid = parsed.filter { validate(it) }
|
||||
return "загружено ${valid.size} из ${parsed.size}: $valid"
|
||||
}
|
||||
|
||||
protected abstract fun parse(data: String): List<String>
|
||||
protected open fun validate(item: String) = item.isNotBlank()
|
||||
}
|
||||
|
||||
class CsvImporter : Importer() {
|
||||
override fun parse(data: String) = data.split(",").map { it.trim() }
|
||||
}
|
||||
|
||||
class StrictCsvImporter : Importer() {
|
||||
override fun parse(data: String) = data.split(",").map { it.trim() }
|
||||
override fun validate(item: String) = item.length >= 3
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(CsvImporter().import("a, b, , c"))
|
||||
// загружено 3 из 4: [a, b, c]
|
||||
println(StrictCsvImporter().import("abc, b, defg"))
|
||||
// загружено 2 из 3: [abc, defg]
|
||||
}
|
||||
```
|
||||
`import` не `open` - порядок шагов менять нельзя. `parse` обязателен, `validate` имеет разумное поведение по умолчанию.
|
||||
|
||||
## protected для членов, нужных подклассам
|
||||
```kotlin
|
||||
abstract class Connection {
|
||||
protected var isOpen = false
|
||||
private set
|
||||
|
||||
protected abstract fun doConnect(): Boolean
|
||||
|
||||
fun connect(): String {
|
||||
isOpen = doConnect()
|
||||
return if (isOpen) "подключено" else "ошибка"
|
||||
}
|
||||
}
|
||||
|
||||
class TcpConnection(private val host: String) : Connection() {
|
||||
override fun doConnect() = host.isNotBlank()
|
||||
fun status() = if (isOpen) "$host: активно" else "$host: закрыто"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = TcpConnection("example.com")
|
||||
println(c.connect()) // подключено
|
||||
println(c.status()) // example.com: активно
|
||||
}
|
||||
```
|
||||
|
||||
## Абстрактный класс или интерфейс
|
||||
```kotlin
|
||||
interface Drawable { fun draw(): String } // контракт
|
||||
|
||||
abstract class Widget(val id: String) : Drawable { // + состояние и общая логика
|
||||
protected var visible = true
|
||||
fun hide() { visible = false }
|
||||
override fun draw() = if (visible) render() else ""
|
||||
protected abstract fun render(): String
|
||||
}
|
||||
|
||||
class Button(id: String, val label: String) : Widget(id) {
|
||||
override fun render() = "[$label]"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val b = Button("b1", "ОК")
|
||||
println(b.draw()) // [ОК]
|
||||
b.hide()
|
||||
println("'${b.draw()}'") // ''
|
||||
}
|
||||
```
|
||||
|
||||
| | Интерфейс | Абстрактный класс |
|
||||
|---|---|---|
|
||||
| сколько можно реализовать | много | один |
|
||||
| состояние (свойство с полем) | нет | да |
|
||||
| конструктор | нет | да |
|
||||
| `init` | нет | да |
|
||||
| `private`-члены | да (с реализацией) | да |
|
||||
| `protected`-члены | **нет** | да |
|
||||
| видимость членов | только `public` или `private` | любая |
|
||||
|
||||
Правило выбора:
|
||||
- **интерфейс** - когда описываете возможность («умеет рисоваться», «сравним»), особенно если её могут захотеть добавить к существующим классам
|
||||
- **абстрактный класс** - когда у наследников общее состояние или нужен шаблонный метод с `protected`-шагами
|
||||
|
||||
На практике часто сочетают: публичный интерфейс для клиентов и абстрактный класс как база для реализаций.
|
||||
|
||||
Подробнее: [Интерфейсы](Интерфейсы.md)
|
||||
|
||||
## Абстрактный класс и sealed
|
||||
```kotlin
|
||||
sealed class Result4 {
|
||||
abstract val isSuccess: Boolean
|
||||
}
|
||||
|
||||
class Ok2(val data: String) : Result4() {
|
||||
override val isSuccess = true
|
||||
}
|
||||
|
||||
class Fail(val error: String) : Result4() {
|
||||
override val isSuccess = false
|
||||
}
|
||||
|
||||
fun handle(r: Result4) = when (r) { // else не нужен
|
||||
is Ok2 -> "данные: ${r.data}"
|
||||
is Fail -> "ошибка: ${r.error}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle(Ok2("готово"))) // данные: готово
|
||||
println(Fail("сбой").isSuccess) // false
|
||||
}
|
||||
```
|
||||
`sealed` класс абстрактен по определению. Разница: `abstract` допускает наследников где угодно, `sealed` - только в своём модуле и пакете, зато даёт исчерпывающий `when`. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `abstract class A` | то же |
|
||||
> | `abstract void f();` | `abstract fun f()` |
|
||||
> | поля `protected` | свойства `protected` |
|
||||
> | абстрактное поле невозможно | `abstract val` |
|
||||
> | `default`-методы в интерфейсе (Java 8+) | реализация в интерфейсе без ключевого слова |
|
||||
>
|
||||
> **`abstract val` - то, чего в Java нет.** Java позволяет объявить абстрактным только метод, поэтому «абстрактное свойство» выражается парой геттер/сеттер:
|
||||
> ```java
|
||||
> abstract class Shape {
|
||||
> abstract double getArea(); // абстрактный геттер
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `abstract val area: Double` оставляет наследнику выбор: хранимое свойство, геттер или параметр конструктора. Клиент разницы не видит.
|
||||
>
|
||||
> **`abstract` не требует `open`**, потому что отсутствие реализации уже означает необходимость переопределения. В Java `final abstract` - ошибка компиляции по той же причине.
|
||||
>
|
||||
> **Доля абстрактных классов в Kotlin ниже.** Часть их задач закрывают другие средства: `sealed` для закрытых иерархий, делегирование для переиспользования кода, функции-расширения для добавления поведения без наследования.
|
||||
>
|
||||
> Сравнить: [Абстрактные классы в Java](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Вызов абстрактного метода из `init` - гарантированная ловушка:**
|
||||
> ```kotlin
|
||||
> abstract class Base33 {
|
||||
> init { println("из init: ${describe()}") }
|
||||
> abstract fun describe(): String
|
||||
> }
|
||||
>
|
||||
> class Derived32 : Base33() {
|
||||
> private val name: String = "Иван"
|
||||
> override fun describe() = "name=$name"
|
||||
> }
|
||||
>
|
||||
> fun main() { Derived32() } // из init: name=null
|
||||
> ```
|
||||
> Подкласс ещё не инициализирован, поэтому non-null свойство оказывается `null`. Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Абстрактный класс расходует единственный слот наследования.** Класс, унаследованный от абстрактного, больше ни от чего наследоваться не сможет. Поэтому публичный API лучше начинать с интерфейса.
|
||||
>
|
||||
> **`protected abstract` - часть контракта с наследниками**, и менять его так же больно, как публичный API: изменение сигнатуры сломает все подклассы.
|
||||
>
|
||||
> **Абстрактный класс с конструктором усложняет тестирование.** Чтобы проверить логику шаблонного метода, нужен наследник; с интерфейсом достаточно лямбды или объекта-выражения.
|
||||
>
|
||||
> **`abstract` и `private` несовместимы** - приватный член невозможно переопределить:
|
||||
> ```kotlin
|
||||
> abstract class Base34 {
|
||||
> // private abstract fun f() // ошибка
|
||||
> protected abstract fun f(): String
|
||||
> fun run3() = f()
|
||||
> }
|
||||
>
|
||||
> class Impl7 : Base34() { override fun f() = "готово" }
|
||||
>
|
||||
> fun main() { println(Impl7().run3()) } // готово
|
||||
> ```
|
||||
>
|
||||
> **Абстрактное свойство легко переопределить несогласованно:**
|
||||
> ```kotlin
|
||||
> abstract class Base35 { abstract val id: Int }
|
||||
>
|
||||
> class Unstable : Base35() {
|
||||
> override val id: Int get() = (0..100).random() // разное при каждом чтении
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val u = Unstable()
|
||||
> println(u.id == u.id) // чаще false
|
||||
> }
|
||||
> ```
|
||||
> Контракт «свойство стабильно» компилятором не проверяется; при использовании в `equals` или как ключа это даст тяжёлые для отладки ошибки.
|
||||
>
|
||||
> **Глубокие абстрактные иерархии трудны для чтения.** Если шаблонный метод вызывает абстрактные шаги, которые вызывают другие шаблонные методы, поведение собирается из четырёх файлов.
|
||||
|
||||
Дополнительно: [Интерфейсы](Интерфейсы.md)
|
||||
Далее: [Интерфейсы](Интерфейсы.md)
|
||||
Reference in New Issue
Block a user