Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
---
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)