198 lines
13 KiB
Markdown
198 lines
13 KiB
Markdown
---
|
||
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)
|