Files
obsidian/Kotlin/ООП/ООП.md
T

198 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)