Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user