--- 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() // состояние 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 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)