13 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Абстрактные классы | abstract против open, шаблонный метод и выбор между абстрактным классом и интерфейсом |
|
Абстрактный класс нельзя создать - только унаследовать.
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 - писать его не нужно:
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 |
есть | можно |
| нет модификатора | есть | нельзя |
Абстрактный класс может не иметь ни одного абстрактного члена - тогда он просто запрещает создание экземпляра:
abstract class Utils {
fun helper() = "общая логика"
}
class Concrete : Utils()
fun main() { println(Concrete().helper()) } // общая логика
Абстрактный класс может иметь состояние
Это главное отличие от интерфейса:
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
}
Конструктор, приватные поля, инициализация - всё доступно. У интерфейса этого нет.
Шаблонный метод
Самое частое применение: алгоритм зафиксирован в базовом классе, шаги переопределяются.
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 для членов, нужных подклассам
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: активно
}
Абстрактный класс или интерфейс
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-шагами
На практике часто сочетают: публичный интерфейс для клиентов и абстрактный класс как база для реализаций.
Подробнее: Интерфейсы
Абстрактный класс и sealed
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
[!info] Отличия от Java
Java Kotlin abstract class Aто же abstract void f();abstract fun f()поля protectedсвойства protectedабстрактное поле невозможно abstract valdefault-методы в интерфейсе (Java 8+)реализация в интерфейсе без ключевого слова
abstract val- то, чего в Java нет. Java позволяет объявить абстрактным только метод, поэтому «абстрактное свойство» выражается парой геттер/сеттер:abstract class Shape { abstract double getArea(); // абстрактный геттер }В Kotlin
abstract val area: Doubleоставляет наследнику выбор: хранимое свойство, геттер или параметр конструктора. Клиент разницы не видит.
abstractне требуетopen, потому что отсутствие реализации уже означает необходимость переопределения. В Javafinal abstract- ошибка компиляции по той же причине.Доля абстрактных классов в Kotlin ниже. Часть их задач закрывают другие средства:
sealedдля закрытых иерархий, делегирование для переиспользования кода, функции-расширения для добавления поведения без наследования.Сравнить: Абстрактные классы в Java
[!warning] Подводные камни Вызов абстрактного метода из
init- гарантированная ловушка: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. Подробнее: ИнициализацияАбстрактный класс расходует единственный слот наследования. Класс, унаследованный от абстрактного, больше ни от чего наследоваться не сможет. Поэтому публичный API лучше начинать с интерфейса.
protected abstract- часть контракта с наследниками, и менять его так же больно, как публичный API: изменение сигнатуры сломает все подклассы.Абстрактный класс с конструктором усложняет тестирование. Чтобы проверить логику шаблонного метода, нужен наследник; с интерфейсом достаточно лямбды или объекта-выражения.
abstractиprivateнесовместимы - приватный член невозможно переопределить: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()) } // готовоАбстрактное свойство легко переопределить несогласованно:
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или как ключа это даст тяжёлые для отладки ошибки.Глубокие абстрактные иерархии трудны для чтения. Если шаблонный метод вызывает абстрактные шаги, которые вызывают другие шаблонные методы, поведение собирается из четырёх файлов.
Дополнительно: Интерфейсы Далее: Интерфейсы