Files
obsidian/Kotlin/ООП/Абстрактные классы.md
T

307 lines
13 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: 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)