13 KiB
title: ООП description: Обзор объектной модели Kotlin: что закрыто по умолчанию, чего нет совсем и почему tags:
- kotlin
- oop
Объектная модель Kotlin устроена так же, как в Java - один суперкласс, множество интерфейсов, виртуальные вызовы. Отличия в значениях по умолчанию и в том, что из Java убрано.
Главное отличие: всё закрыто по умолчанию
class User(val name: String) {
fun greet() = "Привет, $name"
}
// class Admin : User("админ") // ошибка: User закрыт для наследования
Класс нельзя унаследовать, пока он не объявлен open. Метод нельзя переопределить, пока он не open. Это противоположность Java, где final нужно писать явно.
open class User(val name: String) {
open fun greet() = "Привет, $name"
fun id() = name.hashCode() // переопределить нельзя
}
class Admin(name: String) : User(name) {
override fun greet() = "Админ $name"
// override fun id() = 0 // ошибка: id не open
}
fun main() {
val u: User = Admin("Иван")
println(u.greet()) // Админ Иван — виртуальный вызов
}
Причина - проблема хрупкого базового класса. Автор класса, который не проектировал его под наследование, не может гарантировать, что подкласс не нарушит инварианты. open - это явное обещание поддерживать наследование как часть контракта.
Карта раздела
graph TD
A["Классы и конструкторы"] --> B["Свойства"]
B --> C["Инициализация"]
C --> D["Модификаторы видимости"]
D --> E["Наследование"]
E --> F["Переопределение"]
F --> G["Абстрактные классы"]
G --> H["Интерфейсы"]
H --> I["Делегирование"]
I --> J["Вложенные классы"]
J --> K["Полиморфизм"]
| Заметка | О чём |
|---|---|
| Классы и конструкторы | основной и вторичные конструкторы, параметры-свойства |
| Свойства | геттеры, сеттеры, backing field, lateinit, делегаты |
| Инициализация | порядок выполнения init, ловушка вызова open-метода |
| Модификаторы видимости | public, internal, protected, private |
| Наследование | open, вызов конструктора базового класса |
| Переопределение | override, final, super, ковариантность результата |
| Абстрактные классы | abstract против open, выбор с интерфейсом |
| Интерфейсы | реализация по умолчанию, свойства, ромбовидное наследование |
| Делегирование | by для интерфейсов и свойств |
| Вложенные и внутренние классы | nested по умолчанию, inner для ссылки на внешний |
| Полиморфизм | подтипы, перегрузка, дженерики |
Чего в Kotlin нет
| Из Java | Что вместо |
|---|---|
static |
companion object, функции верхнего уровня |
поля (field) в публичном API |
свойства с геттером и сеттером |
| checked exceptions | все исключения непроверяемые, см. Система исключений |
package-private (без модификатора) |
internal - видимость в пределах модуля |
| анонимные классы | объект-выражение |
| примитивы и обёртки как разные типы | один Int, упаковка по необходимости |
wait/notify в Object |
корутины и примитивы JDK |
Классы на любой вкус
Kotlin предлагает специализированные формы класса вместо одного универсального:
class Service // обычный
data class Point(val x: Int, val y: Int) // носитель данных
enum class Status { OK, FAIL } // фиксированный набор значений
sealed interface Result3 // фиксированный набор типов
object Registry // синглтон
@JvmInline value class UserId(val value: Int) // обёртка без аллокации
abstract class Shape // частичная реализация
interface Named // контракт
annotation class Marker // аннотация
fun interface Handler { fun handle(x: Int) } // SAM-интерфейс
Правильный выбор формы снимает большую часть ручной работы: data class избавляет от написания equals, sealed - от else в when, object - от реализации синглтона.
Подробнее о первых пяти: Базовые типы ООП
Свойства вместо полей
В Kotlin нет публичных полей - только свойства. Снаружи обращение выглядит как к полю, но всегда идёт через методы доступа:
class Rect(val width: Int, val height: Int) {
val area: Int get() = width * height // вычисляется при каждом чтении
var scale: Double = 1.0
set(value) {
require(value > 0) { "масштаб должен быть положительным" }
field = value
}
}
fun main() {
val r = Rect(2, 3)
println(r.area) // 6 — похоже на поле, но это вызов геттера
r.scale = 2.0
println(r.scale) // 2.0
}
Практическое следствие: заменить хранимое свойство на вычисляемое можно без изменения кода клиентов. В Java для этого пришлось бы менять obj.field на obj.getField() везде.
Подробнее: Свойства
Композиция удобнее наследования
Kotlin делает композицию такой же короткой, как наследование, - через делегирование:
interface Logger2 { fun log(msg: String) }
class ConsoleLogger : Logger2 {
override fun log(msg: String) = println("[LOG] $msg")
}
class Service2(logger: Logger2) : Logger2 by logger // делегирует всё
fun main() {
Service2(ConsoleLogger()).log("работает") // [LOG] работает
}
Service2 реализует Logger2, не наследуя реализацию: методы просто перенаправляются. Подробнее: Делегирование
Что осталось от Java без изменений
- одиночное наследование классов, множественная реализация интерфейсов
- динамическая диспетчеризация: вызов идёт по фактическому типу объекта
Anyв корне иерархии (аналогObject)- стирание типов в дженериках
- модель памяти и сборка мусора JVM
То есть байт-код, который порождает Kotlin, - обычные JVM-классы. Любая Java-библиотека работает с Kotlin-классами, и наоборот. Подробнее: Взаимодействие с Java
[!info] Отличия от Java
Java Kotlin класс открыт, finalзакрываеткласс закрыт, openоткрываетметод виртуален по умолчанию метод финален по умолчанию @Overrideопциональнаoverrideобязательнополя + геттеры/сеттеры свойства staticcompanion objectнет модификатора = package-private нет модификатора = publicнет аналога internal(видимость в модуле)checked exceptions их нет делегирование вручную byОбязательный
overrideзакрывает два класса ошибок Java: опечатка в имени метода создаёт новый метод вместо переопределения, а добавление метода в базовый класс может случайно «переопределиться» существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции.Отсутствие модификатора означает
public, а не package-private. Это ловушка при переходе: Java-разработчик по привычке не пишет модификатор, рассчитывая на видимость в пакете, и получает публичный API.Сравнить: ООП в Java, Инкапсуляция
[!warning] Подводные камни
openне наследуется транзитивно для метода. Переопределённый метод остаётся открытым, пока его явно не закроют:open class A3 { open fun f() = "A" } open class B3 : A3() { override fun f() = "B" } // всё ещё open! class C3 : B3() { override fun f() = "C" } // можно open class D3 : A3() { final override fun f() = "D" } // так закрывают fun main() { println(listOf(A3(), B3(), C3(), D3()).joinToString { it.f() }) // A, B, C, D }Публичный
val- часть API навсегда. Превратить его вvarможно, а убрать или переименовать - ломающее изменение, потому что клиенты обращаются к нему как к полю.
internalне защищает от Java. В байт-коде такой член становитсяpublicс искажённым именем, поэтому Java-код из другого модуля может его вызвать:class Service3 { internal fun secret() = "виден из Java" } fun main() { println(Service3().secret()) }Отсутствие checked exceptions означает, что сигнатура не говорит об ошибках. Функция может бросить что угодно, и компилятор об этом не предупредит. Способы это компенсировать - в Result и runCatching.
Наследование от
data classневозможно, потому что он не может бытьopen. Иерархии строят наsealed-типах или на интерфейсах.
Далее: Классы и конструкторы