Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -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)