--- title: ООП description: Обзор объектной модели Kotlin: что закрыто по умолчанию, чего нет совсем и почему tags: - kotlin - oop --- Объектная модель Kotlin устроена так же, как в Java - один суперкласс, множество интерфейсов, виртуальные вызовы. Отличия в значениях по умолчанию и в том, что из Java убрано. ## Главное отличие: всё закрыто по умолчанию ```kotlin class User(val name: String) { fun greet() = "Привет, $name" } // class Admin : User("админ") // ошибка: User закрыт для наследования ``` Класс нельзя унаследовать, пока он не объявлен `open`. Метод нельзя переопределить, пока он не `open`. Это противоположность Java, где `final` нужно писать явно. ```kotlin 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` - это явное обещание поддерживать наследование как часть контракта. ## Карта раздела ```mermaid graph TD A["Классы и конструкторы"] --> B["Свойства"] B --> C["Инициализация"] C --> D["Модификаторы видимости"] D --> E["Наследование"] E --> F["Переопределение"] F --> G["Абстрактные классы"] G --> H["Интерфейсы"] H --> I["Делегирование"] I --> J["Вложенные классы"] J --> K["Полиморфизм"] ``` | Заметка | О чём | |---|---| | [Классы и конструкторы](Классы%20и%20конструкторы.md) | основной и вторичные конструкторы, параметры-свойства | | [Свойства](Свойства.md) | геттеры, сеттеры, backing field, `lateinit`, делегаты | | [Инициализация](Инициализация.md) | порядок выполнения `init`, ловушка вызова open-метода | | [Модификаторы видимости](Модификаторы%20видимости.md) | `public`, `internal`, `protected`, `private` | | [Наследование](Наследование.md) | `open`, вызов конструктора базового класса | | [Переопределение](Переопределение.md) | `override`, `final`, `super`, ковариантность результата | | [Абстрактные классы](Абстрактные%20классы.md) | `abstract` против `open`, выбор с интерфейсом | | [Интерфейсы](Интерфейсы.md) | реализация по умолчанию, свойства, ромбовидное наследование | | [Делегирование](Делегирование.md) | `by` для интерфейсов и свойств | | [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md) | `nested` по умолчанию, `inner` для ссылки на внешний | | [Полиморфизм](Полиморфизм.md) | подтипы, перегрузка, дженерики | ## Чего в Kotlin нет | Из Java | Что вместо | |---|---| | `static` | [companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md), функции верхнего уровня | | поля (`field`) в публичном API | [свойства](Свойства.md) с геттером и сеттером | | checked exceptions | все исключения непроверяемые, см. [Система исключений](../Исключения/Система%20исключений.md) | | `package-private` (без модификатора) | `internal` - видимость в пределах модуля | | анонимные классы | [объект-выражение](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) | | примитивы и обёртки как разные типы | один `Int`, упаковка по необходимости | | `wait`/`notify` в `Object` | [корутины](../Корутины/Корутины.md) и примитивы JDK | ## Классы на любой вкус Kotlin предлагает специализированные формы класса вместо одного универсального: ```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` - от реализации синглтона. Подробнее о первых пяти: [Базовые типы ООП](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) ## Свойства вместо полей В Kotlin нет публичных полей - только свойства. Снаружи обращение выглядит как к полю, но всегда идёт через методы доступа: ```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()` везде. Подробнее: [Свойства](Свойства.md) ## Композиция удобнее наследования Kotlin делает композицию такой же короткой, как наследование, - через делегирование: ```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`, не наследуя реализацию: методы просто перенаправляются. Подробнее: [Делегирование](Делегирование.md) ## Что осталось от Java без изменений - одиночное наследование классов, множественная реализация интерфейсов - динамическая диспетчеризация: вызов идёт по фактическому типу объекта - `Any` в корне иерархии (аналог `Object`) - стирание типов в дженериках - модель памяти и сборка мусора JVM То есть байт-код, который порождает Kotlin, - обычные JVM-классы. Любая Java-библиотека работает с Kotlin-классами, и наоборот. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | класс открыт, `final` закрывает | класс закрыт, `open` открывает | > | метод виртуален по умолчанию | метод финален по умолчанию | > | `@Override` опциональна | `override` обязательно | > | поля + геттеры/сеттеры | свойства | > | `static` | `companion object` | > | нет модификатора = package-private | нет модификатора = `public` | > | нет аналога | `internal` (видимость в модуле) | > | checked exceptions | их нет | > | делегирование вручную | `by` | > > **Обязательный `override`** закрывает два класса ошибок Java: опечатка в имени метода создаёт новый метод вместо переопределения, а добавление метода в базовый класс может случайно «переопределиться» существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции. > > **Отсутствие модификатора означает `public`**, а не package-private. Это ловушка при переходе: Java-разработчик по привычке не пишет модификатор, рассчитывая на видимость в пакете, и получает публичный API. > > Сравнить: [ООП в Java](../../Программирование%20на%20Java/ООП/ООП.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) > [!warning] Подводные камни > **`open` не наследуется транзитивно для метода.** Переопределённый метод остаётся открытым, пока его явно не закроют: > ```kotlin > 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-код из другого модуля может его вызвать: > ```kotlin > class Service3 { > internal fun secret() = "виден из Java" > } > > fun main() { println(Service3().secret()) } > ``` > > **Отсутствие checked exceptions означает, что сигнатура не говорит об ошибках.** Функция может бросить что угодно, и компилятор об этом не предупредит. Способы это компенсировать - в [Result и runCatching](../Исключения/Result%20и%20runCatching.md). > > **Наследование от `data class` невозможно**, потому что он не может быть `open`. Иерархии строят на `sealed`-типах или на интерфейсах. Далее: [Классы и конструкторы](Классы%20и%20конструкторы.md)