Compare commits
27 Commits
6853ada348
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c513ce326f | |||
| 9361978648 | |||
| 8f42d4a320 | |||
| c654fcd9c2 | |||
| 2ec4a7e7db | |||
| 283660b9ef | |||
| f0817da47e | |||
| c9bc96dafc | |||
| 6538742678 | |||
| a7f8e74f3f | |||
| 9b0c9f4b4a | |||
| b62e54bbb0 | |||
| 4d0e404259 | |||
| 005017595b | |||
| 23a9e06d53 | |||
| 8c95d15f6c | |||
| 61e293f81b | |||
| 75e45eb930 | |||
| 12a473f46e | |||
| 11432e782f | |||
| fec7f50b3e | |||
| 4886625dac | |||
| 977e016822 | |||
| 3764f7dafa | |||
| ecd928d72d | |||
| c0b8ac0043 | |||
| 88b307c311 |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -8,10 +8,15 @@
|
||||
- Устранить элементы "магического мышления" и "карго культа".
|
||||
## Системная инженерия
|
||||
- [Понятие системы](Системная%20инженерия/Понятие%20системы.md)
|
||||
- [Компьютерные системы](Вычислительные%20системы/Компьютерные%20системы.md)
|
||||
- [Компьютерные системы](Системная%20инженерия/Компьютерные%20системы.md)
|
||||
- [Особенности производства аппаратуры](Системная%20инженерия/Особенности%20производства%20аппаратуры.md)
|
||||
- [Hardware and Software](Системная%20инженерия/Hardware%20and%20Software.md)
|
||||
## Схемотехника
|
||||
- [Цифровая схемотехника](Цифровая%20схемотехника/Цифровая%20схемотехника.md)
|
||||
## Теория вычислений
|
||||
- [Модель вычислений](Вычислительные%20системы/Модель%20вычислений.md)
|
||||
- [Процессоры и системы команд](Вычислительные%20системы/Процессоры%20и%20системы%20команд.md)
|
||||
- [Машина фон Неймана](Вычислительные%20системы/Машина%20фон%20Неймана.md)
|
||||
## Вычислительные архитектуры
|
||||
- [Вычислительные архитектуры](Архитектуры/Вычислительные%20архитектуры.md)
|
||||
## Инструменты low-level
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## Учебная архитектура
|
||||
## Реальные VLIW и Эльбрус
|
||||
### Виды параллелизма
|
||||
> [!warning] Эта часть должна переехать в часть лекций
|
||||
1. На уровне инструкций
|
||||
- Конвейризация
|
||||
- Суперскалярность
|
||||
- Длинные машинные слова (very long instruction word)
|
||||
2. На уровне данных
|
||||
- Векторы
|
||||
3. На уровне потоков - многопоточность
|
||||
4. На уровне процессов - много ядер в процессоре
|
||||
### Оптимизация обращений к памяти
|
||||
- Есть спекулятивные команды, по типу спекулятивного чтения. В самом начале процессор читает, а если нигде по итогу не использовалось, то прочитанное значение отваливается.
|
||||
- Оптимизация обращений к памяти в цикле: вместо `load -> alu -> store` компилятор расслаивает итерации и делает `store i -> alu i+1 -> load i+2` (i - номер итерации)
|
||||
### Оптимизация переходов
|
||||
Переход довольно затратная операция для конвейрных процессоров. Виды оптимизаций:
|
||||
1. Штраф за переход и просто все этапы конвейера сбрасываются (самый простой вариант, на практике встречается редко)
|
||||
2. В Эльбрусе - предикаты, выполняются обе ветки (if-else) а потом записывает только то, что в предикате. Превращает Control Flow в Data Flow. Используется только для коротких веток (3-5 команд). Логика: команды сравнения записывают в регистровый файл предикатов 0 или 1. Компилятор ставит абсолютно все команды в поток исполнения (и из if, и из else), но ставит специальные биты в команду: номер предиката, который контроллирует выполнение. Дальше команды исполняются, на этапе декода процессор видит предикат, а на этапе записи в память делает операцию, только если управляющий предикат - 1. (P.S. такое же было и в Intel Itanium)
|
||||
3. Предикт переходов, в cisc - это делает процессор (у него маленькая область видимости, поэтому может быть неэффективно), в vliw - это делает компилятор (компилятор видит всю программу целиком и может отлично с этим справится). Процессор готовится к переходу и загружает команды в конвейер. Эльбрус позволяет подготовавливать до трех переходов одновременно.
|
||||
### Регистровое окно и стек регистров
|
||||
- Процессор содержит несколько сотен регистров, каждая фукнция "заказывает" себе у процессора нужное количество, при этом видимые для функции регистров делятся на три группы: регистры с параметрами, регистры для временных вычислений, регистры с параметрами для вызываемой функции. В момент вызова функции регистровый файл виртуально сдвигается вниз - первая и вторая группы не будут видны вызванной функции. Третья группа становится первой, а новые вторые и третью группы функция снова заказывает.
|
||||
- Если процессору не хватает физических регистров для функции, то процессор сам в фоновом режиме сбрасывает неиспользуемую часть регистрового файла в стек в ОЗУ. При возврате из функции процессор незаметно восстанавливает регистры из стека.
|
||||
- Есть глобальная группа регистров (типичные регистры из cisc, risc)
|
||||
### Безопасность
|
||||
- Несколько стеков: регистры пользователя на одном, адрес возврата и состояние процессора - на другом - защита от buffer overflow и подмены адреса возврата
|
||||
- Процессор различает в памяти указатели и сами данные. В Эльбрусе нельзя создать указатель не из указателя. При этом первичный указатель безопасно получается специальными командами и механизмами в процессоре
|
||||
@@ -0,0 +1,90 @@
|
||||
## Архитектура
|
||||
- 64-битная (процессоры Intel / AMD)
|
||||
- Ассемблер: `NASM`
|
||||
- OC: Linux
|
||||
- Размеры данных:
|
||||
- `byte`
|
||||
- `word` (два байта)
|
||||
- `dword` (4 байта)
|
||||
- `qword` (8 байт)
|
||||
## Регистры общего назначения
|
||||
Все 64-битные регистры начинаются с `R`. Их младшие части можно прочитать отдельно
|
||||
#### База
|
||||
- `RAX` (Accumulator) - аккумулятор, возвращаются результаты функций и номера сисколлов
|
||||
- `RBX` (Base) - обычная переменная
|
||||
- `RCX` (Counter) - счетчик циклов, 4-й аргумент функции, уничтожается сисколлом
|
||||
- `RDX` (Data) - данные, 3-й аргумент функции, второе возвращаемое значение, хранит остаток при делении
|
||||
#### Индексы и указатели
|
||||
- `RDI` (Destination Index) - 1-й аргумент функции
|
||||
- `RSI` (Source Index) - 2-й аргумент фукнции
|
||||
- `RSP` (Stack Pointer) - указатель вершины стека
|
||||
- `RBP` (Base Pointer) - указатель базы стекового кадра
|
||||
#### Новые регистры
|
||||
- `R8, R9` - 5-й и 6-й аргументы функций
|
||||
- `R10, R11` - временные
|
||||
- `R12-R15` - свободные переменные
|
||||
#### Имена частей регистров
|
||||
|
||||
| *64 бита (qword)* | *32 бита (dword)* | *16 бит (word)* | *8 бит (byte)* |
|
||||
| :---------------: | :---------------: | :-------------: | :------------: |
|
||||
| `rax` | `eax` | `ax` | `al` |
|
||||
| `rbx` | `ebx` | `bx` | `bl` |
|
||||
| `rcx` | `ecx` | `cx` | `cl` |
|
||||
| `rdx` | `edx` | `dx` | `dl` |
|
||||
| `rdi` | `edi` | `di` | `dil` |
|
||||
| `rsi` | `esi` | `si` | `sil` |
|
||||
| `rsp` | `esp` | `sp` | `spl` |
|
||||
| `rbp` | `ebp` | `bp` | `bpl` |
|
||||
| `r8..15` | `r8..15d` | `r8..15w` | `r8..15b` |
|
||||
## Соглашение о вызовах
|
||||
1. Аргументы: `RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9`
|
||||
2. Возврат значения: `RAX` и `RDX`
|
||||
3. Сохранность регистров
|
||||
- *Caller-saved* (портятся при вызове `call`): `RAX`, `RCX`, `RDX`, `RSI`, `RDI`, `R8-R11`. Обязанность запоминать их - на вызывающем
|
||||
- *Callee-saved* (обязаны остаться неизменными): `RBX`, `RBP`, `RSP`, `R12-R15`. Обязанность вернуть их в исходное состояние - на функции
|
||||
4. Выравнивание стека
|
||||
- В момент `call` значение регистра `RSP` должно быть кратно 16
|
||||
- При входе в функцию стек сдвинут адресом возврата, поэтому RSP % 16 == 8. Перед внутренним call нужно выровнять стек (сделать push или sub rsp, 8)
|
||||
## Самые нужные сисколлы
|
||||
Все сисколлы [тут](https://gitlab.se.ifmo.ru/programming-languages/cse-programming-languages-fall-2026/main/-/blob/master/docs/system-calls.md)
|
||||
|
||||
| Назначение | RAX | RDI (fd) | RSI (buffer) | RDX (size) | Возврат в RAX |
|
||||
| :----------------------- | :--- | :----------- | :------------- | :---------- | :--------------------------------------------- |
|
||||
| **Чтение (`sys_read`)** | `0` | `0` (stdin) | Адрес в памяти | Кол-во байт | Кол-во прочитанных байт (0 = EOF, <0 = ошибка) |
|
||||
| **Запись (`sys_write`)** | `1` | `1` (stdout) | Адрес в памяти | Кол-во байт | Кол-во записанных байт |
|
||||
| **Выход (`sys_exit`)** | `60` | Код возврата | - | - | Процесс завершается |
|
||||
## Самые нужные инструкции
|
||||
#### Перемещение данных
|
||||
- `mov A, B` - скопировать B в A
|
||||
- `movzx` - скопировать байт B в регистр A и заполнить старшие биты нулями
|
||||
#### Математика
|
||||
- `add A, B` / `sub A, B` - сложение / вычитание `A = A +/- B`
|
||||
- `inc A` / `dec A` - инкремент / декремент
|
||||
- `neg A` - `A = -A`
|
||||
- `imul A, B` - знаковое умножение `A = A * B`
|
||||
- `mul B` - беззнаковое умножение, умножает `RAX` на B. Старшая часть результата в `RDX`, младшая - в `RAX` (результат - `RDX:RAX`)
|
||||
- `div B` - беззнаковое деление. Делит `RDX:RAX` на B. Частное в `RAX`, остаток в `RDX`
|
||||
#### Логика
|
||||
- `and`, `or`, `not` - побитовое И, ИЛИ, НЕ
|
||||
- `xor A, B` - исключающее ИЛИ (`xor rax, rax` - быстрый способ обнулить регистр)
|
||||
- `test A, B` - побитовое И без сохранения результата (только ставит флаги) - лучший способ проверить регистр на ноль
|
||||
#### Ветвление и переходы
|
||||
- `cmp A, B` - сравнивает A и B, ставит флаги
|
||||
- `jmp метка` - безусловный переход
|
||||
- `je` / `jne` (или `jz` / `jnz`) - прыжок, если `Z = 1` / `Z = 0`
|
||||
- Дли знаковых чисел
|
||||
- `jl` / `jle` (Less / Less or Equal) - меньше / меньше или равно
|
||||
- `jg` / `jge` (Greater / Greater or Equal) - больше / больше или равно
|
||||
- `js` / `jns` (Sign) - отрицательное / положительное (флаг `S`)
|
||||
- Для беззнаковых чисел:
|
||||
- `jb` / `jbe` (Below / Below or Equal) - меньше / меньше или равно
|
||||
- `ja / jae` (Above / Above or Equal) - больше / больше или равно
|
||||
- Для переполнений
|
||||
- `jc` / `jnc` (Carry)
|
||||
- `jo` / `joc` (Overflow)
|
||||
#### Работа со стеком
|
||||
Растет как в БЭВМ (в сторону уменьшения адресов)
|
||||
- `push A` - кладет 8 байт на стек (`rsp -= 8, [rsp] = A`)
|
||||
- `pop A` - достает 8 байт со стека (`A = [rsp], rsp += 8`)
|
||||
- `sub rsp, 24` - выделить буфер на 24 байта
|
||||
- `add rsp, 24` - освободить буфер
|
||||
@@ -1,5 +1,9 @@
|
||||
## Инструментарий
|
||||
- [Платформа Wrench](Платформа%20Wrench.md)
|
||||
## Архитектуры
|
||||
- [x86_64](x86_64.md) (реальная архитектура)
|
||||
- [acc32](acc32.md)
|
||||
- [f32a](f32a.md)
|
||||
- [f32a](f32a.md)
|
||||
- [m68k](m68k.md) (будет)
|
||||
- [risc-iv](risc-iv.md) (будет)
|
||||
- [vliw-iv](vliw-iv.md) (в работе, есть интересные факты про Эльбрус)
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
Цвета - 3 партии входных данных. Цифры - на каком такте будут на данном этапе вычисления
|
||||
### Практика
|
||||
На практике все перечисленные механизмы встречаются в современных [компьютерных системах](Компьютерные%20системы.md), в том числе:
|
||||
На практике все перечисленные механизмы встречаются в современных [компьютерных системах](../Системная%20инженерия/Компьютерные%20системы.md), в том числе:
|
||||
- В процессорах общего назначения
|
||||
- В специализированных процессорах
|
||||
- В распределенных системах
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
1. Развитие [Машины Тьюринга](Модель%20вычислений.md#Машина%20Тьюринга). Отличия:
|
||||
- Лента заменена на Random-Access Memory (RAM)
|
||||
- Инструкции и данные объединены
|
||||
2. Призвана быть максимально простой в реализации и производстве
|
||||
3. Процедурное программирование и ООП - тот же принцип
|
||||
|
||||

|
||||
## Особенности
|
||||
1. Использование двоичного кодирования
|
||||
- Встречается и троичное, и двоично-десятичное кодирование
|
||||
2. Программное управление. Команды выполняются последовательно
|
||||
- Последовательность и порядок сегодня условны
|
||||
3. Память компьютера однородно хранит данные и программы
|
||||
- Однородность спорна
|
||||
4. Ячейки памяти компьютера имеют адреса. Random-Access Memory
|
||||
- Адрес не сводится к целому числу сегодня
|
||||
- Память не является пассивным элементом компьютера
|
||||
5. Возможность условного перехода
|
||||
- И перехода по иным причинам
|
||||
## Окружение
|
||||
### (Микро)процессор
|
||||
Цифровая схема, которая выполняет операции с внешним источником данных (обычно с памятью или потоком данных)
|
||||
### (Микро)контроллер
|
||||
Микросхема, предназначенная для управления электронными устройствами. Типичный микроконтроллер сочетает функции процессора и периферийных устройств, содержит ОЗУ и/или ПЗУ
|
||||
### Оперативная память (память)
|
||||
Обычно часть компьютерной памяти, в которой хранится выполняемый машинный код, а также входные, выходные и промежуточные данные
|
||||
## Control Unit и DataPath
|
||||
### Control Unit
|
||||
Компонент центрального процессора (CPU), управляющей работой процессора. CU часто использует двоичный декодер для преобразования закодированных инструкций в управляющие сигналы, координирующие работу остальных блоков
|
||||
### DataPath
|
||||
АЛУ, набор регистров и внутренние шины CPU, по которым данные перемещаются между ними
|
||||
|
||||

|
||||
## Виды инструкций
|
||||
1. Работа с памятью: запись констант, копирование данных (память, регистры, порты I/O)
|
||||
2. Арифметческие, логические и битовые операции
|
||||
3. Управляющие операции: переходы, вызов и возврат из подпрограмм
|
||||
4. Инструкции сопроцессоров: загрузка и выгрузка данных, управление сопроцессором
|
||||
## Роль машинного слова
|
||||
Единица данных, которая выбрана естественной для данной архитектуры процессора
|
||||
1. Может быть адресована в памяти
|
||||
2. Передается по шине между памятью и процессором за один раз
|
||||
3. Является стандартной единицей обрабатываемой информации
|
||||
4. Связанные вопросы: формат инструкции, форматы данных, форматы адресов
|
||||
## На практике
|
||||
1. В чистом виде практически не встречаются
|
||||
2. Влияние на индустрию - трудно переоценить
|
||||
3. Элементы архитектуры присутствуют в подавляющем большинстве процессоров и средств разработки
|
||||
4. Структурное программирование, ООП - наследие фон Неймана
|
||||
5. Ловушка обратной совместимости
|
||||
## Вариации
|
||||
1. Память команд и данных: Принстонская и Гарвардская архитектуры
|
||||
- Типы данных, адресные пространства, машинные слова, теги
|
||||
2. Принцип работы с данными в DataPath
|
||||
- Accumulator, Register-to-Register, Register-to-Memory, Memory-to-Memory, Stack Architectures
|
||||
3. "Мощность" системы команд: CISC, RISC
|
||||
- Количество операндов (0, 1, 2, ...)
|
||||
- Кодирование инструкций
|
||||
- Принципы построения Control Unit
|
||||
4. Адресация операндов
|
||||
Кроме того: ввод-вывод, параллелизм, иерархия памяти и тп
|
||||
@@ -0,0 +1,82 @@
|
||||
MoC - Model of Computation
|
||||
1. MoC предоставляет язык для описания моделей процессов/программ/структур
|
||||
2. MoC определяет возможности вычислительной машины
|
||||
3. MoC характеризует, как исполняется модель (программа): возможные состояния вычислителя, их последовательность, правила переходов (детерменированные или нет)
|
||||
4. MoC, обычно, минималистичны относительно реальных вычислителей
|
||||
## Последовательные модели
|
||||
Sequential models
|
||||
Относительно просто реализуются в виде аппаратных схем
|
||||
### Конечные автоматы
|
||||
|
||||

|
||||
|
||||
Несколько наборов состояний системы и условия переходов между ними
|
||||
#### Автомат с магазинной памятью
|
||||
|
||||

|
||||
|
||||
Например, парсеры
|
||||
#### Достоинства
|
||||
- Таблица состояний
|
||||
- Верифицируемость переходов
|
||||
- Простая реализация в схемотехнике и ПО
|
||||
### Машина Тьюринга
|
||||
Включает:
|
||||
- Неограниченную двустороннюю ленту, разделенную на ячейки
|
||||
- Устройство управления (головка записи-чтения), которое может находиться в конечном числе состояний
|
||||
Управляющее устройство может:
|
||||
- Перемещаться по ленте влево и вправо
|
||||
- Читать и записывать в ячейки символы конечного алфавита
|
||||
Машина Тьюринга
|
||||
1. Не может быть реализована на практике (нет бесконечной ленты)
|
||||
2. Обладает полнотой по Тьюрингу (можно реализовать любой известный алгоритм)
|
||||
3. Проблема остановки
|
||||
4. Данные и управление отделены
|
||||
5. Ориентирована на реализацию
|
||||
### Random Access Machine
|
||||
|
||||

|
||||
## Функциональные модели
|
||||
Functional models
|
||||
Представление вычислительного процесса как совокупности символов и правил их преобразований
|
||||
1. Арифметика
|
||||
2. Лямбда исчисление (полнота по Тьюрингу)
|
||||
3. Combinatory logic
|
||||
4. General recursive functions
|
||||
5. Abstract rewriting systems
|
||||
Программа/данные - математический объект
|
||||
Возможна Тьюринг неполнота
|
||||
### Лямбда исчисление
|
||||
#### 4 вида выражений
|
||||
1. Переменная: $x, y\ \text{и} \ z$
|
||||
2. Константа: $a,b,c$
|
||||
3. Коминация (вызов): $s\ t$
|
||||
4. Абстракция: $\lambda x.s$
|
||||
#### 3 вида редукций/подстановки
|
||||
1. Переименование переменных: $\lambda u.u\ v \xrightarrow{\alpha} \lambda w.w\ v$
|
||||
2. Применение функции (подстановка аргумента): $(\lambda x.s)\ t \xrightarrow{\beta} s[t/x]$
|
||||
3. Сокращение аргумента: $\lambda u.v\ u \xrightarrow{\eta} v$
|
||||
## Параллельные модели
|
||||
Системы, включающие несколько взаимодействующих процессов
|
||||
- Kahn process networks - однонаправленные каналы с неограниченными буферами (+- как в гошке)
|
||||
- Actor model - отправка сообщений в "почтовые ящики" (как в Erlang)
|
||||
- Discrete Event - события в заданные моменты времени (цифровые схемы)
|
||||
- Multi-Thread Model - общая память (как в джаве)
|
||||
- Synchronous Data Flow - синхронизированные потоки данных
|
||||
## Близкие понятия
|
||||
### Парадигмы программирования
|
||||
Способ классификации языков программирования на основе их функций. Языки могут относиться к нескольким парадигмам
|
||||
### Стиль программирования
|
||||
Это набор правил или рекомендации, используемых при написании исходного кода компьютерной программы
|
||||
## Применение на практике
|
||||
1. Computer Science. Формальные модели
|
||||
2. Дизайн ЯПов
|
||||
3. Ограничение творческого порыва, контроль сложности
|
||||
4. Модель-ориентированная инженерия
|
||||
## Model-Driven Engineering
|
||||
1. Разработка через моделирование предметной области
|
||||
2. Автоматическая генерация кода из моделей
|
||||
3. Повышение уровня абстракции при разработке
|
||||
4. Сокращение разрыва между требованиями и реализацией
|
||||
5. Улучшение коммуникации между заинтересованными сторонами
|
||||
6. Снижение сложности разработки масштабных систем
|
||||
@@ -7,7 +7,7 @@
|
||||
### ПЛК
|
||||
- Специальная разновидность электронной вычислительной машины
|
||||
- Особенности
|
||||
1. [Управляющие системы](Управляющие%20системы.md)
|
||||
1. [Управляющие системы](../Системная%20инженерия/Управляющие%20системы.md)
|
||||
2. Законченные устройства для встроенного использования
|
||||
3. Специализация ввода-вывода, модульность
|
||||
4. Выделение инструментальной составляющей
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
## Инорфмационный процессор
|
||||
- Система (электрическая, механическая или биологическая), которая принимает информацию (последовательность символов или состояний) в одной форме и обрабатывает (преобразует) ее в другую форму
|
||||
- Например: исходный сигнал в статистику, посредством алгоритмического процесса
|
||||
- Включает:
|
||||
1. Ввод
|
||||
2. Процессор
|
||||
3. Хранилище
|
||||
4. Вывод
|
||||
## Универсальный процессор
|
||||
Позволяет решать широкий круг задач, настройка которых производится после производства "по месту" или в run-time
|
||||
### Примечания
|
||||
1. Универсальность != полнота по Тьюрингу
|
||||
2. Теоретическая универсальность != практическая универсальность
|
||||
3. Универсальность противоречит эффективности
|
||||
4. Современный процессор - совокупность программного и аппаратного обеспечения
|
||||
### Свойства
|
||||
1. [Hardware and Software](../Системная%20инженерия/Hardware%20and%20Software.md) (двухэтапное производство)
|
||||
2. Полнота по Тьюрингу
|
||||
3. Отсутствие "серьезных" ограничений на "объем" программы
|
||||
4. Изменяемость ПО
|
||||
## Виды процессоров
|
||||
1. Application-Specific Integrated Circuit
|
||||
2. Coarse-Grained Reconfigurable Arrays
|
||||
3. Field-Programmable Gate Array
|
||||
4. Digital Signal Processor
|
||||
5. Graphics Processing Unit
|
||||
6. Central Processing Unit
|
||||
|
||||

|
||||
## Архитектура процессора
|
||||
То, как видит компьютер программист. Определена набором команд (язык), местом нахождения операндов (регистры и память) и вычислительными механизмами (кеш, прерывания и тд)
|
||||
1. Множество разных [архитектур](../Архитектуры/Вычислительные%20архитектуры.md)
|
||||
2. Чтобы понять архитектуру, нужно знать ее язык
|
||||
3. Слова в языке компьютера - "инструкции" или "команды"
|
||||
4. Словарный запас - "система команд"
|
||||
## Микроархитектура процессора
|
||||
Соединение простейших цифровых элементов в логические блоки, предназначенные для выполнения команд определенной архитектуры
|
||||
### Примечания
|
||||
1. Описывает, как в процессоре расположены и соединены друг с другом регистры, АЛУ, конечные автоматы, блоки памяти, интерфейсы ввода-вывода и тп
|
||||
2. У каждой архитектуры может быть много микроархитектур, обеспечивающих разное соотношение производительности, цены, сложности, технической эстетики. Они смогут выполнять одни и те же программы
|
||||
3. Если ISA и микроархитектура отличаются на уровне MoC - обычно требуется уровень виртуализации (ПО, аппаратура, транслятор и тд)
|
||||
## Система команд (ISA)
|
||||
- Instruction Set Architecture
|
||||
- Абстрактная модель процессора, формирующая интерфейс взаимодействия между программным обеспечением и процессором
|
||||
- Производительность, энергопотребление и задержки часто не рассматриваются
|
||||
### Что входит
|
||||
1. Типы данных
|
||||
2. Модель памяти, система и методы адресации
|
||||
3. Набор инструкций
|
||||
4. Механизмы обработки прерываний и исключений
|
||||
5. Методы ввода и вывода
|
||||
### Машинное слово
|
||||
Фрагмент данных фиксированного размера, обрабатываемый как единое целое процессором
|
||||
### Разнообразие систем команд
|
||||
1. Действие над памятью/регистрами
|
||||
- Произвольной сложности и длины (CISC)
|
||||
- Фиксированного формата (RISC)
|
||||
2. Действие без указания обрабатываемых данных (стековый)
|
||||
3. Управляющие сигналы процессора под видом команды (NISC)
|
||||
4. Группа команд под видом одной команды (VLIW)
|
||||
5. Действие над шиной данных (TTA)
|
||||
6. Команды без последовательного исполнения (редукционные)
|
||||
7. Система команд из одной команды (URISC)
|
||||
8. Управляющие сигналы процессоров под видом команды (NITTA)
|
||||
@@ -0,0 +1,82 @@
|
||||
- **Hardware** производится один раз и дальше не меняется, не корректируется
|
||||
- **Software** адаптируется под задачу, легко изменяется
|
||||
- Разделены по времени/специальности: разные дисциплины и разные индустрии
|
||||
## Как разделить HW/SW
|
||||
1. Определяем возможности "конфигурирования":
|
||||
- Заложенные при проектировании - интерфейс задокументирован и стабилен (ISA, API)
|
||||
- Незадокументированные (нецелевое использование, дыра в безопасности) - используется то, что интерфейсом не гарантировано но работает
|
||||
2. Фиксируем интерфейс "конфигурирования"
|
||||
- Набор механизмов
|
||||
- Модель вычислений
|
||||
- Интструментальные средства
|
||||
## Конфигурирование [РЭА](Особенности%20производства%20аппаратуры.md#Монтаж%20радиоэлектронной%20аппаратуры%20(РЭА))
|
||||
1. Макетные платы
|
||||
2. Печатные платы: перерезать, навесить дорожку
|
||||
3. Джамперы
|
||||
4. Платы расширения
|
||||
- Вычислительные платы
|
||||
- Порты ввода/вывода
|
||||
- Dendy (память программ и графическая память)
|
||||
## Реконфигурация. Коммутация
|
||||
### Реконфигурируемые архитектуры
|
||||
Архитектуры, где задача отображается в структуру вычислителя (пространственно, spatial), а не выполняется потоком инструкций. В отличие от ASIC, это структура может быть перенастроена после изготовления
|
||||
1. Программируемые логические интеграционные схемы
|
||||
2. Крупногранулярные реконфигурируемые массивы (CGRA)
|
||||
3. Выстраивание людей для расчета таблиц
|
||||
## Программное управление
|
||||
1. Данные (программы) определяют то, какой прикладной процесс будет разворачиваться
|
||||
2. Количество уровней исполнения/интерпретации данных ограничено только целесообразностью
|
||||
**Временные вычисления** - одна и та же схема последовательно проходит через разные состояния во времени
|
||||
## Аппаратное обеспечение
|
||||
Электронные и механические части вычислительного устройства, входящие в состав системы. Включает: компьютеры и логические устройства, внешние устройства и диагностическую аппаратуру, энергетическое оборудование
|
||||
## Программное обеспечение (ПО)
|
||||
Совокупность программ и программных документов необходимых для эксплуатации. Позволяет аппаратному обеспечению выполнять функции
|
||||
## Аппаратура и ПО != Hardware и Software
|
||||
1. Программная/аппаратная составляющая слабо связаны с SW/HW:
|
||||
- Minix - ОС встроена в Intel ME/CSME
|
||||
- Микропрограммное обеспечение
|
||||
2. Часто аппаратурой называют то, что работает как аппаратное обеспечение
|
||||
- Virutal-Computer, Virtual-Network, Virtual-Volume, etc
|
||||
- Реконфигурируемые архитектуры (ПЛИС, CGRA)
|
||||
3. Разделение на SW/HW зависит в большей степени от способа использования
|
||||
- Hardware - тяжело/долго/дорого поменять
|
||||
- Software - легко/быстро/дешево поменять
|
||||
## Плюсы и минусы ПО/SW
|
||||
### Часть плюсов
|
||||
1. Быстрый цикл разработки, CI/CD и тд
|
||||
2. Меняем у пользователя
|
||||
3. Пользователь - Beta-тестер
|
||||
### Часть минусов
|
||||
1. Высокая сложность программ
|
||||
2. Безопасность
|
||||
3. Культура разработки. Верификация
|
||||
## Типовое HW/SW проектирование
|
||||
### Проектируем аппаратуру
|
||||
- Система команд и вычислительные механизмы
|
||||
- Интерфейсы управления аппаратурой
|
||||
- Цифровая схематехника, система команд
|
||||
### Пишем программу
|
||||
- Прикладная задача
|
||||
- Язык и библиотеки
|
||||
- Система команд и вычислительные механизмы (виртуальная память, прерывания)
|
||||
- Интерфейсы управления аппаратурой
|
||||
### Проблемы
|
||||
1. Излишне шаблонное проектирование
|
||||
2. Высокие интеграционные риски
|
||||
3. Последовательная разработка (сроки)
|
||||
## Hardware/Software CoDesign
|
||||
Мы проектируем систему
|
||||
1. Разработка интерфейса HW/SW (системы команд)
|
||||
2. Разработка моделей HW/SW, для получения обратной связи
|
||||
3. Разработка HW/SW, верификация моделей
|
||||
4. Заменая моделей на HW/SW
|
||||
|
||||

|
||||
|
||||
В итоге: сокращение цикла разработки
|
||||
## Как выбирать инструменты для разработки Software
|
||||
1. От доступного Hardware и инструментальных средств
|
||||
2. От доступной команды разработки
|
||||
3. От прикладной задачи
|
||||
- От области (библиотеки)
|
||||
- От модели вычислений (принципов организации вычислительного процесса: параллелизм, конвейризация, оптимизации и тд)
|
||||
@@ -12,4 +12,4 @@
|
||||
3. Desktops - компьютеры на рабочем месте, почти у всех
|
||||
4. Сервисы - решения прикладных задач, чтобы не делать их локально
|
||||
5. Мобильные вычисления - повсеместно
|
||||
6. Вычисления окружают нас уже повсеместно
|
||||
6. Вычисления окружают нас уже повсеместно
|
||||
@@ -23,7 +23,7 @@
|
||||
Различают:
|
||||
- одно- и двухслойные платы
|
||||
- многослойные платы
|
||||
## Виды монтажа компонентоы
|
||||
## Виды монтажа компонентов
|
||||
- Выводной (в отверстие)
|
||||
- Поверхностный
|
||||
- Встроенный
|
||||
@@ -94,6 +94,7 @@
|
||||
1. Разные частоты процессоров -> разные SKU из одного wafer
|
||||
2. Core Single / Core Duo / Core Quad -> одинаковый кристалл, разное число активных ядер
|
||||
3. Intel Pentium / Celeron -> урезанные Core с отключенными кэшем и функциями
|
||||
## Реальный мир
|
||||
### Производство
|
||||
1. Логистика
|
||||
2. Склады
|
||||
@@ -107,4 +108,13 @@
|
||||
1. Амортизация. Выход оборудования из строя
|
||||
2. Необходимость физического доступа
|
||||
3. Особенности среды эксплуатации
|
||||
4. Долгосрочные эффекты
|
||||
4. Долгосрочные эффекты
|
||||
### Сложность изменений
|
||||
1. Чем совершеннее технология, тем сложнее внести изменения
|
||||
2. Физический контакт
|
||||
### Варианты замены устаревшей аппаратуры
|
||||
1. Перепроектирование на новой элементной базе
|
||||
2. Модульная организация. Стандартные интерфейсы. Имитация старых систем
|
||||
3. Виртуализация
|
||||
|
||||
Как решаем проблемы аппаратуры - [Hardware and Software](Hardware%20and%20Software.md)
|
||||
@@ -57,4 +57,4 @@
|
||||
Множественность точек зрения на систему. Систему может разрушить проблема коммуникации при разработке
|
||||
Формальный способ зафиксировать эти точки зрения и передать их другим - [архитектура](Понятие%20архитектуры.md)
|
||||
## Применение в Computer Science
|
||||
- [Компьютерные системы](../Вычислительные%20системы/Компьютерные%20системы.md)
|
||||
- [Компьютерные системы](Компьютерные%20системы.md)
|
||||
@@ -1,145 +0,0 @@
|
||||
---
|
||||
title: Kotlin
|
||||
description: Полное пособие по Kotlin — синтаксис, ООП, корутины, инструментарий
|
||||
tags:
|
||||
- kotlin
|
||||
---
|
||||
|
||||
Статически типизированный язык от JetBrains, работающий на [JVM](../Программирование%20на%20Java/JVM/JVM.md), а также компилируемый в JavaScript, WebAssembly и нативный код.
|
||||
|
||||
- Первый релиз - 2011, стабильная 1.0 - 2016
|
||||
- Официальный язык разработки для Android с 2019
|
||||
- Полная двусторонняя совместимость с Java: один проект может содержать оба языка
|
||||
- Текущая версия - 2.4.x, компилятор K2 по умолчанию
|
||||
|
||||
Главные отличия от Java: [null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) на уровне типов, [корутины](Корутины/Корутины.md) вместо потоков, выражения вместо инструкций, отсутствие проверяемых исключений, в 2-3 раза меньше кода на ту же задачу.
|
||||
|
||||
## Маршрут
|
||||
Последовательность для изучения с нуля. Каждая заметка заканчивается ссылкой `Далее:`, так что можно просто идти по цепочке.
|
||||
|
||||
1. [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md) - начало
|
||||
2. [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) - ядро системы типов
|
||||
3. [ООП](ООП/ООП.md) - классы и объекты
|
||||
4. [Коллекции](Коллекции/Коллекции.md) - структуры данных
|
||||
5. [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md) - функциональный стиль
|
||||
6. [Корутины](Корутины/Корутины.md) - асинхронность
|
||||
|
||||
## Синтаксис и типы
|
||||
### Базовый синтаксис
|
||||
- [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md)
|
||||
- [Типы данных](Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
|
||||
- [Переменные](Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
|
||||
- [Операторы и выражения](Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md)
|
||||
- [Условные конструкции](Синтаксис%20и%20типы/Базовый%20синтаксис/Условные%20конструкции.md)
|
||||
- [Циклы и диапазоны](Синтаксис%20и%20типы/Базовый%20синтаксис/Циклы%20и%20диапазоны.md)
|
||||
- [Строковые шаблоны и вывод](Синтаксис%20и%20типы/Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md)
|
||||
- [Пакеты и импорты](Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
|
||||
|
||||
### Null-безопасность
|
||||
- [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
|
||||
- [Операторы null-безопасности](Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
|
||||
- [Умные приведения](Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
|
||||
- [Платформенные типы](Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
|
||||
|
||||
### Работа с данными
|
||||
- [Строки](Синтаксис%20и%20типы/Работа%20с%20данными/Строки.md)
|
||||
- [Char](Синтаксис%20и%20типы/Работа%20с%20данными/Char.md)
|
||||
- [Числа и переполнение](Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
|
||||
- [Массивы](Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
|
||||
- [Функции](Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
|
||||
- [Лямбды и функциональные типы](Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
|
||||
- [Деструктуризация](Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
|
||||
- [Приведение типов](Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md)
|
||||
|
||||
### Базовые типы ООП
|
||||
- [Any, Unit, Nothing](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
- [Data class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
- [Enum class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md)
|
||||
- [Sealed class и interface](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
- [Value class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
|
||||
- [Object и companion object](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
- [Typealias](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Typealias.md)
|
||||
- [equals, hashCode и toString](Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
|
||||
## ООП
|
||||
- [ООП](ООП/ООП.md)
|
||||
- [Классы и конструкторы](ООП/Классы%20и%20конструкторы.md)
|
||||
- [Свойства](ООП/Свойства.md)
|
||||
- [Инициализация](ООП/Инициализация.md)
|
||||
- [Модификаторы видимости](ООП/Модификаторы%20видимости.md)
|
||||
- [Наследование](ООП/Наследование.md)
|
||||
- [Переопределение](ООП/Переопределение.md)
|
||||
- [Абстрактные классы](ООП/Абстрактные%20классы.md)
|
||||
- [Интерфейсы](ООП/Интерфейсы.md)
|
||||
- [Делегирование](ООП/Делегирование.md)
|
||||
- [Вложенные и внутренние классы](ООП/Вложенные%20и%20внутренние%20классы.md)
|
||||
- [Полиморфизм](ООП/Полиморфизм.md)
|
||||
|
||||
## Исключения
|
||||
- [Система исключений](Исключения/Система%20исключений.md)
|
||||
- [Result и runCatching](Исключения/Result%20и%20runCatching.md)
|
||||
- [Проверки и контракты](Исключения/Проверки%20и%20контракты.md)
|
||||
|
||||
## Коллекции
|
||||
- [Коллекции](Коллекции/Коллекции.md)
|
||||
- [List, Set, Map](Коллекции/List,%20Set,%20Map.md)
|
||||
- [Изменяемость](Коллекции/Изменяемость.md)
|
||||
- [Операции](Коллекции/Операции.md)
|
||||
- [Sequence](Коллекции/Sequence.md)
|
||||
- [Коллекции под капотом](Коллекции/Коллекции%20под%20капотом.md)
|
||||
|
||||
## Дженерики
|
||||
- [Параметрический полиморфизм](Дженерики/Параметрический%20полиморфизм.md)
|
||||
- [Вариантность](Дженерики/Вариантность.md)
|
||||
- [Проекции типов](Дженерики/Проекции%20типов.md)
|
||||
- [Reified и inline](Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## Функциональный Kotlin
|
||||
- [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
|
||||
- [Inline-функции](Функциональный%20Kotlin/Inline-функции.md)
|
||||
- [Функции-расширения](Функциональный%20Kotlin/Функции-расширения.md)
|
||||
- [Scope-функции](Функциональный%20Kotlin/Scope-функции.md)
|
||||
- [Инфиксные и операторные функции](Функциональный%20Kotlin/Инфиксные%20и%20операторные%20функции.md)
|
||||
- [Receivers и context parameters](Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
|
||||
- [DSL](Функциональный%20Kotlin/DSL.md)
|
||||
- [Паттерны проектирования](Функциональный%20Kotlin/Паттерны%20проектирования.md)
|
||||
|
||||
## Корутины
|
||||
- [Корутины](Корутины/Корутины.md)
|
||||
- [Suspend-функции](Корутины/Suspend-функции.md)
|
||||
- [Структурированная конкурентность](Корутины/Структурированная%20конкурентность.md)
|
||||
- [Билдеры корутин](Корутины/Билдеры%20корутин.md)
|
||||
- [Диспетчеры и контекст](Корутины/Диспетчеры%20и%20контекст.md)
|
||||
- [Отмена и таймауты](Корутины/Отмена%20и%20таймауты.md)
|
||||
- [Исключения в корутинах](Корутины/Исключения%20в%20корутинах.md)
|
||||
- [Flow](Корутины/Flow.md)
|
||||
- [StateFlow и SharedFlow](Корутины/StateFlow%20и%20SharedFlow.md)
|
||||
- [Channels](Корутины/Channels.md)
|
||||
- [Разделяемое состояние](Корутины/Разделяемое%20состояние.md)
|
||||
- [Корутины против потоков](Корутины/Корутины%20против%20потоков.md)
|
||||
|
||||
## Взаимодействие с Java
|
||||
- [Взаимодействие с Java](Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
|
||||
- [Java из Kotlin](Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
|
||||
- [Kotlin из Java](Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
- [Kotlin в байт-коде](Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
## Инструментарий
|
||||
- [Инструментарий](Инструментарий/Инструментарий.md)
|
||||
- [kotlinc](Инструментарий/kotlinc.md)
|
||||
- [Gradle и Kotlin DSL](Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
|
||||
- [Структура проекта](Инструментарий/Структура%20проекта.md)
|
||||
- [Компилятор K2](Инструментарий/Компилятор%20K2.md)
|
||||
- [KSP](Инструментарий/KSP.md)
|
||||
- [Линтеры и формат](Инструментарий/Линтеры%20и%20формат.md)
|
||||
|
||||
## Тестирование
|
||||
- [Тестирование](Тестирование/Тестирование.md)
|
||||
- [kotlin.test и JUnit 5](Тестирование/kotlin.test%20и%20JUnit%205.md)
|
||||
- [Kotest](Тестирование/Kotest.md)
|
||||
- [MockK](Тестирование/MockK.md)
|
||||
- [Тестирование корутин](Тестирование/Тестирование%20корутин.md)
|
||||
|
||||
## Рефлексия
|
||||
- [Рефлексия](Рефлексия/Рефлексия.md)
|
||||
- [Аннотации](Рефлексия/Аннотации.md)
|
||||
@@ -1,385 +0,0 @@
|
||||
---
|
||||
title: Java из Kotlin
|
||||
description: Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи
|
||||
tags:
|
||||
- kotlin
|
||||
- java-interop
|
||||
---
|
||||
|
||||
Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова.
|
||||
|
||||
```kotlin
|
||||
import java.io.File
|
||||
import java.util.ArrayList
|
||||
|
||||
fun main() {
|
||||
val list = ArrayList<String>()
|
||||
list.add("первый")
|
||||
list.add("второй")
|
||||
println(list.size) // 2
|
||||
println(list.joinToString()) // первый, второй
|
||||
|
||||
val tmp = File.createTempFile("demo", ".txt")
|
||||
tmp.writeText("содержимое")
|
||||
println(tmp.readText()) // содержимое
|
||||
tmp.delete()
|
||||
}
|
||||
```
|
||||
Обратите внимание: `joinToString` и `writeText` - это Kotlin-расширения, работающие на Java-типах. Stdlib добавляет сотни таких функций к `java.util` и `java.io`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
## Платформенные типы
|
||||
Главная особенность: тип из Java записывается как `String!` - компилятор не знает, может ли там быть `null`.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
// System.getProperty возвращает String! — может быть null
|
||||
val asNonNull: String = System.getProperty("java.version") // компилируется
|
||||
val asNullable: String? = System.getProperty("нет.такого") // тоже компилируется
|
||||
|
||||
println(asNonNull.isNotEmpty()) // true
|
||||
println(asNullable?.length) // null
|
||||
}
|
||||
```
|
||||
Компилятор разрешает оба варианта, поэтому ответственность на вас. Если Java вернёт `null` там, где объявлен ненулевой тип, проверка сработает в точке присваивания:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val error = runCatching {
|
||||
val value: String = System.getProperty("отсутствует")
|
||||
value.length
|
||||
}.exceptionOrNull()
|
||||
println(error?.javaClass?.simpleName) // NullPointerException
|
||||
}
|
||||
```
|
||||
Сообщение будет вида «Parameter specified as non-null is null» или «null cannot be cast to non-null type» - проверка вставляется компилятором. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
|
||||
|
||||
**Аннотации в Java решают проблему.** Если Java-код аннотирован, платформенных типов не возникает:
|
||||
```java
|
||||
// Java
|
||||
@Nullable public String findName() { return null; } // в Kotlin: String?
|
||||
@NotNull public String getId() { return "1"; } // в Kotlin: String
|
||||
public String plain() { return null; } // в Kotlin: String!
|
||||
```
|
||||
Поддерживаются `org.jetbrains.annotations`, JSR-305 (`javax.annotation`), `androidx.annotation`, Lombok, Eclipse и Checker Framework. Это единственный способ получить настоящую null-безопасность на границе с Java, поэтому в смешанных проектах аннотировать Java-код стоит.
|
||||
|
||||
## Синтетические свойства
|
||||
Java-геттеры и сеттеры видны как свойства Kotlin:
|
||||
```kotlin
|
||||
import java.util.Calendar
|
||||
|
||||
fun main() {
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
cal.timeInMillis = 0L // вместо setTimeInMillis(0)
|
||||
println(cal.timeInMillis) // 0
|
||||
|
||||
val sb = StringBuilder("текст")
|
||||
println(sb.length) // 5 — вместо length()
|
||||
}
|
||||
```
|
||||
Правила распознавания:
|
||||
|
||||
| Java-метод | Свойство в Kotlin |
|
||||
|---|---|
|
||||
| `getValue()` | `value` (только чтение) |
|
||||
| `getValue()` + `setValue(T)` | `value` (чтение и запись) |
|
||||
| `isReady()` | `isReady` (имя сохраняется) |
|
||||
| `setOnly(T)` без геттера | свойства нет, только метод |
|
||||
| `getValue(int index)` | свойства нет - есть параметр |
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val thread = Thread.currentThread()
|
||||
println(thread.name.isNotEmpty()) // true — getName()
|
||||
println(thread.isDaemon) // false — isDaemon() сохраняет префикс
|
||||
println(thread.priority) // 5 — getPriority()
|
||||
}
|
||||
```
|
||||
Синтетические свойства доступны только для Java-методов. Kotlin-функция с именем `getFoo()` свойством не становится.
|
||||
|
||||
## SAM-конверсия
|
||||
Java-интерфейс с одним абстрактным методом принимает лямбду:
|
||||
```kotlin
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
fun main() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
executor.submit { println("как Runnable") } // лямбда -> Runnable
|
||||
val future = executor.submit<String> { "как Callable" } // лямбда -> Callable
|
||||
println(future.get())
|
||||
|
||||
val items = mutableListOf("гамма", "альфа", "бета")
|
||||
items.sortWith { a, b -> a.compareTo(b) } // Comparator
|
||||
println(items)
|
||||
executor.shutdown()
|
||||
// как Runnable
|
||||
// как Callable
|
||||
// [альфа, бета, гамма]
|
||||
}
|
||||
```
|
||||
Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно:
|
||||
```kotlin
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
fun main() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val task = Runnable { println("явная SAM-конструкция") }
|
||||
executor.submit(task)
|
||||
executor.shutdown()
|
||||
Thread.sleep(50)
|
||||
// явная SAM-конструкция
|
||||
}
|
||||
```
|
||||
Важная тонкость: SAM-конверсия работает для **Java**-интерфейсов автоматически. Для Kotlin-интерфейса нужно объявить его как `fun interface`:
|
||||
```kotlin
|
||||
fun interface Validator {
|
||||
fun validate(input: String): Boolean
|
||||
}
|
||||
|
||||
fun check(v: Validator, s: String) = v.validate(s)
|
||||
|
||||
fun main() {
|
||||
println(check({ it.isNotEmpty() }, "текст")) // true
|
||||
}
|
||||
```
|
||||
Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
|
||||
|
||||
## Массивы
|
||||
Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ints: IntArray = intArrayOf(1, 2, 3) // int[]
|
||||
val strings: Array<String> = arrayOf("a", "b") // String[]
|
||||
val boxed: Array<Int> = arrayOf(1, 2) // Integer[]
|
||||
|
||||
println(ints::class.simpleName) // IntArray
|
||||
println(strings::class.simpleName) // Array
|
||||
println(ints.sum()) // 6
|
||||
println(strings.joinToString()) // a, b
|
||||
println(boxed.size) // 2
|
||||
}
|
||||
```
|
||||
`IntArray` компилируется в `int[]` без упаковки, а `Array<Int>` - в `Integer[]`. Это важно для производительности и для совместимости: Java-метод, принимающий `int[]`, не примет `Array<Int>`.
|
||||
|
||||
Передача массива в varargs требует spread-оператора:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val parts = arrayOf("a", "b", "c")
|
||||
println(String.format("%s-%s-%s", *parts)) // a-b-c
|
||||
|
||||
val numbers = intArrayOf(3, 1, 2)
|
||||
println(listOf(*numbers.toTypedArray()).sorted()) // [1, 2, 3]
|
||||
}
|
||||
```
|
||||
`*` распаковывает массив в отдельные аргументы - без него массив передался бы как один элемент.
|
||||
|
||||
Статические методы класса `java.lang.String` через Kotlin-овский `String` недоступны - `String` в Kotlin это `kotlin.String`, у него нет `join`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val parts = listOf("x", "y")
|
||||
println(java.lang.String.join("/", parts)) // x/y
|
||||
println(parts.joinToString("/")) // x/y — идиоматично
|
||||
}
|
||||
```
|
||||
|
||||
## Статические члены
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(Integer.MAX_VALUE) // 2147483647
|
||||
println(Integer.parseInt("42")) // 42
|
||||
println(Math.max(3, 7)) // 7
|
||||
|
||||
val list = java.util.Collections.emptyList<String>()
|
||||
println(list.size) // 0
|
||||
}
|
||||
```
|
||||
Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: `"42".toInt()` вместо `Integer.parseInt`, `maxOf(3, 7)` вместо `Math.max`, `emptyList()` вместо `Collections.emptyList()`.
|
||||
|
||||
## Java-ключевые слова как идентификаторы
|
||||
Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки:
|
||||
```kotlin
|
||||
class JavaLike {
|
||||
fun `is`(value: Int) = value > 0 // в Kotlin `is` — ключевое слово
|
||||
fun `in`(value: Int) = value
|
||||
fun `object`() = "объект"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val j = JavaLike()
|
||||
println(j.`is`(5)) // true
|
||||
println(j.`in`(1)) // 1
|
||||
println(j.`object`()) // объект
|
||||
}
|
||||
```
|
||||
Чаще всего это встречается с методами `is`, `in`, `object`, `when`, `val`, `var`, `fun`, `typealias`. Backticks работают и для имён с пробелами - этим пользуются в названиях тестов. Подробнее: [kotlin.test и JUnit 5](../Тестирование/kotlin.test%20и%20JUnit%205.md)
|
||||
|
||||
## Checked exceptions исчезают
|
||||
```kotlin
|
||||
import java.io.File
|
||||
|
||||
fun main() {
|
||||
// Java-метод объявляет throws IOException, но Kotlin этого не требует
|
||||
val tmp = File.createTempFile("demo", ".txt")
|
||||
tmp.writeText("данные")
|
||||
println(tmp.readText())
|
||||
tmp.delete()
|
||||
// данные
|
||||
}
|
||||
```
|
||||
В Kotlin нет проверяемых исключений: `try/catch` вокруг `createTempFile` не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
|
||||
## Наследование от Java-класса
|
||||
```kotlin
|
||||
import java.io.IOException
|
||||
|
||||
class CustomException(message: String, cause: Throwable? = null) :
|
||||
IOException(message, cause)
|
||||
|
||||
abstract class BaseTask : Runnable {
|
||||
abstract fun execute()
|
||||
override fun run() {
|
||||
try {
|
||||
execute()
|
||||
} catch (e: Exception) {
|
||||
println("ошибка: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FailingTask : BaseTask() {
|
||||
override fun execute() = throw CustomException("задача упала")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
FailingTask().run() // ошибка: задача упала
|
||||
}
|
||||
```
|
||||
Наследование работает в обе стороны без ограничений: Kotlin-класс может расширять Java-класс, реализовывать Java-интерфейс и быть предком Java-класса.
|
||||
|
||||
## Java-типы, которые заменяют на Kotlin-аналоги
|
||||
| Java | Предпочтительно в Kotlin | Почему |
|
||||
|---|---|---|
|
||||
| `java.util.ArrayList` | `mutableListOf()` | короче, тот же класс внутри |
|
||||
| `Integer.parseInt(s)` | `s.toInt()` | расширение, читается слева направо |
|
||||
| `String.valueOf(x)` | `x.toString()` | то же |
|
||||
| `Math.max(a, b)` | `maxOf(a, b)` | работает с любым `Comparable` |
|
||||
| `Collections.sort(list)` | `list.sort()` | расширение |
|
||||
| `Optional<T>` | `T?` | nullable-тип нативен |
|
||||
| `new String[]{...}` | `arrayOf(...)` | короче |
|
||||
| `StringBuilder` | интерполяция или `buildString` | читаемее |
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val fromJava = java.util.ArrayList<String>().apply { add("a") }
|
||||
val idiomatic = mutableListOf("a")
|
||||
println("тот же класс: ${fromJava::class == idiomatic::class}") // тот же класс: true
|
||||
|
||||
println(buildString {
|
||||
append("собрано ")
|
||||
append("через buildString")
|
||||
})
|
||||
// собрано через buildString
|
||||
}
|
||||
```
|
||||
`mutableListOf()` возвращает именно `java.util.ArrayList` - разница только в синтаксисе.
|
||||
|
||||
## Optional
|
||||
```kotlin
|
||||
import java.util.Optional
|
||||
|
||||
fun main() {
|
||||
val present: Optional<String> = Optional.of("значение")
|
||||
val empty: Optional<String> = Optional.empty()
|
||||
|
||||
println(present.orElse("по умолчанию")) // значение
|
||||
println(empty.orElse("по умолчанию")) // по умолчанию
|
||||
|
||||
// перевод в идиоматичный Kotlin
|
||||
val asNullable: String? = present.orElse(null)
|
||||
println(asNullable?.length) // 8
|
||||
println(empty.getOrNull()?.length) // null
|
||||
}
|
||||
|
||||
fun <T> Optional<T>.getOrNull(): T? = orElse(null)
|
||||
```
|
||||
`Optional` в Kotlin-коде избыточен: его роль выполняет `T?`, который не создаёт обёртку и поддержан синтаксисом (`?.`, `?:`, `let`). При получении `Optional` из Java-API его сразу переводят в nullable.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `getName()` | `name` (синтетическое свойство) |
|
||||
> | `isReady()` | `isReady` |
|
||||
> | `new Runnable() { ... }` | `{ ... }` (SAM) |
|
||||
> | `int[]` | `IntArray` |
|
||||
> | `Integer[]` | `Array<Int>` |
|
||||
> | `method(array)` для varargs | `method(*array)` |
|
||||
> | `throws IOException` обязателен | не требуется |
|
||||
> | `Optional<T>` | `T?` |
|
||||
>
|
||||
> **Платформенные типы - компромисс, которого в Java нет.** В чистом Kotlin компилятор гарантирует отсутствие NPE. На границе с Java гарантия исчезает: тип `String!` означает «компилятор не знает». Альтернативой было бы считать все Java-типы nullable - тогда Java-код требовал бы `?.` и `!!` на каждом шагу и стал бы неудобен. Авторы Kotlin выбрали практичность: ответственность за null на границе лежит на программисте.
|
||||
>
|
||||
> **Синтетические свойства работают только для Java.** Это важная асимметрия: Java-метод `getName()` в Kotlin доступен и как `getName()`, и как `.name`, а Kotlin-функция `getName()` - только как функция. Причина в том, что в Kotlin для этого есть настоящие свойства, и превращать функции в свойства означало бы двусмысленность.
|
||||
>
|
||||
> **SAM-конверсия автоматическая только для Java-интерфейсов.** В Java любой интерфейс с одним методом принимает лямбду. В Kotlin - нет: нужен `fun interface`, иначе требуется `object : Interface { }`. Причина - в Kotlin есть функциональные типы (`(String) -> Boolean`), которые покрывают большинство случаев, и SAM-интерфейсы нужны реже.
|
||||
>
|
||||
> **Массивы различаются сильнее, чем кажется.** В Java `int[]` и `Integer[]` - разные типы, но оба «массивы». В Kotlin это `IntArray` и `Array<Int>`, причём `Array<Int>` ковариантен по-другому: `Array<String>` не является `Array<Any>`, тогда как в Java `String[]` является `Object[]` - что и породило `ArrayStoreException`. Kotlin эту дыру закрыл, сделав `Array<T>` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.md)
|
||||
>
|
||||
> **Отсутствие проверяемых исключений - сознательное решение.** Опыт Java показал, что `throws` чаще приводит к `catch (Exception e) { }` и обёрткам в `RuntimeException`, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный `throws`). Цена - потеря документирующей роли и необходимость `@Throws` при вызове из Java.
|
||||
>
|
||||
> Сравнить: [Массивы в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Платформенный тип даёт NPE в неожиданном месте:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val props = System.getProperties()
|
||||
> val error = runCatching {
|
||||
> val missing: String = props.getProperty("нет.такого")
|
||||
> missing.uppercase()
|
||||
> }.exceptionOrNull()
|
||||
> println("NPE при присваивании: ${error is NullPointerException}")
|
||||
> // NPE при присваивании: true
|
||||
> }
|
||||
> ```
|
||||
> Исключение возникает не там, где значение используется, а там, где оно присваивается ненулевому типу. Стектрейс указывает на строку присваивания, а не на Java-метод.
|
||||
>
|
||||
> **`!!` на платформенном типе бессмысленен.** `javaMethod()!!` не добавляет безопасности - проверка и так вставляется. Правильно - `?:` с осмысленным значением по умолчанию или явный тип `String?`.
|
||||
>
|
||||
> **Java-аннотации nullability обязательны для настоящей безопасности.** Без них компилятор Kotlin не знает ничего, и весь Java-API остаётся зоной риска. Это основная причина добавлять `@Nullable`/`@NotNull` в унаследованный Java-код при миграции.
|
||||
>
|
||||
> **Неизменяемая Kotlin-коллекция, переданная в Java, может быть изменена:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val readOnly = listOf("a")
|
||||
> val error = runCatching { (readOnly as MutableList<String>).add("b") }
|
||||
> .exceptionOrNull()
|
||||
> println(error?.javaClass?.simpleName) // UnsupportedOperationException
|
||||
> }
|
||||
> ```
|
||||
> Java-код увидит `java.util.List` с методом `add()` и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через `toMutableList()`, если Java-код изменяет коллекцию.
|
||||
>
|
||||
> **`Array<Int>` и `IntArray` не взаимозаменяемы.** Java-метод с параметром `int[]` требует `IntArray`; `arrayOf(1, 2)` даёт `Array<Int>` (то есть `Integer[]`) и не подойдёт. Конвертация - `toIntArray()`, и она копирует данные.
|
||||
>
|
||||
> **Забытый spread-оператор меняет смысл:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val parts = arrayOf("a", "b")
|
||||
> println(listOf(parts).size) // 1 — массив как один элемент
|
||||
> println(listOf(*parts).size) // 2 — распакованный
|
||||
> }
|
||||
> ```
|
||||
> Компилятор не всегда предупреждает - `listOf(parts)` корректен, просто создаёт `List<Array<String>>`.
|
||||
>
|
||||
> **`getClass()` вызывается как `javaClass`.** Метод `Object.getClass()` в Kotlin недоступен напрямую: он конфликтует с `::class`. Используйте `javaClass` для `java.lang.Class` и `::class` для `KClass`.
|
||||
>
|
||||
> **Статические вложенные Java-классы требуют полного пути.** `Map.Entry` в Kotlin - это `Map.Entry`, но `java.util.AbstractMap.SimpleEntry` нужно импортировать явно; обращение через внешний класс (`AbstractMap.SimpleEntry`) работает только после импорта внешнего.
|
||||
>
|
||||
> **Lombok-геттеры видны, но не всегда.** Lombok генерирует методы на этапе аннотационной обработки Java, и Kotlin-компилятор, работающий первым, их не видит. Нужен плагин `kotlin("plugin.lombok")` и `lombok.config` с `lombok.addLombokGeneratedAnnotation = true`.
|
||||
>
|
||||
> **Java-дженерики с wildcards переводятся в проекции.** `List<? extends Number>` становится `List<out Number>`, `List<? super Integer>` - `List<in Integer>`. Raw-тип `List` без параметра становится `List<*>`, и элементы из него имеют тип `Any?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md)
|
||||
>
|
||||
> **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации.
|
||||
>
|
||||
> **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом.
|
||||
|
||||
Далее: [Kotlin из Java](Kotlin%20из%20Java.md)
|
||||
@@ -1,434 +0,0 @@
|
||||
---
|
||||
title: Kotlin в байт-коде
|
||||
description: Во что компилируются свойства, лямбды, data class, inline, suspend, value class и null-проверки
|
||||
tags:
|
||||
- kotlin
|
||||
- java-interop
|
||||
- jvm
|
||||
---
|
||||
|
||||
Понимание представления Kotlin в байт-коде отвечает на три практических вопроса: почему API выглядит из Java именно так, откуда берутся накладные расходы и что происходит при `NullPointerException` внутри сгенерированного кода.
|
||||
|
||||
Инструмент один - `javap`:
|
||||
```
|
||||
kotlinc Demo.kt -d out # компиляция в .class
|
||||
javap -p -cp out DemoKt # сигнатуры, включая приватные
|
||||
javap -c -p -cp out DemoKt # с байт-кодом
|
||||
javap -v -p -cp out DemoKt # с аннотациями и константным пулом
|
||||
```
|
||||
|
||||
```kotlin
|
||||
fun add(a: Int, b: Int) = a + b
|
||||
|
||||
fun main() {
|
||||
println(add(2, 3)) // 5
|
||||
}
|
||||
```
|
||||
Эта функция компилируется в `public static final int add(int, int)` внутри класса `DemoKt`. Никакой обёртки нет - сигнатура такая же, как у Java-метода.
|
||||
|
||||
## Где живёт код
|
||||
| Конструкция Kotlin | Класс в байт-коде |
|
||||
|---|---|
|
||||
| `class Foo` | `Foo` |
|
||||
| функции верхнего уровня в `Demo.kt` | `DemoKt` |
|
||||
| `object Bar` | `Bar` с полем `INSTANCE` |
|
||||
| `companion object` в `Foo` | `Foo$Companion` + поле `Foo.Companion` |
|
||||
| вложенный класс | `Foo$Inner` |
|
||||
| анонимный объект | `Foo$method$1` |
|
||||
| лямбда | `invokedynamic`, без отдельного класса |
|
||||
| `enum class Color` | `Color extends java.lang.Enum` |
|
||||
|
||||
```kotlin
|
||||
object Registry {
|
||||
private val items = mutableListOf<String>()
|
||||
fun add(item: String) { items += item }
|
||||
fun size() = items.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Registry.add("a")
|
||||
println(Registry.size()) // 1
|
||||
}
|
||||
```
|
||||
`object` становится классом с приватным конструктором, статическим полем `INSTANCE` и статическим инициализатором. Это классический singleton, и его потокобезопасность обеспечена гарантиями JVM на загрузку класса.
|
||||
|
||||
## Свойства
|
||||
Свойство - это всегда приватное поле плюс аксессоры:
|
||||
```kotlin
|
||||
class Holder {
|
||||
var mutable: String = "a"
|
||||
val immutable: Int = 1
|
||||
private var hidden = 2
|
||||
lateinit var late: String
|
||||
|
||||
fun touch() = hidden++
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val h = Holder()
|
||||
h.mutable = "b"
|
||||
println("${h.mutable} ${h.immutable} ${h.touch()}") // b 1 2
|
||||
}
|
||||
```
|
||||
В байт-коде:
|
||||
```
|
||||
private java.lang.String mutable;
|
||||
private final int immutable;
|
||||
private int hidden;
|
||||
public java.lang.String late; // lateinit — поле публично!
|
||||
public final java.lang.String getMutable();
|
||||
public final void setMutable(java.lang.String);
|
||||
public final int getImmutable();
|
||||
public final java.lang.String getLate();
|
||||
public final void setLate(java.lang.String);
|
||||
```
|
||||
Три наблюдения:
|
||||
|
||||
**`val` даёт `final` поле и только геттер.** Это не константа - значение может вычисляться в конструкторе.
|
||||
|
||||
**`lateinit var` делает поле публичным.** Компилятор не может сделать его приватным, потому что геттер должен различать «не инициализировано» и «значение `null`» - для этого нужна прямая работа с полем, а `lateinit` исключает nullable-тип. Побочный эффект: поле видно из Java напрямую.
|
||||
|
||||
**Приватное свойство аксессоров не получает** - доступ идёт к полю, если нет кастомного геттера.
|
||||
|
||||
Свойство с кастомным геттером поля не создаёт вообще:
|
||||
```kotlin
|
||||
class Rect(val w: Int, val h: Int) {
|
||||
val area: Int get() = w * h
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Rect(3, 4).area) // 12
|
||||
}
|
||||
```
|
||||
`area` - только метод `getArea()`, считающий значение каждый раз. Подробнее: [Свойства](../ООП/Свойства.md)
|
||||
|
||||
## Null-проверки
|
||||
Для каждого ненулевого параметра публичной функции компилятор вставляет проверку:
|
||||
```kotlin
|
||||
fun nn(s: String) = s.length
|
||||
fun nullable(s: String?) = s?.length
|
||||
|
||||
fun main() {
|
||||
println(nn("текст")) // 5
|
||||
println(nullable(null)) // null
|
||||
}
|
||||
```
|
||||
Байт-код `nn`:
|
||||
```
|
||||
0: aload_0
|
||||
1: ldc #9 // String s
|
||||
3: invokestatic #15 // Intrinsics.checkNotNullParameter:(Object;String)V
|
||||
6: aload_0
|
||||
7: invokevirtual #21 // String.length:()I
|
||||
10: ireturn
|
||||
```
|
||||
`checkNotNullParameter` бросает `NullPointerException` с сообщением «Parameter specified as non-null is null» - это и есть защита границы от Java-кода. Для `nullable` проверки нет, зато есть ветвление и упаковка в `Integer`:
|
||||
```
|
||||
public static final java.lang.Integer nullable(java.lang.String);
|
||||
```
|
||||
Возвращаемый тип `Int?` стал `Integer` - nullable-примитив всегда упакован.
|
||||
|
||||
Информация о nullability хранится в аннотациях:
|
||||
```
|
||||
public static final int nn(java.lang.String); @NotNull на параметре
|
||||
public static final java.lang.Integer nullable(...); @Nullable на методе и параметре
|
||||
```
|
||||
Это `org.jetbrains.annotations.@NotNull`/`@Nullable` с `RuntimeInvisible`-удержанием. Их читают Java-IDE и другие JVM-языки. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["String в Kotlin"] --> B["java.lang.String<br/>+ @NotNull<br/>+ checkNotNullParameter"]
|
||||
C["String? в Kotlin"] --> D["java.lang.String<br/>+ @Nullable<br/>без проверки"]
|
||||
E["Int в Kotlin"] --> F["int"]
|
||||
G["Int? в Kotlin"] --> H["java.lang.Integer"]
|
||||
```
|
||||
|
||||
## data class
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val p = Point(1, 2)
|
||||
println(p) // Point(x=1, y=2)
|
||||
println(p.copy(y = 5)) // Point(x=1, y=5)
|
||||
println(p == Point(1, 2)) // true
|
||||
}
|
||||
```
|
||||
Генерируется:
|
||||
```
|
||||
private final int x;
|
||||
private final int y;
|
||||
public Point(int, int);
|
||||
public final int getX();
|
||||
public final int getY();
|
||||
public final int component1();
|
||||
public final int component2();
|
||||
public final Point copy(int, int);
|
||||
public static Point copy$default(Point, int, int, int, java.lang.Object);
|
||||
public java.lang.String toString();
|
||||
public int hashCode();
|
||||
public boolean equals(java.lang.Object);
|
||||
```
|
||||
`component1()`/`component2()` нужны для деструктуризации - компилятор вызывает их по позиции. `copy$default` реализует значения по умолчанию: четвёртый параметр `int` - битовая маска переданных аргументов, пятый `Object` - технический. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
## Значения по умолчанию
|
||||
```kotlin
|
||||
class Service {
|
||||
fun call(url: String, retries: Int = 3, timeout: Long = 1000L) =
|
||||
"$url r=$retries t=$timeout"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service().call("/api")) // /api r=3 t=1000
|
||||
}
|
||||
```
|
||||
Два метода вместо одного:
|
||||
```
|
||||
public final java.lang.String call(java.lang.String, int, long);
|
||||
public static java.lang.String call$default(Service, java.lang.String, int, long, int, java.lang.Object);
|
||||
```
|
||||
`$default` получает битовую маску: бит `i` установлен, если аргумент `i` **не** передан. Внутри он подставляет значение по умолчанию для каждого отсутствующего аргумента и вызывает основной метод. Отсюда два следствия: вызов со значениями по умолчанию стоит на один вызов дороже, а из Java `$default` недоступен (он синтетический и требует знания маски).
|
||||
|
||||
## Лямбды и функциональные типы
|
||||
```kotlin
|
||||
fun noCapture(): () -> Int = { 42 }
|
||||
fun withCapture(n: Int): () -> Int = { n }
|
||||
|
||||
fun main() {
|
||||
println(noCapture()()) // 42
|
||||
println(withCapture(7)()) // 7
|
||||
}
|
||||
```
|
||||
Функциональный тип `() -> Int` - это интерфейс `kotlin.jvm.functions.Function0<Integer>`. Их 23: `Function0` ... `Function22`.
|
||||
```
|
||||
public static final kotlin.jvm.functions.Function0<java.lang.Integer> noCapture();
|
||||
0: invokedynamic #81, 0 // InvokeDynamic #0:invoke:()Lkotlin/jvm/functions/Function0;
|
||||
```
|
||||
Начиная с K2 лямбды компилируются через `invokedynamic` (как в Java), а не в отдельные классы `Foo$lambda$1`. Это уменьшает размер артефакта и ускоряет загрузку, но означает, что имя класса лямбды генерируется в рантайме - в стектрейсах появляются `$$Lambda$...`.
|
||||
|
||||
Важная деталь: `Function0<Integer>` работает с **упакованным** `Integer`. Лямбда, возвращающая `Int`, упаковывает результат на каждом вызове - если это горячий цикл, нужна `inline`-функция. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
|
||||
|
||||
## inline
|
||||
```kotlin
|
||||
inline fun measure(block: () -> Unit) {
|
||||
block()
|
||||
}
|
||||
|
||||
fun useInline() {
|
||||
measure { println("тело") }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
useInline() // тело
|
||||
}
|
||||
```
|
||||
Байт-код `useInline` не содержит вызова `measure` вообще:
|
||||
```
|
||||
public static final void useInline();
|
||||
0: iconst_0
|
||||
1: istore_0
|
||||
2: iconst_0
|
||||
3: istore_1
|
||||
4: ldc #49 // String тело
|
||||
6: getstatic #55 // System.out
|
||||
9: swap
|
||||
10: invokevirtual #61 // println
|
||||
13: nop
|
||||
14: nop
|
||||
15: return
|
||||
```
|
||||
Тело лямбды подставлено прямо в метод: ни объекта `Function0`, ни вызова по ссылке. Это основа `forEach`, `let`, `map` и остальных операций stdlib - они не создают объектов. Цена - код дублируется в каждой точке вызова. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
|
||||
|
||||
Сама `measure` всё равно существует как метод (для Java-клиентов и рефлексии):
|
||||
```
|
||||
public static final void measure(kotlin.jvm.functions.Function0<kotlin.Unit>);
|
||||
```
|
||||
|
||||
## suspend
|
||||
```kotlin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
suspend fun fetch(id: Int): String {
|
||||
delay(10)
|
||||
return "ответ $id"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(fetch(1)) // ответ 1
|
||||
}
|
||||
```
|
||||
Сигнатура меняется радикально:
|
||||
```
|
||||
public static final java.lang.Object fetch(int, kotlin.coroutines.Continuation<? super java.lang.String>);
|
||||
```
|
||||
Три изменения: добавлен параметр `Continuation`, тип возврата стал `Object` (вместо `String`), и само тело превратилось в конечный автомат. Функция может вернуть либо результат, либо специальный маркер `COROUTINE_SUSPENDED`, означающий «я приостановилась, результат придёт через `Continuation`».
|
||||
|
||||
Конечный автомат - это анонимный класс с полем `label`, хранящим номер точки приостановки, и полями для всех локальных переменных, живущих через приостановку. При возобновлении функция вызывается снова, читает `label` и прыгает в нужную ветку `switch`. Это CPS-преобразование (continuation-passing style), выполняемое компилятором.
|
||||
|
||||
Отсюда свойства корутин: приостановка не блокирует поток (нет никакого потока - есть объект состояния), стектрейсы короткие, а Java-код вызвать `suspend`-функцию практически не может. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md)
|
||||
|
||||
## value class
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class UserId(val value: Int)
|
||||
|
||||
fun process(id: UserId) = "обработан ${id.value}"
|
||||
|
||||
fun main() {
|
||||
println(process(UserId(42))) // обработан 42
|
||||
}
|
||||
```
|
||||
Класс `UserId` в байт-коде существует, но в сигнатурах заменяется на обёрнутый тип:
|
||||
```
|
||||
public static final java.lang.String process-4ZD5Yi0(int);
|
||||
```
|
||||
Параметр стал `int` - обёртки в рантайме нет. Суффикс `-4ZD5Yi0` (хеш от типов) нужен, чтобы `process(UserId)` и `process(OrderId)` не конфликтовали: после стирания у обеих сигнатура `process(int)`.
|
||||
|
||||
Обёртка всё-таки создаётся в трёх случаях: когда value-класс используется как дженерик-параметр (`List<UserId>`), как nullable (`UserId?`) и при обращении через супертип. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
|
||||
|
||||
## Делегированные свойства
|
||||
```kotlin
|
||||
class Config {
|
||||
val expensive: String by lazy { "вычислено" }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config().expensive) // вычислено
|
||||
}
|
||||
```
|
||||
```
|
||||
private final kotlin.Lazy v$delegate;
|
||||
public final java.lang.String getV();
|
||||
```
|
||||
Поле хранит не значение, а делегат. Геттер вызывает `delegate.getValue()`. Поэтому каждое делегированное свойство - это дополнительный объект и один лишний вызов на чтение. Для `by lazy` с дефолтным режимом внутри ещё и `synchronized`-блок при первом доступе. Подробнее: [Делегирование](../ООП/Делегирование.md)
|
||||
|
||||
## Дженерики и вариантность
|
||||
```kotlin
|
||||
class Box<T : Comparable<T>>(val value: T) {
|
||||
fun max(other: T) = if (value > other) value else other
|
||||
}
|
||||
|
||||
fun sizes(l: List<out Number>, m: MutableList<in Int>) = l.size + m.size
|
||||
|
||||
fun main() {
|
||||
println(Box(3).max(7)) // 7
|
||||
println(sizes(listOf(1, 2), mutableListOf(3))) // 3
|
||||
}
|
||||
```
|
||||
```
|
||||
public final class Box<T extends java.lang.Comparable<? super T>>
|
||||
public static final int sizes(java.util.List<? extends java.lang.Number>, java.util.List<? super java.lang.Integer>);
|
||||
```
|
||||
Проекции Kotlin становятся wildcards Java: `out T` → `? extends T`, `in T` → `? super T`. Дженерики стираются так же, как в Java - в рантайме `Box<Int>` и `Box<String>` неразличимы. Обойти это позволяет только `reified` в `inline`-функциях, где тип подставляется при компиляции. Подробнее: [Вариантность](../Дженерики/Вариантность.md), [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## Nothing и Unit
|
||||
```kotlin
|
||||
fun fail(): Nothing = throw IllegalStateException("всегда падает")
|
||||
fun act(): Unit = println("действие")
|
||||
|
||||
fun main() {
|
||||
act() // действие
|
||||
println(runCatching { fail() }.isFailure) // true
|
||||
}
|
||||
```
|
||||
`Unit` как возвращаемый тип становится `void`. `Nothing` - `java.lang.Void` (значение которого всегда `null`, потому что функция никогда не возвращается нормально). В обоих случаях Kotlin-компилятор знает больше, чем байт-код: `Nothing` делает код после вызова недостижимым, но Java-компилятор этого не увидит. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
## Метаданные
|
||||
Каждый Kotlin-класс несёт аннотацию `kotlin.Metadata` с закодированной информацией о nullable-типах, значениях по умолчанию, свойствах, `suspend`, вариантности, `inline`, `sealed` и прочем. Именно её читает Kotlin-компилятор, подключая скомпилированную библиотеку: `.class`-файла недостаточно, чтобы восстановить Kotlin-API.
|
||||
|
||||
Практические следствия:
|
||||
|
||||
**Обфускация ломает Kotlin-interop.** ProGuard/R8 должны сохранять `kotlin.Metadata`, иначе библиотека перестанет быть пригодной для Kotlin-клиентов (но останется пригодной для Java).
|
||||
|
||||
**Рефлексия Kotlin требует `kotlin-reflect`.** `KClass.memberProperties` читает метаданные, и для этого нужна отдельная зависимость весом ~2 МБ. Java-рефлексия (`javaClass.declaredFields`) работает без неё, но видит только байт-код. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
|
||||
|
||||
**Java-библиотека не знает про Kotlin-типы.** Jackson, Hibernate и подобные видят только `.class`: конструктор со значениями по умолчанию, nullable-поле как обычное. Для корректной работы нужны модули-адаптеры (`jackson-module-kotlin`), которые читают `kotlin.Metadata`.
|
||||
|
||||
## Сводная таблица
|
||||
| Kotlin | Байт-код |
|
||||
|---|---|
|
||||
| `fun f()` на верхнем уровне | `static final` в `ФайлKt` |
|
||||
| `object O` | класс + `static final O INSTANCE` |
|
||||
| `companion object` | класс `Cls$Companion` + поле |
|
||||
| `val x: T` | `private final T x` + `getX()` |
|
||||
| `var x: T` | `private T x` + `getX()`/`setX()` |
|
||||
| `lateinit var x` | `public T x` (поле публично) |
|
||||
| `val x get() = ...` | только `getX()`, поля нет |
|
||||
| `x: String` (параметр) | `String` + `@NotNull` + `checkNotNullParameter` |
|
||||
| `x: String?` | `String` + `@Nullable` |
|
||||
| `Int` | `int` |
|
||||
| `Int?` | `java.lang.Integer` |
|
||||
| `() -> Int` | `Function0<Integer>` |
|
||||
| лямбда | `invokedynamic` |
|
||||
| `inline fun` | тело подставлено + метод для Java |
|
||||
| `suspend fun f(): T` | `Object f(Continuation)` + автомат |
|
||||
| `@JvmInline value class` | обёрнутый тип + суффикс в имени |
|
||||
| `by lazy` | поле `x$delegate: Lazy` |
|
||||
| `out T` / `in T` | `? extends T` / `? super T` |
|
||||
| `Unit` | `void` |
|
||||
| `Nothing` | `java.lang.Void` |
|
||||
| `internal fun f()` | `public final f$module()` |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | исходник ≈ байт-код | есть зазор, есть метаданные |
|
||||
> | поля публичны, если так написано | поля всегда приватны (кроме `lateinit`) |
|
||||
> | геттер пишется руками | генерируется |
|
||||
> | лямбда → `invokedynamic` | то же (с K2) |
|
||||
> | нет значений по умолчанию | синтетический `$default` |
|
||||
> | нет `inline` | тело подставляется |
|
||||
> | `async` через библиотеку | `suspend` → конечный автомат |
|
||||
> | `int` и `Integer` в коде | `Int` и `Int?` |
|
||||
>
|
||||
> **Главное отличие - наличие метаданных.** В Java `.class` полностью описывает API: что видно в сигнатурах, то и есть. В Kotlin часть системы типов (nullability, значения по умолчанию, `suspend`, `inline`, `sealed`, вариантность) существует только в аннотации `kotlin.Metadata`. Это значит, что Kotlin-компилятор и Java-компилятор видят **разные** API одной библиотеки: первый богаче. Отсюда и необходимость сохранять метаданные при обфускации, и зависимость `kotlin-reflect`, и адаптеры для Jackson.
|
||||
>
|
||||
> **Проверки на null стоят вызова метода на каждый параметр.** В Java такой проверки нет вообще - `NullPointerException` возникает при первом использовании. Kotlin переносит сбой на границу функции, что делает диагностику проще (понятно, какой параметр виноват), но добавляет `invokestatic` на каждый ненулевой параметр публичной функции. В приватных и `inline`-функциях проверок нет: там компилятор уже гарантировал тип.
|
||||
>
|
||||
> **`inline` - возможность, которой в Java нет на уровне языка.** JIT в Java тоже встраивает вызовы, но это решение рантайма, непредсказуемое и не применимое к лямбдам с захватом. Kotlin встраивает на этапе компиляции, гарантированно и вместе с телом лямбды. Это и позволяет `collection.map { }` не создавать объектов, и даёт `reified`, и делает возможными `return` из лямбды. Цена - рост байт-кода и невозможность изменить тело без перекомпиляции клиентов.
|
||||
>
|
||||
> **`suspend` реализован компилятором, а не рантаймом.** В Java асинхронность требует либо потоков, либо `CompletableFuture` с цепочками колбэков, либо виртуальных потоков (Loom), которые меняют JVM. Kotlin делает CPS-преобразование на этапе компиляции и работает на любой JVM 8+. Следствие: корутины не требуют поддержки платформы, но и не интегрируются с Java-кодом.
|
||||
>
|
||||
> **Value-классы ближе к будущим Project Valhalla, чем к Java сегодня.** В Java любая обёртка над `int` - это объект в куче. Kotlin убирает обёртку на этапе компиляции, заменяя тип в сигнатурах. Цена - манглирование имён и обёртка в дженериках, где стирание не позволяет обойтись примитивом.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`lateinit var` создаёт публичное поле.** Это нарушает инкапсуляцию неожиданным образом: Java-код и рефлексия получают прямой доступ в обход геттера, минуя проверку инициализации. Если это важно, используйте `var x: T? = null` с приватным сеттером.
|
||||
>
|
||||
> **Nullable-примитив всегда упакован:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val plain: Int = 1
|
||||
> val nullable: Int? = 1
|
||||
> println(plain == nullable) // true
|
||||
> println(nullable!!::class.java.name) // java.lang.Integer
|
||||
> val a: Int? = 1000
|
||||
> val b: Int? = 1000
|
||||
> println("identity: ${a === b}") // identity: false
|
||||
> }
|
||||
> ```
|
||||
> `Int?` это `Integer`, поэтому `===` на значениях вне кеша `-128..127` даёт `false`. В горячем коде `Int?` в коллекциях и лямбдах создаёт объекты. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
|
||||
>
|
||||
> **Значения по умолчанию стоят лишнего вызова.** `call("/api")` идёт через `call$default`, который разбирает битовую маску и вызывает `call`. Для горячего пути с миллионами вызовов разница измерима - там пишут явные перегрузки.
|
||||
>
|
||||
> **`inline` раздувает байт-код.** Большая `inline`-функция, вызванная в сотне мест, копируется сто раз. Компилятор предупреждает про «expected performance impact... insignificant» для функций без лямбда-параметров - это намёк, что `inline` там не нужен.
|
||||
>
|
||||
> **Изменение тела `inline`-функции требует перекомпиляции клиентов.** Старое тело уже вкомпилировано в их байт-код. Для публичных библиотек это серьёзное ABI-ограничение: `inline` в публичном API фиксирует реализацию.
|
||||
>
|
||||
> **Делегированное свойство - это объект и вызов.** `by lazy` на каждом из сотни свойств даёт сотню объектов `Lazy` и сотню косвенных вызовов. Для простых случаев дешевле `private var x: T? = null` с проверкой в геттере.
|
||||
>
|
||||
> **Стектрейс корутины не показывает путь вызова.** Конечный автомат возобновляется из диспетчера, поэтому в стектрейсе видна только текущая точка приостановки. Для отладки включают `-Dkotlinx.coroutines.debug` или зависимость `kotlinx-coroutines-debug`. Подробнее: [Корутины](../Корутины/Корутины.md)
|
||||
>
|
||||
> **Обфускация без правил для `kotlin.Metadata` ломает библиотеку для Kotlin-клиентов.** Код продолжит работать, но исчезнет nullability, пропадут значения по умолчанию, `kotlin-reflect` перестанет видеть свойства. В правилах R8 нужно `-keep class kotlin.Metadata`.
|
||||
>
|
||||
> **`javap` не показывает Kotlin-API.** Он читает байт-код, не метаданные: `suspend`-функция выглядит как метод с `Continuation`, а `data class` - как обычный класс с кучей методов. Чтобы увидеть Kotlin-вид скомпилированного кода, нужен `kotlinp` или IDE («Show Kotlin Bytecode» → «Decompile»).
|
||||
>
|
||||
> **Синтетические методы видны рефлексии.** `component1()`, `copy$default`, `access$...`, `Foo$Companion` - всё это попадёт в `getDeclaredMethods()`. Код, перебирающий методы класса (сериализаторы, валидаторы), должен фильтровать по `isSynthetic()`.
|
||||
>
|
||||
> **`internal` не защищает.** Манглированное имя `helper$module` публично. Java-код в том же артефакте вызовет его без препятствий, как и любая рефлексия.
|
||||
>
|
||||
> **Проверки `checkNotNullParameter` можно отключить, но не стоит.** Флаг `-Xno-param-assertions` убирает их, давая небольшой выигрыш. Цена - `NullPointerException` будет возникать глубоко внутри вместо границы функции, и диагностика станет заметно хуже.
|
||||
|
||||
Далее: [Инструментарий](../Инструментарий/Инструментарий.md)
|
||||
@@ -1,525 +0,0 @@
|
||||
---
|
||||
title: Kotlin из Java
|
||||
description: Аннотации @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws и то, как конструкции Kotlin выглядят из Java
|
||||
tags:
|
||||
- kotlin
|
||||
- java-interop
|
||||
---
|
||||
|
||||
Обратное направление сложнее: в Kotlin есть конструкции, которых нет в JVM, и компилятор представляет их по-своему. Без аннотаций ваш API окажется неудобным для Java-кода.
|
||||
|
||||
```kotlin
|
||||
class Greeter(val name: String) {
|
||||
fun greet(greeting: String = "Привет") = "$greeting, $name!"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Greeter("Мир").greet()) // Привет, Мир!
|
||||
}
|
||||
```
|
||||
Из Java этот же класс выглядит так:
|
||||
```java
|
||||
// Java
|
||||
Greeter g = new Greeter("Мир");
|
||||
System.out.println(g.greet("Привет")); // параметр обязателен!
|
||||
// g.greet(); // не скомпилируется
|
||||
```
|
||||
Значение по умолчанию существует только для Kotlin: в байт-коде есть `greet(String)` и синтетический `greet$default(Greeter, String, int, Object)`, вызывать который вручную нельзя.
|
||||
|
||||
## Карта проблем и аннотаций
|
||||
| Конструкция Kotlin | Из Java без аннотации | Аннотация |
|
||||
|---|---|---|
|
||||
| значение по умолчанию | один метод со всеми параметрами | `@JvmOverloads` |
|
||||
| `companion object` | `Cls.Companion.method()` | `@JvmStatic` |
|
||||
| `object` | `Singleton.INSTANCE.method()` | `@JvmStatic` |
|
||||
| `val` в `companion object` | `Cls.Companion.getX()` | `@JvmField` |
|
||||
| `const val` | уже поле - ничего не нужно | - |
|
||||
| функция верхнего уровня | `FileNameKt.fn()` | `@JvmName` на файле |
|
||||
| свойство | `getX()` / `setX()` | `@get:JvmName` |
|
||||
| `suspend fun` | лишний параметр `Continuation` | не решается |
|
||||
| `value class` | манглированное имя | не решается |
|
||||
| исключение | не видно в `throws` | `@Throws` |
|
||||
| перегрузки с дженериками | конфликт стирания | `@JvmName` |
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Kotlin API"] --> B{"Вызывается из Java?"}
|
||||
B -->|"нет"| C["аннотации не нужны"]
|
||||
B -->|"да"| D["проверить каждый пункт"]
|
||||
D --> E["значения по умолчанию<br/>@JvmOverloads"]
|
||||
D --> F["companion / object<br/>@JvmStatic"]
|
||||
D --> G["константы<br/>@JvmField или const"]
|
||||
D --> H["исключения<br/>@Throws"]
|
||||
D --> I["имена<br/>@JvmName"]
|
||||
```
|
||||
|
||||
## @JvmOverloads
|
||||
Генерирует перегрузку для каждого значения по умолчанию:
|
||||
```kotlin
|
||||
class Greeter(val name: String) {
|
||||
@JvmOverloads
|
||||
fun greet(greeting: String = "Привет", punct: String = "!") = "$greeting, $name$punct"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val g = Greeter("Мир")
|
||||
println(g.greet()) // Привет, Мир!
|
||||
println(g.greet("Здравствуй")) // Здравствуй, Мир!
|
||||
println(g.greet("Hi", ".")) // Hi, Мир.
|
||||
}
|
||||
```
|
||||
В байт-коде появляются три метода:
|
||||
```
|
||||
public final String greet(String, String) // полный
|
||||
public final String greet(String) // punct по умолчанию
|
||||
public final String greet() // оба по умолчанию
|
||||
```
|
||||
Перегрузки создаются **справа налево**: параметры отбрасываются с конца. Поэтому `greet(punct = ".")` из Java невозможно - такой перегрузки нет. Это ограничение влияет на порядок параметров: самые часто переопределяемые ставьте первыми.
|
||||
|
||||
Для конструктора аннотация работает так же:
|
||||
```kotlin
|
||||
class Config @JvmOverloads constructor(
|
||||
val host: String = "localhost",
|
||||
val port: Int = 8080,
|
||||
) {
|
||||
override fun toString() = "$host:$port"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config()) // localhost:8080
|
||||
println(Config("example.com")) // example.com:8080
|
||||
println(Config("example.com", 443)) // example.com:443
|
||||
}
|
||||
```
|
||||
|
||||
## @JvmStatic
|
||||
Без неё члены `companion object` доступны только через `.Companion`:
|
||||
```kotlin
|
||||
class Repository private constructor(val id: String) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(id: String) = Repository(id)
|
||||
|
||||
fun createNoStatic(id: String) = Repository(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Repository.create("1").id) // 1
|
||||
println(Repository.createNoStatic("2").id) // 2
|
||||
}
|
||||
```
|
||||
Из Kotlin обе формы одинаковы. Из Java - нет:
|
||||
```java
|
||||
// Java
|
||||
Repository.create("1"); // работает: статический метод
|
||||
Repository.Companion.createNoStatic("2"); // без @JvmStatic только так
|
||||
```
|
||||
То же для `object`:
|
||||
```kotlin
|
||||
object Registry {
|
||||
private val items = mutableListOf<String>()
|
||||
|
||||
@JvmStatic
|
||||
fun register(item: String) { items += item }
|
||||
|
||||
fun size() = items.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Registry.register("a")
|
||||
println(Registry.size()) // 1
|
||||
}
|
||||
```
|
||||
Из Java: `Registry.register("a")` против `Registry.INSTANCE.size()`.
|
||||
|
||||
## @JvmField и const
|
||||
Свойство в `companion object` компилируется в приватное поле с геттером на `Companion`. Три варианта:
|
||||
```kotlin
|
||||
class Limits {
|
||||
companion object {
|
||||
const val MAX = 100 // примитив/String, compile-time
|
||||
@JvmField val DEFAULT = "значение" // поле, доступно напрямую
|
||||
val COMPUTED = "вычисляется".uppercase() // только через геттер
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Limits.MAX) // 100
|
||||
println(Limits.DEFAULT) // значение
|
||||
println(Limits.COMPUTED) // ВЫЧИСЛЯЕТСЯ
|
||||
}
|
||||
```
|
||||
Из Java:
|
||||
```java
|
||||
// Java
|
||||
Limits.MAX; // public static final int
|
||||
Limits.DEFAULT; // public static final String
|
||||
Limits.Companion.getCOMPUTED(); // только геттер
|
||||
```
|
||||
`const val` требует примитив или `String` и инициализацию константным выражением - зато встраивается в место вызова (и потому меняет ABI при изменении значения). `@JvmField` работает с любым типом, но несовместим с `private`, `open`, `override`, `lateinit` и делегированными свойствами.
|
||||
|
||||
## @JvmName
|
||||
Задаёт имя в байт-коде. Три точки применения.
|
||||
|
||||
**Файл.** Функции верхнего уровня попадают в класс `ИмяФайлаKt`:
|
||||
```kotlin
|
||||
@file:JvmName("StringUtils")
|
||||
|
||||
fun String.slugify() = lowercase().replace(" ", "-")
|
||||
|
||||
fun main() {
|
||||
println("Привет Мир".slugify()) // привет-мир
|
||||
}
|
||||
```
|
||||
Без аннотации Java-код обращался бы к `SlugifyKt.slugify(s)`, с ней - `StringUtils.slugify(s)`. Расширения становятся статическими методами с получателем первым параметром.
|
||||
|
||||
**Аксессор свойства:**
|
||||
```kotlin
|
||||
class Account(private val raw: String) {
|
||||
@get:JvmName("fetchBalance")
|
||||
val balance: String get() = raw
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Account("1000").balance) // 1000
|
||||
}
|
||||
```
|
||||
Из Java метод называется `fetchBalance()`, а не `getBalance()`.
|
||||
|
||||
**Обход конфликта стирания.** Две функции, различающиеся только параметром дженерика, в байт-коде имеют одинаковую сигнатуру:
|
||||
```kotlin
|
||||
@JvmName("sumInts")
|
||||
fun List<Int>.sumAll() = sum()
|
||||
|
||||
@JvmName("sumDoubles")
|
||||
fun List<Double>.sumAll() = sum()
|
||||
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).sumAll()) // 6
|
||||
println(listOf(1.5, 2.5).sumAll()) // 4.0
|
||||
}
|
||||
```
|
||||
Без `@JvmName` компилятор откажется: «platform declaration clash». Это одна из немногих ситуаций, когда аннотация нужна даже для чисто Kotlin-кода. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## @Throws
|
||||
В Kotlin нет проверяемых исключений, поэтому в байт-коде нет `throws`:
|
||||
```kotlin
|
||||
import java.io.IOException
|
||||
|
||||
class Loader {
|
||||
@Throws(IOException::class)
|
||||
fun load(path: String): String {
|
||||
if (path.isEmpty()) throw IOException("пустой путь")
|
||||
return "данные из $path"
|
||||
}
|
||||
|
||||
fun loadNoThrows(path: String): String {
|
||||
if (path.isEmpty()) throw IOException("пустой путь")
|
||||
return "данные из $path"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val loader = Loader()
|
||||
println(loader.load("/tmp/file")) // данные из /tmp/file
|
||||
println(runCatching { loader.loadNoThrows("") }.isFailure) // true
|
||||
}
|
||||
```
|
||||
Из Java разница принципиальна:
|
||||
```java
|
||||
// Java
|
||||
try { loader.load(""); }
|
||||
catch (IOException e) { } // компилируется
|
||||
|
||||
try { loader.loadNoThrows(""); }
|
||||
catch (IOException e) { } // ОШИБКА: exception is never thrown
|
||||
```
|
||||
Java-компилятор запрещает ловить проверяемое исключение, которое метод не объявляет. Исключение всё равно вылетит в рантайме - просто обработать его можно только через `catch (Exception e)`.
|
||||
|
||||
## Как выглядят остальные конструкции
|
||||
### Nullability
|
||||
```kotlin
|
||||
class Validator {
|
||||
fun check(input: String): Boolean = input.isNotEmpty()
|
||||
fun find(key: String): String? = if (key == "a") "найдено" else null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val v = Validator()
|
||||
println(v.check("текст")) // true
|
||||
println(v.find("b")) // null
|
||||
}
|
||||
```
|
||||
`check(String)` получает аннотацию `@NotNull` на параметре и проверку `Intrinsics.checkNotNullParameter` в начале тела. Java-код, передавший `null`, получит `NullPointerException` с сообщением «Parameter specified as non-null is null» - сразу, а не при первом использовании. `find` возвращает `@Nullable String`, и IDE предупредит Java-код.
|
||||
|
||||
### Свойства
|
||||
```kotlin
|
||||
class User {
|
||||
var name: String = "аноним"
|
||||
val id: Int = 1
|
||||
var internalState: String = "скрыто"
|
||||
private set
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User()
|
||||
u.name = "Иван"
|
||||
println("${u.name} ${u.id} ${u.internalState}") // Иван 1 скрыто
|
||||
}
|
||||
```
|
||||
Из Java: `getName()`/`setName()`, `getId()` без сеттера, `getInternalState()` без сеттера (приватный сеттер в байт-коде приватен). Само поле всегда приватно - прямого доступа из Java нет.
|
||||
|
||||
### Функции-расширения
|
||||
```kotlin
|
||||
fun List<String>.firstOrDefault(default: String) = firstOrNull() ?: default
|
||||
|
||||
fun main() {
|
||||
println(listOf("a").firstOrDefault("нет")) // a
|
||||
println(emptyList<String>().firstOrDefault("нет")) // нет
|
||||
}
|
||||
```
|
||||
Из Java это статический метод с получателем первым аргументом: `FileNameKt.firstOrDefault(list, "нет")`. Никакого синтаксического сахара - расширение существует только в Kotlin.
|
||||
|
||||
### suspend-функции
|
||||
```kotlin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
suspend fun fetch(id: Int): String {
|
||||
delay(10)
|
||||
return "ответ для $id"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(fetch(1)) // ответ для 1
|
||||
}
|
||||
```
|
||||
В байт-коде сигнатура становится `Object fetch(int, Continuation)`, возвращая либо результат, либо маркер `COROUTINE_SUSPENDED`. Вызвать такое из Java практически невозможно. Для Java-клиентов делают обёртку:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.future.future
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class Api(private val scope: CoroutineScope) {
|
||||
suspend fun fetch(id: Int): String {
|
||||
delay(10)
|
||||
return "ответ для $id"
|
||||
}
|
||||
|
||||
// для Java: CompletableFuture вместо suspend
|
||||
fun fetchAsync(id: Int): CompletableFuture<String> =
|
||||
scope.future { fetch(id) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
println(Api(scope).fetchAsync(7).get()) // ответ для 7
|
||||
}
|
||||
```
|
||||
Нужна зависимость `kotlinx-coroutines-jdk8`. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md)
|
||||
|
||||
### value class
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class UserId(val value: Int)
|
||||
|
||||
fun process(id: UserId) = "обработан ${id.value}"
|
||||
|
||||
fun main() {
|
||||
println(process(UserId(42))) // обработан 42
|
||||
}
|
||||
```
|
||||
В байт-коде `process` получает имя `process-4ZD5Yi0(int)` - суффикс нужен, чтобы отличать перегрузки с разными value-классами над одним примитивом. Вызвать такое имя из Java нельзя: дефис недопустим в идентификаторе. Для Java-API добавляют обычную перегрузку:
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class Money(val cents: Long)
|
||||
|
||||
fun charge(amount: Money) = "списано ${amount.cents}"
|
||||
|
||||
@JvmName("chargeCents")
|
||||
fun charge(cents: Long) = charge(Money(cents))
|
||||
|
||||
fun main() {
|
||||
println(charge(Money(500))) // списано 500
|
||||
println(charge(500L)) // списано 500
|
||||
}
|
||||
```
|
||||
Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
|
||||
|
||||
### internal
|
||||
```kotlin
|
||||
class Service {
|
||||
internal fun helper() = "вспомогательное"
|
||||
fun public() = helper()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service().public()) // вспомогательное
|
||||
}
|
||||
```
|
||||
`internal` компилируется в `public` с манглированным именем вида `helper$main` (суффикс - имя модуля). Java-код технически может вызвать `helper$main()`. Это не изоляция, а соглашение, которое соблюдает только Kotlin-компилятор. Подробнее: [Модификаторы видимости](../ООП/Модификаторы%20видимости.md)
|
||||
|
||||
### data class
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val p = Point(1, 2)
|
||||
println(p) // Point(x=1, y=2)
|
||||
println(p.copy(y = 5)) // Point(x=1, y=5)
|
||||
val (x, y) = p
|
||||
println("$x $y") // 1 2
|
||||
}
|
||||
```
|
||||
Из Java доступны `getX()`, `getY()`, `equals`, `hashCode`, `toString`, `component1()`, `component2()` и `copy(int, int)`. Но `copy` с одним аргументом недоступна - это те же значения по умолчанию, и `@JvmOverloads` на сгенерированном `copy` поставить нельзя. Деструктуризации в Java нет. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
### sealed
|
||||
```kotlin
|
||||
sealed interface Result
|
||||
data class Ok(val value: String) : Result
|
||||
data class Err(val message: String) : Result
|
||||
|
||||
fun describe(r: Result) = when (r) {
|
||||
is Ok -> "успех: ${r.value}"
|
||||
is Err -> "ошибка: ${r.message}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe(Ok("данные"))) // успех: данные
|
||||
println(describe(Err("сбой"))) // ошибка: сбой
|
||||
}
|
||||
```
|
||||
Из Java `sealed interface` Kotlin - обычный интерфейс (при JVM-таргете 17+ может компилироваться в настоящий `sealed`). Исчерпывающего `switch` Java не получит - гарантия полноты обеспечивается только Kotlin-компилятором. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Чеклист публичного API для Java-клиентов
|
||||
1. `@JvmOverloads` на всех функциях со значениями по умолчанию.
|
||||
2. `@JvmStatic` на членах `companion object` и `object`.
|
||||
3. `const val` или `@JvmField` для констант.
|
||||
4. `@Throws` там, где Java-код должен ловить проверяемое исключение.
|
||||
5. `@file:JvmName` для файлов с функциями верхнего уровня.
|
||||
6. Нет `suspend` в публичном API - только обёртки (`CompletableFuture`, колбэки).
|
||||
7. Нет `value class` в сигнатурах - или перегрузка с `@JvmName`.
|
||||
8. Возвращать `List`, а не `MutableList`, и отдавать копию, если Java может изменить.
|
||||
9. Nullable-типы объявлять явно - Java-IDE прочитает аннотации.
|
||||
10. Проверить результат через `javap` на реальном классе.
|
||||
|
||||
```kotlin
|
||||
// пример API, готового к вызову из Java
|
||||
@file:JvmName("UserApi")
|
||||
|
||||
class UserService @JvmOverloads constructor(
|
||||
private val baseUrl: String = "https://api.example.com",
|
||||
) {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmOverloads
|
||||
fun find(id: String, includeDeleted: Boolean = false): String {
|
||||
require(id.isNotBlank()) { "id не может быть пустым" }
|
||||
return "$baseUrl/users/$id?deleted=$includeDeleted"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_TIMEOUT_MS = 5_000L
|
||||
|
||||
@JvmStatic
|
||||
fun default() = UserService()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(UserService.default().find("42"))
|
||||
println(UserService.DEFAULT_TIMEOUT_MS)
|
||||
// https://api.example.com/users/42?deleted=false
|
||||
// 5000
|
||||
}
|
||||
```
|
||||
|
||||
## Проверка результата
|
||||
Единственный надёжный способ увидеть реальный API - посмотреть байт-код:
|
||||
```
|
||||
kotlinc Api.kt -d out
|
||||
javap -cp out com.example.UserApi
|
||||
javap -cp out 'com.example.UserService$Companion'
|
||||
```
|
||||
`javap` покажет точные сигнатуры, включая синтетические методы `$default` и манглированные имена. Это стоит делать перед релизом библиотеки. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin из Java |
|
||||
> |---|---|
|
||||
> | `static` метод | `@JvmStatic` в `companion object` |
|
||||
> | `public static final` поле | `const val` или `@JvmField` |
|
||||
> | перегрузки пишутся руками | `@JvmOverloads` генерирует |
|
||||
> | `throws` в сигнатуре | `@Throws` |
|
||||
> | имя класса = имя файла | `@file:JvmName` |
|
||||
> | утилитный класс со `static` | функции верхнего уровня |
|
||||
> | геттер пишется руками | генерируется из свойства |
|
||||
>
|
||||
> **Аннотации `@Jvm*` - это не костыли, а явное управление ABI.** В Java представление в байт-коде однозначно: что написано, то и скомпилируется. В Kotlin между исходником и байт-кодом есть зазор, и аннотации дают над ним контроль. Плата за выразительность языка: `@JvmOverloads` существует потому, что в JVM нет значений по умолчанию, а не из-за недоработки.
|
||||
>
|
||||
> **Значения по умолчанию в Kotlin устроены иначе, чем перегрузки в Java.** В Java N необязательных параметров требуют N перегрузок, каждую с делегированием. В Kotlin - один метод и синтетический `$default`, принимающий битовую маску переданных аргументов. Отсюда и ограничение `@JvmOverloads`: перегрузки генерируются только отбрасыванием с конца, потому что произвольные комбинации дали бы экспоненциальное число методов.
|
||||
>
|
||||
> **`companion object` - настоящий объект, а не пространство имён.** В Java `static` - модификатор: метод принадлежит классу. В Kotlin companion - отдельный экземпляр класса `Cls$Companion`, у него есть тип, он может реализовывать интерфейсы и быть получателем расширений. Поэтому без `@JvmStatic` вызов идёт через `Cls.Companion`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
>
|
||||
> **Отсутствие проверяемых исключений нельзя «включить» обратно.** `@Throws` только добавляет запись в атрибут `Exceptions` метода - сам Kotlin-компилятор её не проверяет. Фактически это документация для Java-компилятора: без неё Java не разрешит `catch`, с ней - потребует обработки.
|
||||
>
|
||||
> **`suspend` принципиально несовместим с Java.** Корутины реализованы через CPS-преобразование на этапе компиляции Kotlin: функция превращается в конечный автомат с параметром `Continuation`. Java-компилятор этого преобразования не делает, поэтому написать Java-вызывающий код для `suspend`-функции нельзя - только обёртки. Это самая жёсткая граница interop. Подробнее: [Корутины против потоков](../Корутины/Корутины%20против%20потоков.md)
|
||||
>
|
||||
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `fun f(a: Int = 1, b: Int = 2, c: Int = 3)` появятся `f(a,b,c)`, `f(a,b)`, `f(a)`, `f()` - но не `f(c)` и не `f(a,c)`. Из Java нельзя передать только третий параметр. Ставьте чаще переопределяемые параметры раньше.
|
||||
>
|
||||
> **Добавление `@JvmStatic` или `@JvmOverloads` позже ломает совместимость.** `@JvmStatic` не удаляет метод с `Companion`, но меняет рекомендуемую точку входа; снятие аннотации удаляет статический метод и роняет скомпилированный Java-код с `NoSuchMethodError`. Решайте до релиза.
|
||||
>
|
||||
> **Изменение `const val` требует перекомпиляции клиентов.** Значение встраивается в место вызова, поэтому Java-код, собранный против старой версии, продолжит использовать старое число. Для значений, которые могут меняться, лучше `@JvmField val`.
|
||||
>
|
||||
> **`@JvmField` несовместим с половиной модификаторов:**
|
||||
> ```kotlin
|
||||
> class Limits {
|
||||
> companion object {
|
||||
> @JvmField val OK = "работает"
|
||||
> // @JvmField private val BAD = "" // ошибка: не может быть private
|
||||
> // @JvmField lateinit var LATE: String // ошибка: не для lateinit
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Limits.OK) // работает
|
||||
> }
|
||||
> ```
|
||||
> Запрещено для `private`, `open`, `override`, `const`, `lateinit` и делегированных свойств.
|
||||
>
|
||||
> **`copy()` у `data class` из Java требует все аргументы.** `@JvmOverloads` на сгенерированный `copy` поставить нельзя. Если Java-коду нужна частичная копия, пишите свой метод или билдер.
|
||||
>
|
||||
> **Манглированные имена value-классов недоступны из Java.** `fun takeId(id: UserId)` компилируется в `takeId-4ZD5Yi0(int)`; дефис делает имя невызываемым в Java. Это проявится только при попытке интеграции - Kotlin-компилятор не предупредит.
|
||||
>
|
||||
> **`internal` виден из Java.** `internalFun$main()` - публичный метод в байт-коде. Если нужна настоящая закрытость для Java-кода в том же проекте, используйте `private` или выносите в отдельный модуль.
|
||||
>
|
||||
> **`List<T>` из Kotlin в Java имеет `add()`.** Тип стирается до `java.util.List`, и Java-код может вызвать мутирующий метод. Падение будет в рантайме с `UnsupportedOperationException`. Для публичного API возвращайте `Collections.unmodifiableList(...)` или копию.
|
||||
>
|
||||
> **Платформенные объявления конфликтуют при стирании:**
|
||||
> ```kotlin
|
||||
> @JvmName("countStrings")
|
||||
> fun List<String>.count2() = size
|
||||
>
|
||||
> @JvmName("countNumbers")
|
||||
> fun List<Int>.count2() = size
|
||||
>
|
||||
> fun main() {
|
||||
> println(listOf("a").count2()) // 1
|
||||
> println(listOf(1, 2).count2()) // 2
|
||||
> }
|
||||
> ```
|
||||
> Без `@JvmName` будет ошибка компиляции «platform declaration clash» - обе функции дают `count2(List)`. Это проявляется и в чисто Kotlin-проекте.
|
||||
>
|
||||
> **`@file:JvmName` должна стоять до `package`.** Файловые аннотации идут самой первой строкой файла, иначе компилятор их не примет.
|
||||
>
|
||||
> **`@get:JvmName` на свойстве в интерфейсе или `open` классе запрещена.** Переименование аксессора сломало бы переопределение, поэтому компилятор его не допускает для `open`/`override` членов.
|
||||
>
|
||||
> **Делегированные свойства из Java выглядят как обычные.** `by lazy` даёт `getX()`, но поле хранит `Lazy<T>` под именем `x$delegate`. Прямого доступа к значению нет, и `@JvmField` не применить. Подробнее: [Делегирование](../ООП/Делегирование.md)
|
||||
>
|
||||
> **Расширения не переопределяются и не видны как методы.** `fun Foo.bar()` из Java - это `FileKt.bar(foo)`. Java-код не найдёт `foo.bar()`, а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса.
|
||||
|
||||
Далее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
title: Взаимодействие с Java
|
||||
description: Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел
|
||||
tags:
|
||||
- kotlin
|
||||
- java-interop
|
||||
---
|
||||
|
||||
Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен.
|
||||
|
||||
```kotlin
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
|
||||
fun main() {
|
||||
val id = UUID.randomUUID()
|
||||
val today = LocalDate.of(2026, 1, 15)
|
||||
|
||||
println(id.toString().length) // 36
|
||||
println(today.plusDays(30)) // 2026-02-14
|
||||
println(today.dayOfWeek) // THURSDAY
|
||||
}
|
||||
```
|
||||
Это обычный Java API, вызванный как родной Kotlin-код: никаких обёрток, конвертеров и объявлений внешних типов.
|
||||
|
||||
## Что происходит без усилий
|
||||
| Что | Как работает |
|
||||
|---|---|
|
||||
| вызов Java-метода из Kotlin | как обычный метод |
|
||||
| геттеры/сеттеры Java | как свойства Kotlin |
|
||||
| Java-коллекции | как `MutableList`, `MutableMap` и т.д. |
|
||||
| наследование от Java-класса | как от Kotlin-класса |
|
||||
| реализация Java-интерфейса | SAM-конверсия из лямбды |
|
||||
| Java-аннотации | применяются к Kotlin-коду |
|
||||
| смешанная компиляция в одном модуле | поддерживается Gradle-плагином |
|
||||
|
||||
```kotlin
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
fun main() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
executor.submit { println("Runnable из лямбды") } // SAM-конверсия
|
||||
executor.shutdown()
|
||||
|
||||
val list = java.util.ArrayList<String>()
|
||||
list.add("элемент") // как MutableList
|
||||
println(list.size) // 1
|
||||
|
||||
val builder = StringBuilder()
|
||||
builder.append("a").append("b")
|
||||
println(builder.length) // 2
|
||||
// Runnable из лямбды
|
||||
}
|
||||
```
|
||||
|
||||
## Где возникает трение
|
||||
Три системы типов Kotlin не имеют соответствия в Java, и именно на этих границах появляются сложности.
|
||||
|
||||
**1. Null-безопасность.** Java не различает `String` и `String?`, поэтому любое значение из Java - платформенный тип `String!`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
// Java-метод вернул null, а Kotlin считал тип ненулевым
|
||||
val value: String = System.getProperty("несуществующее.свойство")
|
||||
?: "значение по умолчанию"
|
||||
println(value) // значение по умолчанию
|
||||
}
|
||||
```
|
||||
Без `?:` здесь был бы `NullPointerException` в момент присваивания. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
|
||||
|
||||
**2. Изменяемость коллекций.** В Kotlin `List` и `MutableList` - разные типы, в Java это один `java.util.List`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val immutable: List<String> = listOf("a", "b")
|
||||
// передача в Java-метод, который может вызвать add()
|
||||
val asJava: java.util.List<String> = immutable as java.util.List<String>
|
||||
val error = runCatching { asJava.add("c") }.exceptionOrNull()
|
||||
println("неизменяемый список отверг изменение: ${error != null}") // неизменяемый список отверг изменение: true
|
||||
}
|
||||
```
|
||||
Java-код не видит разницы и может попытаться изменить список, который Kotlin считает неизменяемым. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
|
||||
|
||||
**3. Возможности Kotlin, которых нет в Java.** Значения по умолчанию, функции-расширения, `suspend`, value-классы, `object` - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации `@JvmStatic`, `@JvmOverloads`, `@JvmName` и другие. Подробнее: [Kotlin из Java](Kotlin%20из%20Java.md)
|
||||
|
||||
## Асимметрия направлений
|
||||
Направления не равноценны - и это главное, что нужно понять про interop.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Java-код"] -->|"почти без трения:<br/>только null-безопасность"| B["Kotlin-код"]
|
||||
B -->|"требует аннотаций:<br/>@JvmStatic, @JvmOverloads,<br/>@JvmName, @JvmField"| A
|
||||
```
|
||||
|
||||
**Java из Kotlin** работает почти прозрачно: язык создавался именно для этого. Единственная серьёзная проблема - платформенные типы, то есть риск `NullPointerException` там, где компилятор уверен в ненулевом значении.
|
||||
|
||||
**Kotlin из Java** требует внимания: без аннотаций ваш API окажется неудобным - значения по умолчанию превратятся в один метод с лишними параметрами, `companion object` потребует обращения через `.Companion`, а `suspend`-функция получит параметр `Continuation`.
|
||||
|
||||
Практический вывод: если ваш Kotlin-код вызывается из Java, его публичный API нужно проектировать осознанно. Если Kotlin-код вызывается только из Kotlin, аннотации не нужны вообще.
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [Java из Kotlin](Java%20из%20Kotlin.md) | платформенные типы, SAM, массивы, статика, синтетические свойства |
|
||||
| [Kotlin из Java](Kotlin%20из%20Java.md) | `@JvmStatic`, `@JvmOverloads`, `@JvmName`, `@JvmField`, `@Throws` |
|
||||
| [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) | во что компилируются свойства, лямбды, `data class`, `suspend` |
|
||||
|
||||
## Смешанный проект
|
||||
Gradle-плагин Kotlin компилирует Java и Kotlin вместе, разрешая циклические зависимости между языками:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
java
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
// Kotlin и Java в одном модуле:
|
||||
// src/main/kotlin/ и src/main/java/ — компилируются совместно
|
||||
```
|
||||
Порядок компиляции: сначала Kotlin видит Java-исходники (через стабы), затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java-класс может наследоваться от Kotlin-класса, который реализует Java-интерфейс. Подробнее: [Структура проекта](../Инструментарий/Структура%20проекта.md)
|
||||
|
||||
## Стратегия миграции
|
||||
Типичный путь для существующего Java-проекта:
|
||||
|
||||
1. **Новый код на Kotlin, старый не трогать.** Interop позволяет это без прослоек.
|
||||
2. **Конвертировать по файлу**, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей.
|
||||
3. **Проверять результат автоконвертера.** IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable, `!!` вместо `?.`, классы вместо `data class`.
|
||||
4. **Публичный API, вызываемый из Java, аннотировать** сразу - иначе придётся менять его потом.
|
||||
5. **Тесты конвертировать последними или не конвертировать** - они и так работают.
|
||||
|
||||
```kotlin
|
||||
// типичный результат автоконвертера — работает, но это не идиоматичный Kotlin
|
||||
class UserDtoConverted {
|
||||
var name: String? = null
|
||||
var age: Int? = null
|
||||
fun getDisplayName(): String {
|
||||
return if (name != null) name!! else "аноним"
|
||||
}
|
||||
}
|
||||
|
||||
// идиоматичная версия
|
||||
data class UserDto(val name: String?, val age: Int?) {
|
||||
val displayName: String get() = name ?: "аноним"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(UserDtoConverted().getDisplayName()) // аноним
|
||||
println(UserDto(null, null).displayName) // аноним
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | один язык, одна система типов | две системы, граница между ними |
|
||||
> | `String` может быть `null` | `String` и `String?` различаются |
|
||||
> | `List` один тип | `List` и `MutableList` |
|
||||
> | `int` и `Integer` различаются | `Int` и `Int?` |
|
||||
> | статические методы | `companion object` + `@JvmStatic` |
|
||||
> | нет аналога | платформенные типы `String!` |
|
||||
> | checked exceptions обязательны | проверяемых исключений нет |
|
||||
>
|
||||
> **Interop работает, потому что Kotlin компилируется в тот же байт-код.** Kotlin-класс - это обычный класс JVM: у него те же модификаторы доступа, то же наследование, тот же формат `.class`. Никакой прослойки, маршалинга или рефлексии в runtime нет, поэтому вызов Kotlin-метода из Java стоит ровно столько же, сколько вызов Java-метода. Это принципиально отличает ситуацию от interop между языками разных платформ (JNI, JNA), где границу приходится пересекать с накладными расходами.
|
||||
>
|
||||
> **Цена - необходимость представлять конструкции Kotlin средствами JVM.** В байт-коде нет ни nullable-типов, ни значений по умолчанию, ни функций-расширений. Kotlin эмулирует их: nullable-информацию хранит в аннотациях и метаданных, значения по умолчанию - в синтетическом методе `greet$default`, расширения - в статических методах. Из Kotlin это видно как родные конструкции (компилятор читает метаданные), из Java - как реализация. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
|
||||
>
|
||||
> **Проверяемые исключения - односторонняя потеря.** В Kotlin их нет: `throws IOException` из Java-метода не обязывает ни к `try/catch`, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без `@Throws`. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
>
|
||||
> **Null-безопасность Kotlin не распространяется на Java-код.** Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации `@Nullable`/`@NotNull` в Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Платформенные типы отключают проверки компилятора.** Значение из Java имеет тип `String!`, который компилятор позволяет использовать и как `String`, и как `String?`. Ошибка проявится как `NullPointerException` в рантайме, в точке присваивания:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val error = runCatching {
|
||||
> val value: String = System.getProperty("нет.такого") // вернёт null
|
||||
> value.length
|
||||
> }.exceptionOrNull()
|
||||
> println("NPE от платформенного типа: ${error is NullPointerException}")
|
||||
> // NPE от платформенного типа: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Автоконвертер даёт работающий, но плохой код.** Типичные следы: `!!` вместо `?.`, `var x: T? = null` вместо конструктора, геттеры вместо свойств, обычные классы вместо `data class`. После конвертации файл нужно переписывать вручную, иначе Kotlin-проект унаследует все недостатки Java-кода и не получит преимуществ.
|
||||
>
|
||||
> **Java-код может нарушить неизменяемость Kotlin-коллекции.** `List<String>` из Kotlin приходит в Java как `java.util.List`, у которого есть `add()`. Вызов упадёт с `UnsupportedOperationException` в рантайме, а компилятор об этом не предупредит. Передавайте в Java копию (`toList()`) или `Collections.unmodifiableList`.
|
||||
>
|
||||
> **Kotlin-код без аннотаций неудобен из Java.** Значения по умолчанию, `companion object`, `object`, `suspend`-функции и функции верхнего уровня выглядят из Java не так, как задумано. Если API публичный, это нужно решать до релиза - добавление `@JvmStatic` позже меняет сигнатуры и ломает совместимость.
|
||||
>
|
||||
> **`internal` в Kotlin из Java доступен.** Он компилируется в `public` с манглированным именем (`internalFun$main`), поэтому Java-код в том же проекте технически может его вызвать. Это не защита, а соглашение.
|
||||
>
|
||||
> **Проверяемые исключения из Kotlin не видны Java.** Kotlin-метод, бросающий `IOException`, из Java выглядит как не бросающий ничего - `catch (IOException e)` вокруг него не скомпилируется («exception is never thrown»). Нужна аннотация `@Throws`.
|
||||
>
|
||||
> **Смешанная компиляция требует правильного плагина.** В Gradle нужен `kotlin("jvm")` и, при наличии Java-исходников, корректно настроенный `sourceSets`. Без этого Java-код не увидит Kotlin-классы, а ошибка выглядит как «cannot find symbol».
|
||||
>
|
||||
> **Generics Java стираются и в Kotlin.** `List<String>` и `List<Int>` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
Далее: [Java из Kotlin](Java%20из%20Kotlin.md)
|
||||
@@ -1,359 +0,0 @@
|
||||
---
|
||||
title: Reified и inline
|
||||
description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет
|
||||
tags:
|
||||
- kotlin
|
||||
- generics
|
||||
---
|
||||
|
||||
На JVM параметры типа стираются: в рантайме `List<Int>` и `List<String>` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`.
|
||||
|
||||
```kotlin
|
||||
inline fun <reified T> isOfType(value: Any): Boolean = value is T
|
||||
|
||||
fun main() {
|
||||
println(isOfType<String>("текст")) // true
|
||||
println(isOfType<Int>("текст")) // false
|
||||
println(isOfType<List<*>>(listOf(1))) // true
|
||||
}
|
||||
```
|
||||
Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно.
|
||||
|
||||
## Что делает компилятор
|
||||
```kotlin
|
||||
inline fun <reified T> nameOf(): String = T::class.simpleName ?: "?"
|
||||
|
||||
fun main() {
|
||||
println(nameOf<String>()) // String
|
||||
println(nameOf<Int>()) // Int
|
||||
}
|
||||
```
|
||||
В байт-коде вызова `nameOf<String>()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего.
|
||||
|
||||
Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически.
|
||||
|
||||
## Главный случай: парсинг и десериализация
|
||||
Задача, ради которой `reified` чаще всего и используют:
|
||||
```kotlin
|
||||
inline fun <reified T> parseAs(raw: String): T? = when (T::class) {
|
||||
Int::class -> raw.toIntOrNull() as T?
|
||||
Long::class -> raw.toLongOrNull() as T?
|
||||
Double::class -> raw.toDoubleOrNull() as T?
|
||||
Boolean::class -> raw.toBooleanStrictOrNull() as T?
|
||||
String::class -> raw as T
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parseAs<Int>("42")) // 42
|
||||
println(parseAs<Int>("abc")) // null
|
||||
println(parseAs<Double>("1.5")) // 1.5
|
||||
println(parseAs<Boolean>("true")) // true
|
||||
println(parseAs<String>("текст")) // текст
|
||||
}
|
||||
```
|
||||
В Java такая функция потребовала бы параметр `Class<T> type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun <reified T> readValue(json: String): T`, и тип выводится из контекста:
|
||||
```kotlin
|
||||
// С kotlinx.serialization или Jackson Kotlin module:
|
||||
// val user: User = mapper.readValue(json) // тип берётся из объявления переменной
|
||||
```
|
||||
|
||||
## filterIsInstance и прочее в stdlib
|
||||
```kotlin
|
||||
fun main() {
|
||||
val mixed: List<Any> = listOf(1, "a", 2.0, 3, "b")
|
||||
|
||||
println(mixed.filterIsInstance<Int>()) // [1, 3]
|
||||
println(mixed.filterIsInstance<String>()) // [a, b]
|
||||
println(mixed.filterIsInstance<Number>()) // [1, 2.0, 3]
|
||||
|
||||
val target = mutableListOf<Int>()
|
||||
mixed.filterIsInstanceTo(target)
|
||||
println(target) // [1, 3]
|
||||
}
|
||||
```
|
||||
`filterIsInstance` объявлена как `inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>` - без `reified` поэлементная проверка типа была бы невозможна.
|
||||
|
||||
Другие `reified`-функции stdlib:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val array = arrayOf(1, 2, 3) // arrayOf — reified
|
||||
println(listOf("a", "b").toTypedArray().size) // 2 — тоже reified
|
||||
println(array.javaClass.simpleName) // Integer[]
|
||||
|
||||
val typed: Array<String?> = arrayOfNulls(2)
|
||||
println(typed.size) // 2
|
||||
|
||||
println(emptyArray<Int>().size) // 0
|
||||
}
|
||||
```
|
||||
`toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array<T>` в рантайме есть настоящий тип элементов.
|
||||
|
||||
## Безопасное приведение
|
||||
```kotlin
|
||||
inline fun <reified T> Any?.asOrNull(): T? = this as? T
|
||||
|
||||
fun main() {
|
||||
val value: Any = "текст"
|
||||
println(value.asOrNull<String>()?.length) // 5
|
||||
println(value.asOrNull<Int>()) // null — без исключения
|
||||
}
|
||||
```
|
||||
Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции.
|
||||
|
||||
Разбор `Map<String, Any>` - типичная задача при работе с конфигами и JSON:
|
||||
```kotlin
|
||||
inline fun <reified T> Map<String, Any>.getTyped(key: String): T? = this[key] as? T
|
||||
|
||||
fun main() {
|
||||
val config = mapOf<String, Any>("port" to 8080, "host" to "localhost", "tls" to true)
|
||||
|
||||
println(config.getTyped<Int>("port")) // 8080
|
||||
println(config.getTyped<String>("host")) // localhost
|
||||
println(config.getTyped<Int>("host")) // null — тип не тот
|
||||
println(config.getTyped<Int>("нет")) // null — ключа нет
|
||||
}
|
||||
```
|
||||
|
||||
## reified и Java-рефлексия
|
||||
```kotlin
|
||||
inline fun <reified T : Any> newInstance(): T = T::class.java.getDeclaredConstructor().newInstance()
|
||||
|
||||
class Service2 {
|
||||
override fun toString() = "сервис создан"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val service: Service2 = newInstance()
|
||||
println(service) // сервис создан
|
||||
}
|
||||
```
|
||||
`T::class` даёт `KClass<T>`, `T::class.java` - `Class<T>`. Это мост к любому Java API, требующему `Class`:
|
||||
```kotlin
|
||||
inline fun <reified T> typeName(): String = T::class.java.name
|
||||
|
||||
fun main() {
|
||||
println(typeName<String>()) // java.lang.String
|
||||
println(typeName<Int>()) // java.lang.Integer — не int!
|
||||
println(typeName<List<String>>()) // java.util.List
|
||||
println(Int::class.java.name) // int — а так примитив
|
||||
}
|
||||
```
|
||||
Две детали из этого примера:
|
||||
- `T::class` знает только стёртый тип: `List<String>` и `List<Int>` дадут одинаковый результат.
|
||||
- Параметр типа всегда представлен ссылочным типом, поэтому `T = Int` даёт `java.lang.Integer`, хотя написанный напрямую `Int::class.java` возвращает примитивный `int`.
|
||||
|
||||
## Что происходит с inline
|
||||
`reified` - побочный эффект `inline`, а у `inline` есть своя основная задача: убрать объект лямбды.
|
||||
```kotlin
|
||||
inline fun measure(block: () -> Unit): Long {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
return System.nanoTime() - start
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val time = measure { (1..1000).sum() }
|
||||
println(time > 0) // true
|
||||
}
|
||||
```
|
||||
Тело `block` подставляется прямо в `main` - объекта `Function0` не создаётся. Подробнее об этом и о `noinline`/`crossinline`: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
|
||||
|
||||
Для `reified` важно только одно следствие inline: **функция перестаёт существовать как метод**, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку.
|
||||
|
||||
## Ограничения reified
|
||||
```kotlin
|
||||
inline fun <reified T> demo(): String {
|
||||
val cls = T::class // можно
|
||||
val arr = arrayOfNulls<T>(1) // можно
|
||||
// val instance = T() // нельзя: нет конструктора в типе
|
||||
// val companion = T.Companion // нельзя
|
||||
return "${cls.simpleName} ${arr.size}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(demo<String>()) // String 1
|
||||
}
|
||||
```
|
||||
|
||||
| Можно с `reified T` | Нельзя |
|
||||
|---|---|
|
||||
| `value is T`, `value as? T` | `T()` - создать экземпляр |
|
||||
| `T::class`, `T::class.java` | обращаться к статическим членам `T` |
|
||||
| `arrayOfNulls<T>()`, `arrayOf<T>()` | различать `List<Int>` и `List<String>` |
|
||||
| передать `T` в другую `reified`-функцию | вызвать из Java |
|
||||
| вывести `T` из типа переменной | передать тип как значение в рантайме |
|
||||
|
||||
Создание экземпляра требует рефлексии:
|
||||
```kotlin
|
||||
inline fun <reified T : Any> createOrNull(): T? = runCatching {
|
||||
T::class.java.getDeclaredConstructor().newInstance()
|
||||
}.getOrNull()
|
||||
|
||||
class WithDefault { override fun toString() = "создан" }
|
||||
class WithoutDefault(val x: Int)
|
||||
|
||||
fun main() {
|
||||
println(createOrNull<WithDefault>()) // создан
|
||||
println(createOrNull<WithoutDefault>()) // null — нет пустого конструктора
|
||||
}
|
||||
```
|
||||
|
||||
## Проброс reified дальше
|
||||
```kotlin
|
||||
inline fun <reified T> logType(value: Any) {
|
||||
println("${T::class.simpleName}: ${check2<T>(value)}")
|
||||
}
|
||||
|
||||
inline fun <reified T> check2(value: Any): Boolean = value is T
|
||||
|
||||
fun main() {
|
||||
logType<String>("текст") // String: true
|
||||
logType<Int>("текст") // Int: false
|
||||
}
|
||||
```
|
||||
`reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет:
|
||||
```kotlin
|
||||
fun <T> notReified(value: Any): Boolean = value != null // T здесь стёрт
|
||||
|
||||
inline fun <reified T> caller(value: Any): Boolean = notReified<T>(value)
|
||||
|
||||
fun main() {
|
||||
println(caller<String>("текст")) // true — но проверки типа внутри нет
|
||||
}
|
||||
```
|
||||
|
||||
## Альтернатива без reified
|
||||
Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно:
|
||||
```kotlin
|
||||
fun <T : Any> parseWith(raw: String, type: kotlin.reflect.KClass<T>): T? = when (type) {
|
||||
Int::class -> raw.toIntOrNull() as T?
|
||||
String::class -> raw as T
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parseWith("42", Int::class)) // 42
|
||||
println(parseWith("текст", String::class)) // текст
|
||||
}
|
||||
```
|
||||
Это ровно Java-подход с `Class<T>`. Он многословнее в вызове, зато функция остаётся обычным методом.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `<T> T parse(String s, Class<T> type)` | `inline fun <reified T> parse(s: String): T` |
|
||||
> | `parse(json, User.class)` | `parse<User>(json)` или `val u: User = parse(json)` |
|
||||
> | `x instanceof T` - невозможно | `x is T` с `reified` |
|
||||
> | `new T[]` - запрещён | `arrayOf<T>()` с `reified` |
|
||||
> | `T.class` - невозможно | `T::class` с `reified` |
|
||||
> | нет inline-функций | `inline` + подстановка на этапе компиляции |
|
||||
>
|
||||
> **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java:
|
||||
> ```java
|
||||
> User user = mapper.readValue(json, User.class);
|
||||
> List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
|
||||
> ```
|
||||
> Kotlin-модуль того же Jackson:
|
||||
> ```kotlin
|
||||
> // val user: User = mapper.readValue(json)
|
||||
> // val users: List<User> = mapper.readValue(json)
|
||||
> ```
|
||||
> Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной.
|
||||
>
|
||||
> **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа:
|
||||
> ```kotlin
|
||||
> inline fun <reified T> describeType(): String = T::class.simpleName ?: "?"
|
||||
>
|
||||
> fun main() {
|
||||
> println(describeType<List<String>>()) // List — параметр потерян
|
||||
> println(describeType<List<Int>>()) // List — то же самое
|
||||
> }
|
||||
> ```
|
||||
> Для полного типа с аргументами нужен `typeOf<T>()` из `kotlin.reflect`, и он требует зависимости `kotlin-reflect`.
|
||||
>
|
||||
> **Inline-функций в Java нет.** JVM инлайнит методы на уровне JIT в рантайме, но это оптимизация, невидимая системе типов. Kotlin инлайнит на этапе компиляции, что даёт две вещи сразу: отсутствие объекта лямбды и `reified`. Цена - рост размера байт-кода и невозможность вызвать такую функцию из Java.
|
||||
>
|
||||
> **`filterIsInstance` аналога в Java не имеет.** Эквивалент - `stream().filter(X.class::isInstance).map(X.class::cast)`, где класс указывается дважды.
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`reified` без `inline` не компилируется** - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать `KClass<T>` вручную.
|
||||
>
|
||||
> **Тип всё равно стёрт внутри аргументов:**
|
||||
> ```kotlin
|
||||
> inline fun <reified T> isList(value: Any): Boolean = value is T
|
||||
>
|
||||
> fun main() {
|
||||
> println(isList<List<String>>(listOf(1, 2))) // true — хотя это List<Int>!
|
||||
> }
|
||||
> ```
|
||||
> `is List<String>` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа.
|
||||
>
|
||||
> **`as T` внутри reified-функции не всегда проверяется:**
|
||||
> ```kotlin
|
||||
> inline fun <reified T> forceCast(value: Any): T = value as T
|
||||
>
|
||||
> fun main() {
|
||||
> val list: List<String> = forceCast(listOf(1, 2)) // прошло!
|
||||
> println(list.size) // 2
|
||||
> try {
|
||||
> println(list[0].length)
|
||||
> } catch (e: ClassCastException) {
|
||||
> println("упало только при обращении к элементу")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Каст к `List<String>` сводится к касту к `List`, который успешен. Ошибка всплывает позже.
|
||||
>
|
||||
> **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций.
|
||||
>
|
||||
> **Inline-функцию нельзя вызвать из Java:**
|
||||
> ```kotlin
|
||||
> inline fun <reified T> kotlinOnly(): String = T::class.simpleName ?: "?"
|
||||
>
|
||||
> fun main() { println(kotlinOnly<Int>()) } // Int
|
||||
> ```
|
||||
> Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class<T>`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора.
|
||||
>
|
||||
> **Ссылку на inline-функцию взять нельзя:**
|
||||
> ```kotlin
|
||||
> inline fun <reified T> typeNameOf(): String = T::class.simpleName ?: "?"
|
||||
>
|
||||
> fun main() {
|
||||
> // val ref = ::typeNameOf // ошибка: нельзя взять ссылку
|
||||
> println(typeNameOf<Int>()) // Int
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя:
|
||||
> ```kotlin
|
||||
> inline fun <reified T> show2(value: Any) = println(value is T)
|
||||
>
|
||||
> fun <U> wrapper(value: Any) {
|
||||
> // show2<U>(value) // ошибка: U не reified
|
||||
> show2<Any>(value)
|
||||
> }
|
||||
>
|
||||
> fun main() { wrapper<String>("x") } // true
|
||||
> ```
|
||||
> Цепочка `reified` обрывается на первой обычной обобщённой функции.
|
||||
>
|
||||
> **`T::class.java` и `Int::class.java` дают разные классы:**
|
||||
> ```kotlin
|
||||
> inline fun <reified T> jvmName(): String = T::class.java.name
|
||||
>
|
||||
> fun main() {
|
||||
> println(jvmName<Int>()) // java.lang.Integer
|
||||
> println(Int::class.java.name) // int
|
||||
> println(Int::class.javaObjectType.name) // java.lang.Integer
|
||||
> }
|
||||
> ```
|
||||
> Через параметр типа всегда получается обёртка - примитив не может быть аргументом типа. Написанный напрямую `Int::class.java` даёт примитивный `int`, и если результат передаётся в Java-API, различие имеет значение: `Integer.class` и `int.class` - разные объекты, и, например, `getDeclaredMethod` по ним найдёт разные методы. Явные формы - `javaPrimitiveType` и `javaObjectType`. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
|
||||
>
|
||||
> **Изменение тела inline-функции требует перекомпиляции всех вызывающих.** Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline.
|
||||
|
||||
Далее: [Функции высшего порядка](../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
|
||||
@@ -1,351 +0,0 @@
|
||||
---
|
||||
title: Вариантность
|
||||
description: out и in в объявлении класса, ковариантность, контравариантность и правило PECS
|
||||
tags:
|
||||
- kotlin
|
||||
- generics
|
||||
---
|
||||
|
||||
Вариантность отвечает на один вопрос: если `String` - подтип `Any`, то является ли `Box<String>` подтипом `Box<Any>`?
|
||||
|
||||
По умолчанию - **нет**. Дженерики инвариантны:
|
||||
```kotlin
|
||||
class Box6<T>(val value: T)
|
||||
|
||||
fun main() {
|
||||
val str: Box6<String> = Box6("текст")
|
||||
// val any: Box6<Any> = str // ошибка: несовместимые типы
|
||||
println(str.value) // текст
|
||||
}
|
||||
```
|
||||
|
||||
Причина видна, если допустить обратное на изменяемой коллекции:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strings = mutableListOf("a", "b")
|
||||
// val anys: MutableList<Any> = strings // если бы компилировалось...
|
||||
// anys.add(42) // ...сюда положили бы Int
|
||||
// val s: String = strings[0] // ...и здесь получили бы ClassCastException
|
||||
println(strings.size) // 2
|
||||
}
|
||||
```
|
||||
Запрет инвариантности спасает от этой дыры. Но он же мешает там, где изменения нет.
|
||||
|
||||
## out - ковариантность
|
||||
`out T` означает, что `T` встречается только **на выходе**: в возвращаемых значениях, но не в параметрах.
|
||||
|
||||
```kotlin
|
||||
class Producer<out T>(private val value: T) {
|
||||
fun get(): T = value // T на выходе — разрешено
|
||||
// fun set(v: T) { } // ошибка: T на входе запрещён
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val strings: Producer<String> = Producer("текст")
|
||||
val anys: Producer<Any> = strings // работает!
|
||||
println(anys.get()) // текст
|
||||
}
|
||||
```
|
||||
Это безопасно: из `Producer<String>` можно только читать, а всякая прочитанная `String` является `Any`.
|
||||
|
||||
Именно так объявлен `List`:
|
||||
```kotlin
|
||||
fun printSize(items: List<Any>) = items.size
|
||||
|
||||
fun main() {
|
||||
println(printSize(listOf("a", "b"))) // 2
|
||||
println(printSize(listOf(1, 2, 3))) // 3
|
||||
|
||||
val numbers: List<Number> = listOf(1, 2.0, 3L)
|
||||
println(numbers.sumOf { it.toDouble() }) // 6.0
|
||||
}
|
||||
```
|
||||
В stdlib: `List<out E>`, `Set<out E>`, `Map<K, out V>`, `Iterable<out T>`, `Sequence<out T>`, `Pair<out A, out B>`.
|
||||
|
||||
## in - контравариантность
|
||||
`in T` означает, что `T` встречается только **на входе**: в параметрах, но не в возвращаемых типах.
|
||||
|
||||
```kotlin
|
||||
class Consumer<in T> {
|
||||
fun consume(value: T) { println("принял $value") }
|
||||
// fun produce(): T = TODO() // ошибка: T на выходе запрещён
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val anyConsumer: Consumer<Any> = Consumer()
|
||||
val stringConsumer: Consumer<String> = anyConsumer // работает!
|
||||
stringConsumer.consume("текст") // принял текст
|
||||
}
|
||||
```
|
||||
Направление перевёрнуто: `Consumer<Any>` является подтипом `Consumer<String>`. Логика простая - тот, кто умеет принимать любой `Any`, справится и со `String`.
|
||||
|
||||
Классический пример - `Comparator`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val anyComparator = Comparator<Any> { a, b -> a.hashCode() - b.hashCode() }
|
||||
val strings = mutableListOf("bb", "a")
|
||||
strings.sortWith(anyComparator) // Comparator<Any> для List<String>
|
||||
println(strings.size) // 2
|
||||
|
||||
val byLength: Comparator<CharSequence> = compareBy { it.length }
|
||||
println(listOf("ccc", "a", "bb").sortedWith(byLength)) // [a, bb, ccc]
|
||||
}
|
||||
```
|
||||
`sortedWith` принимает `Comparator<in T>`, поэтому компаратор по `CharSequence` годится для `List<String>`.
|
||||
|
||||
В stdlib: `Comparator<in T>`, `Comparable<in T>`, функциональные типы `(in P) -> out R`.
|
||||
|
||||
## Сводка
|
||||
| Объявление | Называется | `T` может быть | Направление подтипов |
|
||||
|---|---|---|---|
|
||||
| `Box<T>` | инвариантность | везде | нет связи |
|
||||
| `Box<out T>` | ковариантность | только возврат | `Box<String>` -> `Box<Any>` |
|
||||
| `Box<in T>` | контравариантность | только параметр | `Box<Any>` -> `Box<String>` |
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph co["out T: ковариантность"]
|
||||
CS["Producer<String>"] --> CA["Producer<Any>"]
|
||||
end
|
||||
subgraph contra["in T: контравариантность"]
|
||||
KA["Consumer<Any>"] --> KS["Consumer<String>"]
|
||||
end
|
||||
subgraph inv["T: инвариантность"]
|
||||
IS["Box<String>"]
|
||||
IA["Box<Any>"]
|
||||
end
|
||||
```
|
||||
|
||||
## Функциональные типы
|
||||
Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val produceString: () -> String = { "текст" }
|
||||
val produceAny: () -> Any = produceString // возврат ковариантен
|
||||
println(produceAny()) // текст
|
||||
|
||||
val acceptAny: (Any) -> Unit = { println("принял $it") }
|
||||
val acceptString: (String) -> Unit = acceptAny // параметр контравариантен
|
||||
acceptString("строка") // принял строка
|
||||
}
|
||||
```
|
||||
Тип `(P) -> R` объявлен как `Function1<in P, out R>`: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая.
|
||||
|
||||
Практическое следствие для API:
|
||||
```kotlin
|
||||
class EventBus {
|
||||
private val handlers = mutableListOf<(Any) -> Unit>()
|
||||
|
||||
fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) }
|
||||
fun publish(event: Any) = handlers.forEach { it(event) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val bus = EventBus()
|
||||
bus.subscribe { println("получено: $it") }
|
||||
bus.publish("событие") // получено: событие
|
||||
bus.publish(42) // получено: 42
|
||||
}
|
||||
```
|
||||
|
||||
## Когда помечать свой класс
|
||||
Правило простое: если параметр типа используется **только** на выходе - ставьте `out`, только на входе - `in`. Компилятор проверит и подскажет.
|
||||
|
||||
```kotlin
|
||||
interface Reader<out T> {
|
||||
fun read(): T
|
||||
fun readAll(count: Int): List<T> // List<T> — тоже выход, ок
|
||||
}
|
||||
|
||||
interface Writer<in T> {
|
||||
fun write(value: T)
|
||||
fun writeAll(values: List<T>) // List<T> на входе, ок
|
||||
}
|
||||
|
||||
interface Storage<T> : Reader<T>, Writer<T> // и то и другое — инвариантен
|
||||
|
||||
class MemoryStorage<T>(private var value: T) : Storage<T> {
|
||||
override fun read(): T = value
|
||||
override fun readAll(count: Int) = List(count) { value }
|
||||
override fun write(value: T) { this.value = value }
|
||||
override fun writeAll(values: List<T>) { values.lastOrNull()?.let { value = it } }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val storage = MemoryStorage("начальное")
|
||||
val reader: Reader<Any> = storage // ковариантность работает
|
||||
storage.write("новое")
|
||||
println(reader.read()) // новое
|
||||
println(reader.readAll(2)) // [новое, новое]
|
||||
}
|
||||
```
|
||||
Разделение на `Reader` и `Writer` - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный.
|
||||
|
||||
## Nothing и вариантность
|
||||
`Nothing` - подтип всех типов, поэтому `List<Nothing>` подходит под любой `List<T>`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val empty: List<Nothing> = emptyList()
|
||||
val ints: List<Int> = empty // работает благодаря out
|
||||
val strings: List<String> = empty
|
||||
println("${ints.size} ${strings.size}") // 0 0
|
||||
}
|
||||
```
|
||||
Именно поэтому `emptyList()` один на все типы - это синглтон `EmptyList : List<Nothing>`. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
Тот же приём в sealed-иерархиях:
|
||||
```kotlin
|
||||
sealed interface Outcome<out T> {
|
||||
data class Ok<T>(val value: T) : Outcome<T>
|
||||
data object Empty : Outcome<Nothing>
|
||||
}
|
||||
|
||||
fun describe4(o: Outcome<Int>): String = when (o) {
|
||||
is Outcome.Ok -> "значение ${o.value}"
|
||||
Outcome.Empty -> "пусто"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe4(Outcome.Ok(1))) // значение 1
|
||||
println(describe4(Outcome.Empty)) // пусто
|
||||
}
|
||||
```
|
||||
`Empty` может быть `object`, а не обобщённым классом, только потому что `Outcome` ковариантен. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Ограничение out: нельзя принимать T
|
||||
Иногда ковариантный класс всё-таки должен принять `T`. Для этого параметр объявляют с более широкой границей:
|
||||
```kotlin
|
||||
class ImmutableBox<out T>(private val value: T) {
|
||||
fun get(): T = value
|
||||
|
||||
fun <R> withDefault(default: R): R where R : Any = default
|
||||
|
||||
fun <U> plus(other: U): List<Any?> = listOf(value, other)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val box: ImmutableBox<String> = ImmutableBox("a")
|
||||
val any: ImmutableBox<Any> = box
|
||||
println(any.get()) // a
|
||||
println(box.plus(1)) // [a, 1]
|
||||
}
|
||||
```
|
||||
В stdlib этот приём виден в `List.plus`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strings: List<String> = listOf("a")
|
||||
val mixed: List<Any> = strings + 1 // plus объявлен как <T> List<T>.plus(element: T)
|
||||
println(mixed) // [a, 1]
|
||||
}
|
||||
```
|
||||
Тип `T` здесь выводится как `Any`, а не как `String`, поэтому ковариантность не нарушается.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `List<? extends T>` в каждой сигнатуре | `List<out E>` один раз в объявлении |
|
||||
> | `List<? super T>` | `MutableList<in E>` или `in` в объявлении |
|
||||
> | `Collection<?>` | `Collection<*>` |
|
||||
> | нет declaration-site variance | `out`/`in` при объявлении класса |
|
||||
> | PECS - правило для программиста | компилятор проверяет сам |
|
||||
>
|
||||
> **Главное отличие - где объявляется вариантность.** Java знает только use-site variance: интерфейс `List<E>` инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard:
|
||||
> ```java
|
||||
> void printAll(Collection<? extends Object> items) { ... }
|
||||
> double sum(List<? extends Number> numbers) { ... }
|
||||
> void addAll(List<? super Integer> dest, List<? extends Integer> src) { ... }
|
||||
> ```
|
||||
> В Kotlin `List<out E>` объявлен один раз в stdlib, и дальше всё работает само:
|
||||
> ```kotlin
|
||||
> fun sum3(numbers: List<Number>) = numbers.sumOf { it.toDouble() }
|
||||
>
|
||||
> fun main() {
|
||||
> println(sum3(listOf(1, 2, 3))) // 6.0 — List<Int> подошёл
|
||||
> }
|
||||
> ```
|
||||
> Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это [проекции типов](Проекции%20типов.md), но нужны они редко.
|
||||
>
|
||||
> **PECS из правила превратился в проверку компилятора.** В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс `out`, и компилятор откажется принять `T` в параметр: ошибка возникает при объявлении, а не у пользователя API.
|
||||
>
|
||||
> **`Comparator` в Kotlin контравариантен автоматически.** В Java `Comparator<T>` инвариантен, и методы вроде `Collections.sort` объявляют `Comparator<? super T>`. В Kotlin stdlib `Comparator` уже `in T`, поэтому `sortedWith(comparator)` принимает компаратор по любому супертипу без дополнительных обозначений.
|
||||
>
|
||||
> **Разделение `List`/`MutableList` делает `out` возможным.** В Java `List` содержит `add`, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы в `MutableList<E>` (инвариантный), и `List<out E>` стал безопасным по построению.
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`out` запрещает `T` в параметрах - включая неочевидные места:**
|
||||
> ```kotlin
|
||||
> class Holder<out T>(private val value: T) {
|
||||
> fun get(): T = value
|
||||
> // fun contains(other: T): Boolean = value == other // ошибка
|
||||
> fun contains(other: Any?): Boolean = value == other // так можно
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Holder("a").contains("a")) // true
|
||||
> }
|
||||
> ```
|
||||
> Расширение границы до `Any?` - стандартный обход, но оно теряет проверку типов: `contains(42)` тоже скомпилируется.
|
||||
>
|
||||
> **`private` функции могут нарушать вариантность:**
|
||||
> ```kotlin
|
||||
> class Mutable<out T>(private var value: T) {
|
||||
> fun get(): T = value
|
||||
> private fun set(v: T) { value = v } // private — проверка не применяется
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Mutable("a").get()) } // a
|
||||
> ```
|
||||
> Компилятор разрешает `T` на входе в `private` членах, потому что снаружи они недоступны. Но `private var value: T` - всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным.
|
||||
>
|
||||
> **`MutableList` инвариантен, и это не ошибка:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings = mutableListOf("a")
|
||||
> // val anys: MutableList<Any> = strings // не скомпилируется — и правильно
|
||||
> val readOnly: List<Any> = strings // а так можно
|
||||
> println(readOnly.size) // 1
|
||||
> }
|
||||
> ```
|
||||
> Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте `List`, а не `MutableList`.
|
||||
>
|
||||
> **Массивы инвариантны в Kotlin и ковариантны в Java:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings = arrayOf("a", "b")
|
||||
> // val anys: Array<Any> = strings // ошибка компиляции
|
||||
> println(strings.size) // 2
|
||||
> }
|
||||
> ```
|
||||
> В Java `String[]` является `Object[]`, поэтому `Object[] o = new String[1]; o[0] = 1;` компилируется и бросает `ArrayStoreException` в рантайме. Kotlin закрыл эту дыру ценой того, что `Array<Any>` не принимает `Array<String>`. Для чтения используйте `Array<out T>`.
|
||||
>
|
||||
> **Вариантность не распространяется на вложенные параметры автоматически:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val nested: List<List<String>> = listOf(listOf("a"))
|
||||
> val widened: List<List<Any>> = nested // работает: оба List ковариантны
|
||||
> println(widened.size) // 1
|
||||
>
|
||||
> val maps: List<MutableList<String>> = listOf(mutableListOf("a"))
|
||||
> // val wide: List<MutableList<Any>> = maps // не работает: MutableList инвариантен
|
||||
> println(maps.size) // 1
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`in` и `out` легко перепутать направление.** Мнемоника: `out` - значение **выходит** из объекта (producer, можно только читать), `in` - значение **входит** в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна.
|
||||
>
|
||||
> **`Map<K, out V>` ковариантен только по значению:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val m: Map<String, Int> = mapOf("a" to 1)
|
||||
> val widened: Map<String, Any> = m // ок — V ковариантен
|
||||
> // val keys: Map<Any, Int> = m // ошибка — K инвариантен
|
||||
> println(widened.size) // 1
|
||||
> }
|
||||
> ```
|
||||
> Ключ инвариантен, потому что он используется на входе в `get(key)`.
|
||||
>
|
||||
> **Ковариантный класс с `var`-полем типа `T` объявить нельзя** - сеттер принимает `T`. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий.
|
||||
|
||||
Далее: [Проекции типов](Проекции%20типов.md)
|
||||
@@ -1,393 +0,0 @@
|
||||
---
|
||||
title: Параметрический полиморфизм
|
||||
description: Обобщённые функции и классы, ограничения параметров типа и вывод типов
|
||||
tags:
|
||||
- kotlin
|
||||
- generics
|
||||
---
|
||||
|
||||
Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции.
|
||||
|
||||
```kotlin
|
||||
fun <T> firstOrDefault(items: List<T>, default: T): T =
|
||||
if (items.isEmpty()) default else items[0]
|
||||
|
||||
fun main() {
|
||||
println(firstOrDefault(listOf(1, 2), 0)) // 1
|
||||
println(firstOrDefault(emptyList(), "нет")) // нет
|
||||
// println(firstOrDefault(listOf(1), "нет")) // ошибка: T не может быть и Int, и String
|
||||
}
|
||||
```
|
||||
`<T>` объявляется **перед** именем функции. Компилятор выводит его из аргументов.
|
||||
|
||||
## Обобщённые классы
|
||||
```kotlin
|
||||
class Box5<T>(private val value: T) {
|
||||
fun get(): T = value
|
||||
fun <R> map(f: (T) -> R): Box5<R> = Box5(f(value))
|
||||
override fun toString() = "Box($value)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val intBox = Box5(42)
|
||||
println(intBox.get() + 1) // 43
|
||||
println(intBox.map { it.toString() + "!" }) // Box(42!)
|
||||
|
||||
val explicit = Box5<String>("явно")
|
||||
println(explicit.get().length) // 4
|
||||
}
|
||||
```
|
||||
Метод `map` объявляет **свой** параметр типа `R` - он не связан с `T` класса.
|
||||
|
||||
Несколько параметров:
|
||||
```kotlin
|
||||
class Pair2<A, B>(val first: A, val second: B) {
|
||||
fun swap(): Pair2<B, A> = Pair2(second, first)
|
||||
override fun toString() = "($first, $second)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val p = Pair2("ключ", 1)
|
||||
println(p) // (ключ, 1)
|
||||
println(p.swap()) // (1, ключ)
|
||||
}
|
||||
```
|
||||
|
||||
## Ограничения параметра типа
|
||||
Без ограничения у `T` доступны только методы `Any?`:
|
||||
```kotlin
|
||||
fun <T> describe(value: T): String = "значение: $value" // только toString
|
||||
|
||||
fun <T : Comparable<T>> maxOf3(a: T, b: T): T = if (a > b) a else b
|
||||
|
||||
fun <T : Number> sum2(items: List<T>): Double = items.sumOf { it.toDouble() }
|
||||
|
||||
fun main() {
|
||||
println(describe(listOf(1))) // значение: [1]
|
||||
println(maxOf3(3, 7)) // 7
|
||||
println(maxOf3("abc", "abd")) // abd
|
||||
println(sum2(listOf(1, 2.5, 3L))) // 6.5
|
||||
}
|
||||
```
|
||||
Верхняя граница после двоеточия говорит компилятору, какие методы доступны.
|
||||
|
||||
По умолчанию граница - `Any?`, то есть `T` может быть nullable:
|
||||
```kotlin
|
||||
fun <T> firstOrNull2(items: List<T>): T? = items.firstOrNull()
|
||||
|
||||
fun <T : Any> requireFirst(items: List<T>): T = items.first()
|
||||
|
||||
fun main() {
|
||||
println(firstOrNull2(listOf<String?>(null))) // null
|
||||
println(requireFirst(listOf(1, 2))) // 1
|
||||
}
|
||||
```
|
||||
`T : Any` - способ запретить nullable-аргумент типа.
|
||||
|
||||
### Несколько ограничений
|
||||
```kotlin
|
||||
interface Named4 { val name: String }
|
||||
interface Aged { val age: Int }
|
||||
|
||||
fun <T> describe2(value: T): String where T : Named4, T : Aged =
|
||||
"${value.name}, ${value.age}"
|
||||
|
||||
data class Person2(override val name: String, override val age: Int) : Named4, Aged
|
||||
|
||||
fun main() {
|
||||
println(describe2(Person2("Иван", 30))) // Иван, 30
|
||||
}
|
||||
```
|
||||
Больше одного ограничения задаётся через `where` после сигнатуры.
|
||||
|
||||
## Обобщённые функции-расширения
|
||||
```kotlin
|
||||
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
|
||||
|
||||
fun <T : Comparable<T>> List<T>.isSorted(): Boolean =
|
||||
zipWithNext().all { (a, b) -> a <= b }
|
||||
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).secondOrNull()) // 2
|
||||
println(listOf(1).secondOrNull()) // null
|
||||
println(listOf(1, 2, 3).isSorted()) // true
|
||||
println(listOf(3, 1).isSorted()) // false
|
||||
}
|
||||
```
|
||||
Это основной способ добавить обобщённую операцию к существующему типу. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
## Вывод типов
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = listOf(1, 2) // List<Int>
|
||||
val b = listOf(1, "a") // List<Any>
|
||||
val c = listOf<Number>(1, 2.0) // List<Number> — указали явно
|
||||
val d = mutableListOf<String>() // без аргументов тип не вывести
|
||||
val e = emptyMap<String, Int>()
|
||||
|
||||
println("${a::class.simpleName} ${b.size} ${c.size} ${d.size} ${e.size}")
|
||||
// ArrayList 2 2 0 0
|
||||
}
|
||||
```
|
||||
Когда аргументов нет или нужен более широкий тип, параметр указывают явно.
|
||||
|
||||
Вывод работает и через возвращаемое значение:
|
||||
```kotlin
|
||||
class Box7<T>(private val value: T?) {
|
||||
fun get(): T? = value
|
||||
}
|
||||
|
||||
fun <T> emptyBox(): Box7<T> = Box7(null)
|
||||
|
||||
fun main() {
|
||||
val box: Box7<String> = emptyBox() // T выведен из типа переменной
|
||||
println(box.get()) // null
|
||||
}
|
||||
```
|
||||
|
||||
## Обобщённые интерфейсы
|
||||
```kotlin
|
||||
interface Repository6<T, ID> {
|
||||
fun find(id: ID): T?
|
||||
fun save(item: T): ID
|
||||
fun all(): List<T>
|
||||
}
|
||||
|
||||
data class User9(val id: Int, val name: String)
|
||||
|
||||
class InMemoryUserRepository : Repository6<User9, Int> {
|
||||
private val storage = mutableMapOf<Int, User9>()
|
||||
|
||||
override fun find(id: Int) = storage[id]
|
||||
override fun save(item: User9): Int { storage[item.id] = item; return item.id }
|
||||
override fun all() = storage.values.toList()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = InMemoryUserRepository()
|
||||
repo.save(User9(1, "Иван"))
|
||||
println(repo.find(1)?.name) // Иван
|
||||
println(repo.find(2)) // null
|
||||
println(repo.all().size) // 1
|
||||
}
|
||||
```
|
||||
Реализация может зафиксировать параметры типа (как здесь) или оставить их обобщёнными:
|
||||
```kotlin
|
||||
interface Repository7<T, ID> {
|
||||
fun find(id: ID): T?
|
||||
fun save(item: T): ID
|
||||
fun all(): List<T>
|
||||
}
|
||||
|
||||
data class User10(val id: Int, val name: String)
|
||||
|
||||
class InMemoryRepository<T : Any, ID : Any>(
|
||||
private val idOf: (T) -> ID,
|
||||
) : Repository7<T, ID> {
|
||||
private val storage = mutableMapOf<ID, T>()
|
||||
|
||||
override fun find(id: ID) = storage[id]
|
||||
override fun save(item: T): ID {
|
||||
val id = idOf(item)
|
||||
storage[id] = item
|
||||
return id
|
||||
}
|
||||
override fun all() = storage.values.toList()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = InMemoryRepository<User10, Int> { it.id }
|
||||
repo.save(User10(1, "Иван"))
|
||||
println(repo.find(1)?.name) // Иван
|
||||
}
|
||||
```
|
||||
|
||||
## Стирание типов
|
||||
Параметры типа существуют только при компиляции. В рантайме их нет:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ints = listOf(1, 2)
|
||||
val strings = listOf("a", "b")
|
||||
|
||||
println(ints.javaClass == strings.javaClass) // true — один класс
|
||||
// println(ints is List<Int>) // ошибка: нельзя проверить
|
||||
println(ints is List<*>) // true — только «какой-то List»
|
||||
}
|
||||
```
|
||||
Это наследие JVM: дженерики появились в Java 5 и реализованы стиранием ради совместимости. Следствия:
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
// 1. Нельзя создать массив параметра типа напрямую
|
||||
// fun <T> makeArray(): Array<T> = arrayOf() // ошибка
|
||||
|
||||
// 2. Нельзя перегрузить по параметру типа
|
||||
// fun show(x: List<String>) = "строки"
|
||||
// fun show(x: List<Int>) = "числа" // та же JVM-сигнатура
|
||||
|
||||
// 3. Непроверяемый каст компилируется с предупреждением
|
||||
val any: Any = listOf(1, 2)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val back = any as List<String> // не упадёт здесь
|
||||
try {
|
||||
println(back[0].length) // упадёт здесь
|
||||
} catch (e: ClassCastException) {
|
||||
println("ClassCastException при обращении")
|
||||
}
|
||||
}
|
||||
```
|
||||
Обойти стирание в Kotlin можно для inline-функций через `reified`. Подробнее: [Reified и inline](Reified%20и%20inline.md)
|
||||
|
||||
## Параметры типа и значения по умолчанию
|
||||
```kotlin
|
||||
class Cache<K, V>(
|
||||
private val maxSize: Int = 100,
|
||||
private val storage: MutableMap<K, V> = LinkedHashMap(),
|
||||
) {
|
||||
fun put(key: K, value: V) {
|
||||
if (storage.size >= maxSize) storage.remove(storage.keys.first())
|
||||
storage[key] = value
|
||||
}
|
||||
|
||||
fun get(key: K): V? = storage[key]
|
||||
val size: Int get() = storage.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val cache = Cache<String, Int>(maxSize = 2)
|
||||
cache.put("a", 1)
|
||||
cache.put("b", 2)
|
||||
cache.put("c", 3) // "a" вытеснен
|
||||
println("${cache.get("a")} ${cache.get("c")} ${cache.size}") // null 3 2
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `<T> T first(List<T> x)` | `fun <T> first(x: List<T>): T` |
|
||||
> | `<T extends Number>` | `<T : Number>` |
|
||||
> | `<T extends A & B>` | `where T : A, T : B` |
|
||||
> | `new Box<>(1)` (ромб) | `Box(1)` - вывод |
|
||||
> | `List<?>` | `List<*>` |
|
||||
> | `List<? extends T>` | `List<out T>` |
|
||||
> | `List<? super T>` | `List<in T>` |
|
||||
> | стирание типов | стирание типов (то же) |
|
||||
> | нет аналога | `reified` в inline-функциях |
|
||||
>
|
||||
> **Синтаксис почти совпадает** - это сознательное решение, чтобы перенос кода был механическим. Два отличия: двоеточие вместо `extends` и `where` для нескольких границ.
|
||||
>
|
||||
> **Вариантность объявляется в месте определения.** В Java `List<? extends Number>` пишется каждый раз в сигнатуре метода (use-site variance). В Kotlin `List<out E>` объявлено один раз в самом интерфейсе, поэтому `List<String>` **является** `List<Any>` автоматически:
|
||||
> ```kotlin
|
||||
> fun printAll(items: List<Any>) = items.size
|
||||
>
|
||||
> fun main() {
|
||||
> println(printAll(listOf("a", "b"))) // 2, без всяких ? extends
|
||||
> }
|
||||
> ```
|
||||
> В Java пришлось бы объявить параметр как `List<? extends Object>`. Подробнее: [Вариантность](Вариантность.md)
|
||||
>
|
||||
> **`reified` решает главную проблему стирания.** В Java чтобы узнать тип в рантайме, передают `Class<T>`:
|
||||
> ```java
|
||||
> <T> T parse(String json, Class<T> type) { ... }
|
||||
> parse(json, User.class);
|
||||
> ```
|
||||
> В Kotlin inline-функция может объявить `reified T` и использовать `T::class` напрямую. Это убирает параметр-костыль из сигнатуры.
|
||||
>
|
||||
> **Массивы дженериков одинаково проблемны.** `new T[]` запрещён в Java, `arrayOf<T>()` - в Kotlin. Обход - `Array<Any?>` с кастом или `ArrayList`.
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Типы стираются - проверить `is List<Int>` нельзя:**
|
||||
> ```kotlin
|
||||
> fun describe3(x: Any): String = when (x) {
|
||||
> is List<*> -> "список из ${x.size}" // только так
|
||||
> is Map<*, *> -> "карта"
|
||||
> else -> "другое"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(describe3(listOf(1, 2))) // список из 2
|
||||
> println(describe3(listOf("a"))) // список из 2? нет — список из 1
|
||||
> }
|
||||
> ```
|
||||
> Отличить `List<Int>` от `List<String>` в рантайме невозможно.
|
||||
>
|
||||
> **Непроверяемый каст откладывает ошибку:**
|
||||
> ```kotlin
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> fun <T> castAll(items: List<Any>): List<T> = items as List<T>
|
||||
>
|
||||
> fun main() {
|
||||
> val wrong: List<String> = castAll(listOf(1, 2))
|
||||
> println(wrong.size) // 2 — каст «прошёл»
|
||||
> try {
|
||||
> println(wrong[0].length)
|
||||
> } catch (e: ClassCastException) {
|
||||
> println("упало только при использовании")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> `ClassCastException` возникает не в месте каста, а там, где значение используется - отлаживать это тяжело.
|
||||
>
|
||||
> **Параметр типа без ограничения почти бесполезен внутри функции.** У `T` доступны только `toString`, `equals`, `hashCode`. Если нужны операции - нужна граница.
|
||||
>
|
||||
> **`T` по умолчанию nullable:**
|
||||
> ```kotlin
|
||||
> fun <T> process3(value: T): String = value.toString()
|
||||
>
|
||||
> fun main() {
|
||||
> println(process3(null)) // null — T выведен как Nothing?
|
||||
> }
|
||||
> ```
|
||||
> Чтобы запретить это, нужно `<T : Any>`.
|
||||
>
|
||||
> **Перегрузка по параметру типа невозможна** - после стирания сигнатуры совпадают. Обход - `@JvmName`:
|
||||
> ```kotlin
|
||||
> @JvmName("sumInts")
|
||||
> fun sumAll(items: List<Int>) = items.sum()
|
||||
> fun sumAll(items: List<String>) = items.sumOf { it.length }
|
||||
>
|
||||
> fun main() { println(sumAll(listOf("ab", "c"))) } // 3
|
||||
> ```
|
||||
>
|
||||
> **Массив параметра типа создать нельзя:**
|
||||
> ```kotlin
|
||||
> class Buffer<T>(size: Int) {
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> private val items = arrayOfNulls<Any?>(size) as Array<T?>
|
||||
> operator fun set(i: Int, v: T) { items[i] = v }
|
||||
> operator fun get(i: Int): T? = items[i]
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val b = Buffer<String>(2)
|
||||
> b[0] = "a"
|
||||
> println(b[0]) // a
|
||||
> }
|
||||
> ```
|
||||
> Это стандартный обход, он безопасен, пока массив не утекает наружу как `Array<T>`.
|
||||
>
|
||||
> **Вывод типа может дать неожиданно широкий тип:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val mixed = listOf(1, "a") // List<Any>
|
||||
> val nullable = listOf(1, null) // List<Int?>
|
||||
> println("${mixed.size} ${nullable.size}") // 2 2
|
||||
> // mixed[0] + 1 // ошибка: Any не число
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Ограничение `T : Comparable<T>` не покрывает смешанные типы.** `maxOf3(1, 2.0)` не скомпилируется, потому что `Int` и `Double` не сравнимы между собой.
|
||||
>
|
||||
> **Обобщённое исключение объявить нельзя:**
|
||||
> ```kotlin
|
||||
> // class MyException<T> : Exception() // ошибка: generic class cannot extend Throwable
|
||||
> class MyException(val payload: Any?) : Exception()
|
||||
>
|
||||
> fun main() { println(MyException(1).payload) } // 1
|
||||
> ```
|
||||
> Причина - стирание: `catch` не смог бы различить типы параметров.
|
||||
|
||||
Далее: [Вариантность](Вариантность.md)
|
||||
@@ -1,331 +0,0 @@
|
||||
---
|
||||
title: Проекции типов
|
||||
description: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает
|
||||
tags:
|
||||
- kotlin
|
||||
- generics
|
||||
---
|
||||
|
||||
[Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**.
|
||||
|
||||
```kotlin
|
||||
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
|
||||
for (item in from) to.add(item)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val source = mutableListOf("a", "b")
|
||||
val target = mutableListOf<Any>(1)
|
||||
copy(source, target)
|
||||
println(target) // [1, a, b]
|
||||
}
|
||||
```
|
||||
Без `out` в сигнатуре `MutableList<String>` не подошёл бы под `MutableList<Any>`. С `out` компилятор принимает любой `MutableList<T>`, где `T` - подтип `Any`, но взамен запрещает использовать `from` на запись.
|
||||
|
||||
## out-проекция
|
||||
```kotlin
|
||||
fun sumAll2(numbers: MutableList<out Number>): Double {
|
||||
// numbers.add(1) // ошибка: add принимает Nothing
|
||||
return numbers.sumOf { it.toDouble() }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(sumAll2(mutableListOf(1, 2, 3))) // 6.0
|
||||
println(sumAll2(mutableListOf(1.5, 2.5))) // 4.0
|
||||
}
|
||||
```
|
||||
Внутри функции `MutableList<out Number>` ведёт себя как `List<Number>`: читать можно, писать нельзя. Формально тип параметра `add` становится `Nothing` - значения такого типа не существует, поэтому вызвать его невозможно.
|
||||
|
||||
## in-проекция
|
||||
Зеркальный случай - нужно только писать:
|
||||
```kotlin
|
||||
fun fillWithOnes(target: MutableList<in Int>) {
|
||||
target.add(1)
|
||||
target.add(2)
|
||||
// val x: Int = target[0] // ошибка: читается как Any?
|
||||
val any: Any? = target[0]
|
||||
println("первый: $any")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val numbers = mutableListOf<Number>(0.5)
|
||||
fillWithOnes(numbers)
|
||||
println(numbers) // [0.5, 1, 2]
|
||||
|
||||
val anys = mutableListOf<Any>("x")
|
||||
fillWithOnes(anys)
|
||||
println(anys) // [x, 1, 2]
|
||||
}
|
||||
```
|
||||
`MutableList<in Int>` принимает `MutableList<Int>`, `MutableList<Number>`, `MutableList<Any>` - любой список, в который `Int` поместится. Чтение даёт `Any?`, потому что компилятор не знает фактический тип элементов.
|
||||
|
||||
| Проекция | Можно | Нельзя | Принимает |
|
||||
|---|---|---|---|
|
||||
| `MutableList<out T>` | читать как `T` | писать | `MutableList<подтип T>` |
|
||||
| `MutableList<in T>` | писать `T` | читать как `T` (только `Any?`) | `MutableList<супертип T>` |
|
||||
| `MutableList<T>` | всё | - | только точно `MutableList<T>` |
|
||||
|
||||
Это ровно PECS из Java: **P**roducer - `out`, **C**onsumer - `in`.
|
||||
|
||||
## Копирование с обеими проекциями
|
||||
```kotlin
|
||||
fun <T> transfer(from: MutableList<out T>, to: MutableList<in T>) {
|
||||
for (item in from) to.add(item)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val source = mutableListOf(1, 2, 3)
|
||||
val target = mutableListOf<Number>(0.0)
|
||||
transfer(source, target)
|
||||
println(target) // [0.0, 1, 2, 3]
|
||||
}
|
||||
```
|
||||
Здесь `from` - производитель, `to` - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник.
|
||||
|
||||
## Star-проекция
|
||||
Когда параметр типа не важен вообще, пишут `*`:
|
||||
```kotlin
|
||||
fun describeSize(collection: Collection<*>): String = "элементов: ${collection.size}"
|
||||
|
||||
fun main() {
|
||||
println(describeSize(listOf(1, 2))) // элементов: 2
|
||||
println(describeSize(setOf("a"))) // элементов: 1
|
||||
println(describeSize(mapOf("a" to 1).values)) // элементов: 1
|
||||
}
|
||||
```
|
||||
|
||||
`*` - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list: MutableList<*> = mutableListOf(1, 2)
|
||||
|
||||
println(list.size) // 2 — не зависит от T
|
||||
val item: Any? = list[0] // чтение даёт Any?
|
||||
println(item) // 1
|
||||
// list.add(3) // ошибка: параметр имеет тип Nothing
|
||||
list.removeAt(0) // не требует T — разрешено
|
||||
println(list) // [2]
|
||||
}
|
||||
```
|
||||
|
||||
| Объявление | `*` означает |
|
||||
|---|---|
|
||||
| `Foo<out T : TUpper>` | чтение как `TUpper` |
|
||||
| `Foo<in T>` | запись как `Nothing` (невозможна) |
|
||||
| `Foo<T>` | чтение `TUpper`, запись `Nothing` |
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map: Map<*, *> = mapOf("a" to 1)
|
||||
println(map.size) // 1
|
||||
println(map.keys.first()) // a — тип Any?
|
||||
println(map.values.first()) // 1 — тип Any?
|
||||
|
||||
val function: (Nothing) -> Any? = { "любая функция" }
|
||||
println(function::class.simpleName) // null — у лямбды нет имени класса
|
||||
}
|
||||
```
|
||||
|
||||
## Star-проекция и проверка типов
|
||||
Главное применение `*` - проверки `is`, где из-за стирания типов иначе никак:
|
||||
```kotlin
|
||||
fun classify(value: Any): String = when (value) {
|
||||
is List<*> -> "список из ${value.size}"
|
||||
is Map<*, *> -> "карта из ${value.size}"
|
||||
is Set<*> -> "множество"
|
||||
is Array<*> -> "массив из ${value.size}"
|
||||
else -> "что-то ещё"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(classify(listOf(1, 2))) // список из 2
|
||||
println(classify(mapOf("a" to 1))) // карта из 1
|
||||
println(classify(arrayOf(1, 2, 3))) // массив из 3
|
||||
println(classify(42)) // что-то ещё
|
||||
}
|
||||
```
|
||||
`is List<Int>` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md)
|
||||
|
||||
Безопасная работа с содержимым после такой проверки:
|
||||
```kotlin
|
||||
fun sumIfNumbers(value: Any): Double? {
|
||||
if (value !is List<*>) return null
|
||||
return value.filterIsInstance<Number>().sumOf { it.toDouble() }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(sumIfNumbers(listOf(1, 2.5))) // 3.5
|
||||
println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано
|
||||
println(sumIfNumbers("строка")) // null
|
||||
}
|
||||
```
|
||||
`filterIsInstance` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания.
|
||||
|
||||
## Когда проекция, когда параметр типа
|
||||
```kotlin
|
||||
fun printFirst(items: List<*>) = println(items.firstOrNull())
|
||||
|
||||
fun <T> firstOrNull3(items: List<T>): T? = items.firstOrNull()
|
||||
|
||||
fun main() {
|
||||
printFirst(listOf("a")) // a — но тип потерян
|
||||
val value: String? = firstOrNull3(listOf("a"))
|
||||
println(value?.length) // 1 — тип сохранён
|
||||
}
|
||||
```
|
||||
Правило: если тип нужен в возвращаемом значении - объявляйте `<T>`. Проекция подходит, когда тип внутри функции действительно не используется.
|
||||
|
||||
```kotlin
|
||||
fun <T> copyAll(from: List<T>, to: MutableList<in T>) {
|
||||
to.addAll(from)
|
||||
}
|
||||
|
||||
fun countAll(collections: List<Collection<*>>): Int = collections.sumOf { it.size }
|
||||
|
||||
fun main() {
|
||||
val target = mutableListOf<Any>()
|
||||
copyAll(listOf(1, 2), target)
|
||||
println(target) // [1, 2]
|
||||
println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3
|
||||
}
|
||||
```
|
||||
|
||||
## Проекции в своих классах
|
||||
```kotlin
|
||||
class Container<T>(private val items: MutableList<T>) {
|
||||
fun add(item: T) { items.add(item) }
|
||||
fun get(index: Int): T = items[index]
|
||||
val size: Int get() = items.size
|
||||
|
||||
fun addAllFrom(other: Container<out T>) {
|
||||
repeat(other.size) { items.add(other.get(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val numbers = Container<Number>(mutableListOf(0))
|
||||
val ints = Container(mutableListOf(1, 2))
|
||||
|
||||
numbers.addAllFrom(ints) // Container<Int> подошёл под Container<out Number>
|
||||
println(numbers.size) // 3
|
||||
println(numbers.get(1)) // 1
|
||||
}
|
||||
```
|
||||
Без `out` в `addAllFrom` пришлось бы либо делать класс ковариантным (невозможно - есть `add`), либо требовать точного совпадения типов.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `List<? extends T>` | `List<out T>` |
|
||||
> | `List<? super T>` | `List<in T>` |
|
||||
> | `List<?>` | `List<*>` |
|
||||
> | `Class<?>` | `KClass<*>` |
|
||||
> | wildcard нужен почти всегда | проекция нужна редко |
|
||||
> | `? extends` на чтение, `? super` на запись | то же самое |
|
||||
>
|
||||
> **Это тот же механизм, что wildcard в Java.** `MutableList<out Number>` и `List<? extends Number>` означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно.
|
||||
>
|
||||
> **Разница в том, как часто это нужно.** В Java `List` инвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию:
|
||||
> ```java
|
||||
> double sum(Collection<? extends Number> items) { ... }
|
||||
> void addAll(List<? super String> dest) { ... }
|
||||
> ```
|
||||
> В Kotlin `List<out E>` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов:
|
||||
> ```kotlin
|
||||
> fun sum4(items: Collection<Number>) = items.sumOf { it.toDouble() }
|
||||
>
|
||||
> fun main() { println(sum4(listOf(1, 2))) } // 3.0
|
||||
> ```
|
||||
> Практическое следствие: `out`/`in` в сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-only `List`.
|
||||
>
|
||||
> **Синтаксис короче и симметричен объявлению.** В Java `extends`/`super` в wildcard никак не связаны с объявлением класса. В Kotlin те же `out`/`in`, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе».
|
||||
>
|
||||
> **`List<*>` и `List<?>` ведут себя одинаково**, но `*` в Kotlin учитывает верхнюю границу параметра. Для `class Box<T : Number>` чтение из `Box<*>` даёт `Number`, а не `Object`:
|
||||
> ```kotlin
|
||||
> class NumberBox<T : Number>(val value: T)
|
||||
>
|
||||
> fun main() {
|
||||
> val box: NumberBox<*> = NumberBox(42)
|
||||
> val n: Number = box.value // Number, а не Any?
|
||||
> println(n.toDouble()) // 42.0
|
||||
> }
|
||||
> ```
|
||||
> Java в этой ситуации тоже выводит границу, но менее последовательно - через capture conversion, которая не всегда видна в сообщениях об ошибках.
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`out`-проекция запрещает запись полностью:**
|
||||
> ```kotlin
|
||||
> fun broken(items: MutableList<out Number>) {
|
||||
> // items.add(1) // ошибка даже для Int
|
||||
> // items[0] = 1 // тоже ошибка
|
||||
> items.removeAt(0) // а это можно — не требует T
|
||||
> println(items.size)
|
||||
> }
|
||||
>
|
||||
> fun main() { broken(mutableListOf(1, 2)) } // 1
|
||||
> ```
|
||||
> Запрещены только методы, принимающие `T`. `clear`, `removeAt`, `size` работают - это иногда удивляет, потому что коллекция вроде бы «только для чтения».
|
||||
>
|
||||
> **`in`-проекция теряет тип при чтении:**
|
||||
> ```kotlin
|
||||
> fun readBack(items: MutableList<in String>) {
|
||||
> items.add("новый")
|
||||
> val value = items.last() // Any?, не String
|
||||
> println(value.toString().length) // приходится приводить
|
||||
> }
|
||||
>
|
||||
> fun main() { readBack(mutableListOf("a")) } // 6
|
||||
> ```
|
||||
>
|
||||
> **Проекция `*` не равна `Any?`:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val stars: MutableList<*> = mutableListOf(1, 2)
|
||||
> val anys: MutableList<Any?> = mutableListOf(1, 2)
|
||||
>
|
||||
> // stars.add(3) // ошибка
|
||||
> anys.add(3) // работает
|
||||
> println("${stars.size} ${anys.size}") // 2 3
|
||||
> }
|
||||
> ```
|
||||
> `MutableList<Any?>` - конкретный тип, в который можно класть что угодно. `MutableList<*>` - неизвестный тип, в который нельзя класть ничего.
|
||||
>
|
||||
> **`Array<*>` и `Array<Any?>` тоже разные** - последний не примет `Array<String>`, потому что массивы инвариантны.
|
||||
>
|
||||
> **После `is List<*>` элементы имеют тип `Any?`:**
|
||||
> ```kotlin
|
||||
> fun process4(value: Any) {
|
||||
> if (value is List<*>) {
|
||||
> // value.sumOf { it } // ошибка: it — Any?
|
||||
> println(value.filterIsInstance<Int>().sum())
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { process4(listOf(1, 2, "x")) } // 3
|
||||
> ```
|
||||
> Умное приведение даёт только `List<Any?>`. Содержимое нужно проверять поэлементно.
|
||||
>
|
||||
> **`is List<Int>` не компилируется, а `as List<Int>` компилируется с предупреждением** - и откладывает `ClassCastException` до момента использования:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val any: Any = listOf("a", "b")
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> val ints = any as List<Int>
|
||||
> println(ints.size) // 2 — каст «прошёл»
|
||||
> try {
|
||||
> println(ints[0] + 1)
|
||||
> } catch (e: ClassCastException) {
|
||||
> println("упало при обращении, а не при касте")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Проекцию легко применить там, где нужен параметр типа.** `fun first(items: List<*>): Any?` теряет тип результата; `fun <T> first(items: List<T>): T?` его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем.
|
||||
>
|
||||
> **Проекция `out` на уже ковариантном типе бессмысленна.** `List<out String>` компилируется, но ничего не добавляет к `List<String>`, потому что `List` уже объявлен как `List<out E>`. Компилятор выдаёт предупреждение о лишней проекции.
|
||||
>
|
||||
> **Внутри класса `this` не проецируется.** Если метод принимает `Container<out T>`, то `other.add(x)` запрещён, но `this.add(x)` работает - проекция применена только к параметру.
|
||||
|
||||
Далее: [Reified и inline](Reified%20и%20inline.md)
|
||||
@@ -1,383 +0,0 @@
|
||||
---
|
||||
title: Gradle и Kotlin DSL
|
||||
description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- gradle
|
||||
---
|
||||
|
||||
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
|
||||
application { mainClass.set("com.example.MainKt") }
|
||||
|
||||
tasks.test { useJUnitPlatform() }
|
||||
```
|
||||
Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином.
|
||||
|
||||
## Файлы сборки
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов |
|
||||
| `build.gradle.kts` | конфигурация модуля |
|
||||
| `gradle.properties` | флаги Gradle, память JVM |
|
||||
| `gradle/libs.versions.toml` | version catalog |
|
||||
| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle |
|
||||
| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle |
|
||||
| `buildSrc/` или `build-logic/` | общая логика сборки |
|
||||
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
rootProject.name = "myapp"
|
||||
|
||||
include("core", "api", "app")
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories { mavenCentral() }
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
# gradle.properties
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
kotlin.incremental=true
|
||||
```
|
||||
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
|
||||
|
||||
## Конфигурации зависимостей
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
|
||||
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
|
||||
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
|
||||
testImplementation(kotlin("test"))
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
}
|
||||
```
|
||||
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|
||||
|---|---|---|---|
|
||||
| `implementation` | да | да | нет |
|
||||
| `api` | да | да | да |
|
||||
| `compileOnly` | да | нет | нет |
|
||||
| `runtimeOnly` | нет | да | да |
|
||||
| `testImplementation` | только тесты | только тесты | нет |
|
||||
| `ksp` | обработка аннотаций | нет | нет |
|
||||
|
||||
Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
|
||||
|
||||
Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость.
|
||||
|
||||
Для библиотеки нужен плагин `java-library`:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
`java-library`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
}
|
||||
```
|
||||
Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка.
|
||||
|
||||
## Version catalog
|
||||
Версии в одном месте, типизированный доступ из DSL:
|
||||
```
|
||||
# gradle/libs.versions.toml
|
||||
[versions]
|
||||
kotlin = "2.4.0"
|
||||
coroutines = "1.11.0"
|
||||
ktor = "3.0.0"
|
||||
|
||||
[libraries]
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
|
||||
[bundles]
|
||||
async = ["coroutines-core", "ktor-client"]
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
|
||||
```
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.async)
|
||||
implementation(libs.coroutines.core)
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
```
|
||||
Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна.
|
||||
|
||||
Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
|
||||
|
||||
## Настройка компилятора
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_4)
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_2)
|
||||
allWarningsAsErrors.set(true)
|
||||
progressiveMode.set(true)
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xjsr305=strict",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
| Опция | Зачем |
|
||||
|---|---|
|
||||
| `jvmToolchain(n)` | Gradle сам скачает нужный JDK |
|
||||
| `jvmTarget` | версия байт-кода |
|
||||
| `languageVersion` | какие возможности языка разрешены |
|
||||
| `apiVersion` | какой API stdlib доступен |
|
||||
| `allWarningsAsErrors` | предупреждения ломают сборку |
|
||||
| `progressiveMode` | исправления применяются сразу |
|
||||
| `freeCompilerArgs` | произвольные флаги |
|
||||
|
||||
`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md)
|
||||
|
||||
## Таски
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
tasks.register("generateVersion") {
|
||||
val outputDir = layout.buildDirectory.dir("generated/version")
|
||||
val version = project.version.toString()
|
||||
outputs.dir(outputDir)
|
||||
doLast {
|
||||
val file = outputDir.get().file("Version.kt").asFile
|
||||
file.parentFile.mkdirs()
|
||||
file.writeText("const val APP_VERSION = \"$version\"\n")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("compileKotlin") {
|
||||
dependsOn("generateVersion")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
|
||||
testLogging {
|
||||
events("passed", "failed", "skipped")
|
||||
}
|
||||
}
|
||||
```
|
||||
Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку.
|
||||
|
||||
Частые таски:
|
||||
|
||||
| Таск | Что делает |
|
||||
|---|---|
|
||||
| `build` | компиляция + тесты + сборка артефактов |
|
||||
| `assemble` | только артефакты, без тестов |
|
||||
| `test` | тесты |
|
||||
| `clean` | удалить `build/` |
|
||||
| `run` | запуск (плагин `application`) |
|
||||
| `dependencies` | дерево зависимостей |
|
||||
| `tasks --all` | список всех тасков |
|
||||
| `kotlinDslAccessorsReport` | доступные аксессоры DSL |
|
||||
|
||||
```
|
||||
./gradlew build --no-daemon
|
||||
./gradlew test --tests "com.example.UserServiceTest"
|
||||
./gradlew dependencies --configuration runtimeClasspath
|
||||
./gradlew build --scan # отчёт о производительности
|
||||
./gradlew --profile build # локальный профиль сборки
|
||||
```
|
||||
|
||||
## Многомодульный проект
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts # общая конфигурация
|
||||
├── gradle/libs.versions.toml
|
||||
├── core/build.gradle.kts
|
||||
├── api/build.gradle.kts
|
||||
└── app/build.gradle.kts
|
||||
```
|
||||
```kotlin
|
||||
// корневой build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0" apply false
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
"testImplementation"(kotlin("test"))
|
||||
}
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":api"))
|
||||
}
|
||||
```
|
||||
`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
|
||||
|
||||
Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
|
||||
|
||||
Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Kotlin DSL против Groovy DSL
|
||||
| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) |
|
||||
|---|---|
|
||||
| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` |
|
||||
| `ext.version = '1.0'` | `val version = "1.0"` |
|
||||
| `task hello { ... }` | `tasks.register("hello") { ... }` |
|
||||
| `apply plugin: 'java'` | `plugins { java }` |
|
||||
| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` |
|
||||
| динамический, без проверок | типизированный, с автодополнением |
|
||||
| быстрее первая конфигурация | требует компиляции скрипта |
|
||||
|
||||
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
|
||||
|
||||
## Публикация
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0"
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
from(components["java"])
|
||||
pom {
|
||||
name.set("My Library")
|
||||
description.set("Описание библиотеки")
|
||||
licenses {
|
||||
license {
|
||||
name.set("Apache-2.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java-мир | Kotlin-мир |
|
||||
> |---|---|
|
||||
> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL |
|
||||
> | `<dependency><scope>compile</scope>` | `implementation` / `api` |
|
||||
> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` |
|
||||
> | `<properties>` для версий | `libs.versions.toml` |
|
||||
> | annotation processor path | конфигурация `ksp` |
|
||||
> | XML, декларативно | код, императивно |
|
||||
>
|
||||
> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
|
||||
>
|
||||
> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.
|
||||
>
|
||||
> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости.
|
||||
>
|
||||
> **Version catalog - ответ на `<dependencyManagement>`.** В Maven версии централизуются через BOM и `<properties>`, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`.
|
||||
>
|
||||
> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
|
||||
>
|
||||
> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым.
|
||||
>
|
||||
> **Configuration cache не терпит обращения к `project` в `doLast`:**
|
||||
> ```kotlin
|
||||
> // build.gradle.kts
|
||||
> // неправильно: project читается во время выполнения
|
||||
> tasks.register("bad") {
|
||||
> doLast { println(project.version) }
|
||||
> }
|
||||
>
|
||||
> // правильно: значение захвачено в фазе конфигурации
|
||||
> tasks.register("good") {
|
||||
> val v = project.version.toString()
|
||||
> doLast { println(v) }
|
||||
> }
|
||||
> ```
|
||||
> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
|
||||
>
|
||||
> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
|
||||
>
|
||||
> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI.
|
||||
>
|
||||
> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся.
|
||||
>
|
||||
> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`.
|
||||
>
|
||||
> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md)
|
||||
>
|
||||
> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.
|
||||
>
|
||||
> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум.
|
||||
>
|
||||
> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела.
|
||||
>
|
||||
> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.
|
||||
>
|
||||
> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку.
|
||||
|
||||
Далее: [Структура проекта](Структура%20проекта.md)
|
||||
@@ -1,383 +0,0 @@
|
||||
---
|
||||
title: KSP
|
||||
description: Kotlin Symbol Processing — кодогенерация по аннотациям, написание процессора, миграция с kapt
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- ksp
|
||||
---
|
||||
|
||||
KSP (Kotlin Symbol Processing) - механизм генерации кода по аннотациям, работающий с Kotlin напрямую. Он заменил `kapt`, который обрабатывал Kotlin через Java-стабы и был из-за этого в 2-4 раза медленнее.
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("com.google.devtools.ksp") version "2.3.9"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("com.squareup.moshi:moshi:1.15.2")
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
}
|
||||
```
|
||||
Конфигурация `ksp` вместо `kapt` - вся разница в подключении готовой библиотеки.
|
||||
|
||||
## Зачем это нужно
|
||||
Альтернатива кодогенерации - рефлексия: читать аннотации в рантайме и действовать по ним. Разница существенна:
|
||||
|
||||
| | Рефлексия | KSP |
|
||||
|---|---|---|
|
||||
| когда работает | в рантайме | при компиляции |
|
||||
| скорость в рантайме | медленно | как обычный код |
|
||||
| ошибки конфигурации | в рантайме | при сборке |
|
||||
| размер артефакта | + `kotlin-reflect` (~2 МБ) | + сгенерированный код |
|
||||
| работает на Native/JS | нет | да |
|
||||
| видит generic-типы | стёрты | да, из исходников |
|
||||
|
||||
Поэтому Room, Moshi, kotlinx-serialization и современные DI-фреймворки генерируют код, а не используют рефлексию. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
|
||||
|
||||
## Библиотеки на KSP
|
||||
| Библиотека | Что генерирует |
|
||||
|---|---|
|
||||
| Room | реализации DAO и миграции БД |
|
||||
| Moshi | адаптеры JSON |
|
||||
| Dagger / Hilt | графы зависимостей |
|
||||
| kotlin-inject | DI без рефлексии |
|
||||
| Ktorfit | реализации HTTP-интерфейсов |
|
||||
| Arrow Optics | линзы для иммутабельных структур |
|
||||
| Auto-Service | `META-INF/services` |
|
||||
| Showkase | каталог Compose-компонентов |
|
||||
|
||||
`kotlinx-serialization` сюда не входит: это плагин компилятора, он модифицирует IR, а не генерирует файлы. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Как это работает
|
||||
```mermaid
|
||||
graph LR
|
||||
A["исходники .kt"] --> B["KSP:<br/>анализ символов"]
|
||||
B --> C["процессор:<br/>генерация .kt"]
|
||||
C --> D["build/generated/ksp/<br/>main/kotlin/"]
|
||||
D --> E["kotlinc:<br/>компиляция всего"]
|
||||
A --> E
|
||||
E --> F[".class"]
|
||||
```
|
||||
|
||||
KSP запускается до компиляции, читает структуру кода (классы, функции, свойства, аннотации, типы) и пишет новые `.kt`-файлы в `build/generated/ksp/main/kotlin/`. Затем компилятор собирает исходники вместе со сгенерированными.
|
||||
|
||||
Ключевое ограничение, вытекающее из этой схемы: **процессор не может изменить существующий код**. Только добавить новый. Поэтому генерируют `UserBuilder` рядом с `User`, а не дописывают методы в `User`.
|
||||
|
||||
## Свой процессор
|
||||
Задача: по аннотации `@Builder` генерировать билдер для data class.
|
||||
|
||||
**Модуль процессора:**
|
||||
```kotlin
|
||||
// processor/build.gradle.kts
|
||||
plugins { kotlin("jvm") version "2.4.0" }
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9")
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
```
|
||||
|
||||
**Процессор:**
|
||||
```kotlin
|
||||
import com.google.devtools.ksp.processing.CodeGenerator
|
||||
import com.google.devtools.ksp.processing.Dependencies
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.validate
|
||||
|
||||
class BuilderProcessor(
|
||||
private val codeGenerator: CodeGenerator,
|
||||
private val logger: KSPLogger,
|
||||
) : SymbolProcessor {
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val symbols = resolver
|
||||
.getSymbolsWithAnnotation("demo.Builder")
|
||||
.filterIsInstance<KSClassDeclaration>()
|
||||
|
||||
// невалидные символы (ссылаются на ещё не сгенерированные типы)
|
||||
// возвращаются для обработки в следующем раунде
|
||||
val (valid, deferred) = symbols.partition { it.validate() }
|
||||
|
||||
valid.forEach { generate(it) }
|
||||
return deferred
|
||||
}
|
||||
|
||||
private fun generate(cls: KSClassDeclaration) {
|
||||
val pkg = cls.packageName.asString()
|
||||
val name = cls.simpleName.asString()
|
||||
val params = cls.primaryConstructor?.parameters.orEmpty()
|
||||
|
||||
val fields = params.joinToString("\n") { p ->
|
||||
val n = p.name!!.asString()
|
||||
val t = p.type.resolve().declaration.qualifiedName!!.asString()
|
||||
" var $n: $t? = null"
|
||||
}
|
||||
val missing = "не задан"
|
||||
val args = params.joinToString(", ") { p ->
|
||||
val n = p.name!!.asString()
|
||||
"$n = $n ?: error(\"$n $missing\")"
|
||||
}
|
||||
|
||||
codeGenerator.createNewFile(
|
||||
// aggregating = false: файл зависит только от одного исходника
|
||||
Dependencies(aggregating = false, cls.containingFile!!),
|
||||
pkg,
|
||||
"${name}Builder",
|
||||
).bufferedWriter().use { w ->
|
||||
w.write("package $pkg\n\n")
|
||||
w.write("class ${name}Builder {\n")
|
||||
w.write("$fields\n")
|
||||
w.write(" fun build() = $name($args)\n")
|
||||
w.write("}\n")
|
||||
}
|
||||
logger.info("сгенерирован ${name}Builder")
|
||||
}
|
||||
}
|
||||
|
||||
class BuilderProcessorProvider : SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
|
||||
BuilderProcessor(environment.codeGenerator, environment.logger)
|
||||
}
|
||||
```
|
||||
|
||||
**Регистрация через ServiceLoader** - файл `processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`:
|
||||
```
|
||||
BuilderProcessorProvider
|
||||
```
|
||||
|
||||
**Подключение:**
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("com.google.devtools.ksp") version "2.3.9"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
ksp(project(":processor"))
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
|
||||
application { mainClass.set("demo.MainKt") }
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
```kotlin
|
||||
// app/src/main/kotlin/demo/Main.kt
|
||||
// @Target(AnnotationTarget.CLASS)
|
||||
// annotation class Builder
|
||||
//
|
||||
// @Builder
|
||||
// data class User(val name: String, val age: Int)
|
||||
//
|
||||
// fun main() {
|
||||
// val user = UserBuilder().apply {
|
||||
// name = "Иван"
|
||||
// age = 30
|
||||
// }.build()
|
||||
// println(user) // User(name=Иван, age=30)
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("User(name=Иван, age=30)") // User(name=Иван, age=30)
|
||||
}
|
||||
```
|
||||
Результат в `app/build/generated/ksp/main/kotlin/demo/UserBuilder.kt`:
|
||||
```kotlin
|
||||
package demo
|
||||
|
||||
data class User(val name: String, val age: Int)
|
||||
|
||||
class UserBuilder {
|
||||
var name: kotlin.String? = null
|
||||
var age: kotlin.Int? = null
|
||||
fun build() = User(name = name ?: error("name не задан"), age = age ?: error("age не задан"))
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(UserBuilder().apply { name = "Иван"; age = 30 }.build())
|
||||
// User(name=Иван, age=30)
|
||||
}
|
||||
```
|
||||
Подробнее про сам паттерн: [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
|
||||
|
||||
## API KSP
|
||||
| Тип | Что представляет |
|
||||
|---|---|
|
||||
| `Resolver` | точка доступа ко всем символам |
|
||||
| `KSClassDeclaration` | класс, интерфейс, объект, enum |
|
||||
| `KSFunctionDeclaration` | функция или конструктор |
|
||||
| `KSPropertyDeclaration` | свойство |
|
||||
| `KSValueParameter` | параметр функции |
|
||||
| `KSTypeReference` | ссылка на тип (ленивая) |
|
||||
| `KSType` | разрешённый тип с аргументами и nullability |
|
||||
| `KSAnnotation` | аннотация с аргументами |
|
||||
| `KSFile` | файл с исходниками |
|
||||
| `CodeGenerator` | запись новых файлов |
|
||||
| `KSPLogger` | сообщения в лог сборки |
|
||||
|
||||
```kotlin
|
||||
// типичные операции (псевдокод в комментариях — требует KSP в classpath)
|
||||
// resolver.getSymbolsWithAnnotation("demo.Builder") // по аннотации
|
||||
// resolver.getAllFiles() // все файлы
|
||||
// resolver.getClassDeclarationByName("demo.User") // по имени
|
||||
// cls.getAllProperties() // свойства, включая наследованные
|
||||
// cls.primaryConstructor?.parameters // параметры конструктора
|
||||
// cls.superTypes // супертипы
|
||||
// type.resolve().isMarkedNullable // nullable ли
|
||||
// type.resolve().arguments // аргументы дженерика
|
||||
// cls.modifiers // DATA, SEALED, ABSTRACT...
|
||||
// cls.annotations.filter { it.shortName.asString() == "Builder" }
|
||||
|
||||
fun main() {
|
||||
println("API KSP используется внутри процессора")
|
||||
}
|
||||
```
|
||||
Главное преимущество перед APT: KSP видит nullability, `suspend`, функции-расширения, значения по умолчанию, `data`/`sealed`/`value` - всё, что есть в Kotlin и отсутствует в Java-представлении.
|
||||
|
||||
## Многораундовая обработка
|
||||
`process()` может вызываться несколько раз. Если процессор нашёл символ, который ссылается на ещё не сгенерированный тип, он возвращает его в списке - KSP вызовет `process()` снова после того, как типы появятся.
|
||||
```kotlin
|
||||
// внутри процессора:
|
||||
// val (valid, deferred) = symbols.partition { it.validate() }
|
||||
// valid.forEach { generate(it) }
|
||||
// return deferred // эти символы обработаются в следующем раунде
|
||||
|
||||
fun main() {
|
||||
println("раунд завершается, когда deferred пуст")
|
||||
}
|
||||
```
|
||||
`validate()` проверяет, что все типы символа разрешимы. Возврат пустого списка означает «всё обработано».
|
||||
|
||||
## Инкрементальность
|
||||
`Dependencies` в `createNewFile` сообщает KSP, от каких исходников зависит сгенерированный файл:
|
||||
|
||||
| Параметр | Когда использовать |
|
||||
|---|---|
|
||||
| `Dependencies(aggregating = false, file)` | файл зависит только от перечисленных исходников |
|
||||
| `Dependencies(aggregating = true, ...)` | файл зависит от всех обработанных символов |
|
||||
| `Dependencies.ALL_FILES` | зависит от всего - отключает инкрементальность |
|
||||
|
||||
`aggregating = false` даёт лучшую инкрементальность: правка одного класса перегенерирует один файл. `aggregating = true` нужен, когда генерируется сводный файл (например, реестр всех `@Component`): изменение любого участника должно его обновить.
|
||||
|
||||
Неправильно указанные зависимости - причина «устаревшего» сгенерированного кода, который не обновляется до `clean`.
|
||||
|
||||
## Опции процессора
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
ksp {
|
||||
arg("builderPrefix", "Make")
|
||||
arg("verbose", "true")
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// в процессоре:
|
||||
// class Provider : SymbolProcessorProvider {
|
||||
// override fun create(env: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
// val prefix = env.options["builderPrefix"] ?: "Builder"
|
||||
// return MyProcessor(env.codeGenerator, prefix)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("опции доступны через environment.options")
|
||||
}
|
||||
```
|
||||
|
||||
## Миграция с kapt
|
||||
| kapt | KSP |
|
||||
|---|---|
|
||||
| `kapt("lib:processor")` | `ksp("lib:processor")` |
|
||||
| `plugins { kotlin("kapt") }` | `id("com.google.devtools.ksp")` |
|
||||
| `kapt { arguments { arg(...) } }` | `ksp { arg(...) } ` |
|
||||
| `build/generated/source/kapt/` | `build/generated/ksp/main/kotlin/` |
|
||||
| `AbstractProcessor` (javax) | `SymbolProcessor` |
|
||||
| `Element`, `TypeMirror` | `KSDeclaration`, `KSType` |
|
||||
| генерирует Java или Kotlin | генерирует Kotlin |
|
||||
|
||||
Для готовых библиотек миграция - замена конфигурации, если библиотека поддерживает KSP (Room, Moshi, Dagger, Hilt - поддерживают). Для своего процессора нужно переписывать: API принципиально другой.
|
||||
|
||||
Смысл перехода - скорость. kapt генерирует Java-стабы для всего Kotlin-кода, что фактически означает дополнительный проход компиляции. Выигрыш обычно 2-4 раза на фазе обработки аннотаций.
|
||||
|
||||
## Диагностика
|
||||
```
|
||||
# что сгенерировано
|
||||
find build/generated/ksp -name "*.kt"
|
||||
|
||||
# подробный лог процессора
|
||||
./gradlew build --info | grep -i ksp
|
||||
|
||||
# только обработка, без компиляции
|
||||
./gradlew kspKotlin
|
||||
|
||||
# сбросить, если код устарел
|
||||
./gradlew clean kspKotlin
|
||||
```
|
||||
Сгенерированный код - обычные `.kt`-файлы: их можно открыть и прочитать. Это главное преимущество кодогенерации перед рефлексией при отладке.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java (APT) | Kotlin (KSP) |
|
||||
> |---|---|
|
||||
> | `AbstractProcessor` | `SymbolProcessor` |
|
||||
> | `javax.lang.model.Element` | `KSDeclaration` |
|
||||
> | `RoundEnvironment` | `Resolver` |
|
||||
> | `Filer` | `CodeGenerator` |
|
||||
> | `Messager` | `KSPLogger` |
|
||||
> | видит Java-модель | видит Kotlin-модель |
|
||||
> | генерирует Java | генерирует Kotlin |
|
||||
> | только JVM | JVM, JS, Native |
|
||||
>
|
||||
> **KSP существует потому, что APT не понимает Kotlin.** Модель `javax.lang.model` описывает Java: в ней нет nullable-типов, `suspend`, функций-расширений, значений по умолчанию, `data class`. Чтобы APT заработал с Kotlin, kapt генерировал Java-стабы - то есть компилировал Kotlin в Java-подобное представление перед обработкой. Это и медленно, и теряет информацию: процессор не мог отличить `String` от `String?`.
|
||||
>
|
||||
> **Запрет на модификацию кода - сознательное ограничение, как и в APT.** Процессор может только добавлять файлы. Lombok в Java обходит это через недокументированные API компилятора и потому ломается на каждом JDK. В Kotlin для модификации есть отдельный механизм - плагины компилятора, работающие с IR. Разделение чёткое: KSP стабилен и прост, плагины мощны и нестабильны.
|
||||
>
|
||||
> **Кодогенерация заменяет рефлексию сильнее, чем в Java.** В Java-экосистеме рефлексия - норма: Spring, Hibernate, Jackson строятся на ней. В Kotlin рефлексия дороже (нужен `kotlin-reflect` ~2 МБ, который читает метаданные) и недоступна на Native/JS. Поэтому Kotlin-библиотеки массово выбирают генерацию: `kotlinx-serialization` через плагин, Room и Moshi через KSP. Результат - меньше рантайм-магии и ошибки конфигурации на этапе сборки.
|
||||
>
|
||||
> **Версионирование KSP отражает его связь с компилятором.** Исторически версия выглядела как `2.0.21-1.0.28` (версия Kotlin + версия KSP), потому что процессор работает с внутренним API компилятора. KSP2 перешёл на самостоятельную нумерацию (`2.3.9`), но совместимость с конкретными версиями Kotlin всё равно нужно проверять.
|
||||
>
|
||||
> Сравнить: [Reflection API](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Версия KSP должна быть совместима с версией Kotlin.** Старый формат - `2.0.21-1.0.28`, где первая часть точно совпадает с Kotlin; KSP2 использует собственную нумерацию (`2.3.9`). Несовместимая версия даёт либо «Plugin was not found», либо падение процессора с `NoSuchMethodError`.
|
||||
>
|
||||
> **Процессор не может изменить существующий класс.** Нельзя дописать метод в `User` - только сгенерировать `UserBuilder` рядом. Если нужна модификация, это задача плагина компилятора, а не KSP.
|
||||
>
|
||||
> **Сгенерированный код не виден другому процессору в том же раунде.** Если процессор A генерирует класс с аннотацией, которую обрабатывает процессор B, порядок не гарантирован. Решение - возвращать символы из `process()` для следующего раунда.
|
||||
>
|
||||
> **Неправильные `Dependencies` ломают инкрементальность.** `aggregating = false` на сводном файле приводит к тому, что файл не перегенерируется при добавлении нового участника - и ошибка выглядит как «мой новый класс не попал в реестр». Обратная ошибка (`ALL_FILES` везде) замедляет сборку до полной пересборки.
|
||||
>
|
||||
> **Сгенерированный код отсутствует до первой сборки.** Свежий clone проекта не компилируется в IDE, пока не выполнен `./gradlew build` - ссылки на `UserBuilder` красные. Это нормально, но сбивает с толку.
|
||||
>
|
||||
> **IDE может не видеть сгенерированные исходники.** Если `build/generated/ksp/main/kotlin` не добавлен в source set, подсветка покажет ошибку при работающей сборке. Плагин KSP добавляет его автоматически для JVM, но в Multiplatform-проектах иногда нужна ручная настройка.
|
||||
>
|
||||
> **`type.resolve()` дорогой.** Разрешение типа - основная статья расходов процессора. Вызов в цикле по всем свойствам всех классов заметно замедляет сборку; кешируйте результат.
|
||||
>
|
||||
> **`validate()` нельзя пропускать.** Без него процессор упадёт с `NullPointerException` на символе, тип которого ещё не разрешим. Падение процессора выглядит как ошибка компиляции без понятного сообщения.
|
||||
>
|
||||
> **kapt и KSP вместе не дают выигрыша.** Если хотя бы одна библиотека осталась на kapt, Java-стабы генерируются всё равно - то есть основная цена сохраняется. Миграцию делают целиком.
|
||||
>
|
||||
> **`ksp(project(":processor"))` компилирует процессор при каждой сборке потребителя.** Это нормально для проекта, но в CI может удваивать время. Для стабильного процессора лучше опубликовать его как артефакт.
|
||||
>
|
||||
> **Файл `META-INF/services` обязателен и чувствителен к имени.** Путь должен быть точно `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`, содержимое - полное имя класса провайдера. Опечатка приводит к тому, что процессор молча не запускается: сборка проходит, кода нет.
|
||||
>
|
||||
> **`logger.error()` останавливает сборку, `logger.info()` не виден без `--info`.** Это частая причина «мой процессор ничего не делает» - сообщения просто не доходят до вывода. Для отладки используйте `logger.warn()`.
|
||||
|
||||
Далее: [Линтеры и формат](Линтеры%20и%20формат.md)
|
||||
@@ -1,273 +0,0 @@
|
||||
---
|
||||
title: kotlinc
|
||||
description: Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
---
|
||||
|
||||
`kotlinc` - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода.
|
||||
|
||||
```
|
||||
kotlinc Hello.kt -include-runtime -d hello.jar
|
||||
java -jar hello.jar
|
||||
```
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("скомпилировано kotlinc")
|
||||
}
|
||||
```
|
||||
|
||||
Проверить версию:
|
||||
```
|
||||
$ kotlinc -version
|
||||
info: kotlinc-jvm 2.4.21 (JRE 27)
|
||||
```
|
||||
Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода).
|
||||
|
||||
## Установка
|
||||
| Способ | Команда |
|
||||
|---|---|
|
||||
| Homebrew | `brew install kotlin` |
|
||||
| SDKMAN | `sdk install kotlin` |
|
||||
| Snap | `snap install --classic kotlin` |
|
||||
| Chocolatey | `choco install kotlinc` |
|
||||
| Вручную | архив с GitHub Releases + `PATH` |
|
||||
|
||||
Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам.
|
||||
|
||||
## Три режима компиляции
|
||||
**В каталог с `.class`** - для дальнейшей работы с байт-кодом:
|
||||
```
|
||||
kotlinc Demo.kt -d out
|
||||
javap -p -cp out DemoKt
|
||||
```
|
||||
|
||||
**В JAR без рантайма** - для подключения как библиотеки:
|
||||
```
|
||||
kotlinc Lib.kt -d lib.jar
|
||||
```
|
||||
JAR не запустится напрямую: в нём нет stdlib.
|
||||
|
||||
**В исполняемый JAR** - со встроенной stdlib (~5 МБ):
|
||||
```
|
||||
kotlinc App.kt -include-runtime -d app.jar
|
||||
java -jar app.jar
|
||||
```
|
||||
|
||||
## Ключевые флаги
|
||||
| Флаг | Что делает |
|
||||
|---|---|
|
||||
| `-d <путь>` | каталог или `.jar` для результата |
|
||||
| `-include-runtime` | вложить stdlib в JAR |
|
||||
| `-classpath` / `-cp` | зависимости |
|
||||
| `-jvm-target <v>` | версия байт-кода (по умолчанию **1.8**) |
|
||||
| `-module-name <n>` | имя модуля (влияет на манглирование `internal`) |
|
||||
| `-nowarn` | подавить предупреждения |
|
||||
| `-Werror` | считать предупреждения ошибками |
|
||||
| `-Wextra` | дополнительные проверки K2 |
|
||||
| `-progressive` | исправления применяются сразу, без цикла миграции |
|
||||
| `-language-version <v>` | ограничить возможности языка |
|
||||
| `-api-version <v>` | запретить API новее указанной версии stdlib |
|
||||
| `-opt-in <fq.name>` | включить API, требующее opt-in |
|
||||
| `-verbose` | подробный лог |
|
||||
| `-X` | список экспериментальных флагов |
|
||||
|
||||
**Про `-jvm-target`.** Значение по умолчанию - `1.8`, то есть байт-код major version 52, даже на JDK 27:
|
||||
```
|
||||
$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major
|
||||
major version: 52
|
||||
|
||||
$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major
|
||||
major version: 65
|
||||
```
|
||||
Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно.
|
||||
|
||||
**Про `-Werror`.** Останавливает сборку на любом предупреждении:
|
||||
```
|
||||
$ kotlinc -Werror Demo.kt -d out
|
||||
error: warnings found and -Werror specified
|
||||
Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело.
|
||||
```
|
||||
В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - `allWarningsAsErrors.set(true)`.
|
||||
|
||||
```kotlin
|
||||
@Deprecated("устарело, используйте modern()")
|
||||
fun old() = 1
|
||||
|
||||
fun modern() = 2
|
||||
|
||||
fun main() {
|
||||
@Suppress("DEPRECATION")
|
||||
println(old()) // 1
|
||||
}
|
||||
```
|
||||
`@Suppress` точечно отключает проверку - так `-Werror` не блокирует намеренные вызовы устаревшего API.
|
||||
|
||||
## Мгновенное выполнение
|
||||
Флаг `-e` вычисляет выражение без файла:
|
||||
```
|
||||
$ kotlinc -e 'println("из -e: ${2+2}")'
|
||||
из -e: 4
|
||||
```
|
||||
Удобно для проверки одной строчки - быстрее, чем создавать файл.
|
||||
|
||||
## Скрипты .main.kts
|
||||
Файл с расширением `.main.kts` - это Kotlin-скрипт: код на верхнем уровне, без `fun main`.
|
||||
```
|
||||
$ cat hello.main.kts
|
||||
println("из скрипта: ${listOf(1,2,3).sum()}")
|
||||
|
||||
$ kotlinc -script hello.main.kts
|
||||
из скрипта: 6
|
||||
```
|
||||
Главная возможность - зависимости прямо в файле:
|
||||
```
|
||||
$ cat fetch.main.kts
|
||||
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
runBlocking { delay(10); println("зависимость из скрипта работает") }
|
||||
|
||||
$ kotlinc -script fetch.main.kts
|
||||
зависимость из скрипта работает
|
||||
```
|
||||
`@file:DependsOn` скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект.
|
||||
|
||||
Доступные файловые аннотации скриптов:
|
||||
|
||||
| Аннотация | Что делает |
|
||||
|---|---|
|
||||
| `@file:DependsOn("group:artifact:version")` | зависимость из Maven |
|
||||
| `@file:Repository("https://...")` | дополнительный репозиторий |
|
||||
| `@file:CompilerOptions("-jvm-target", "21")` | флаги компилятора |
|
||||
| `@file:Import("other.main.kts")` | подключить другой скрипт |
|
||||
|
||||
Аргументы командной строки доступны как `args`:
|
||||
```kotlin
|
||||
// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt
|
||||
// val path = args.firstOrNull() ?: error("укажите файл")
|
||||
// println(java.io.File(path).readLines().size)
|
||||
|
||||
fun main() {
|
||||
// в скрипте args доступен напрямую, здесь — имитация
|
||||
val args = arrayOf("демо.txt")
|
||||
println("аргументов: ${args.size}") // аргументов: 1
|
||||
}
|
||||
```
|
||||
|
||||
Shebang делает скрипт исполняемым:
|
||||
```
|
||||
#!/usr/bin/env kotlin
|
||||
println("запущено как ./script.main.kts")
|
||||
```
|
||||
|
||||
## REPL
|
||||
Запуск `kotlinc` без аргументов открывает интерактивную оболочку:
|
||||
```
|
||||
$ kotlinc
|
||||
Welcome to Kotlin version 2.4.21
|
||||
>>> val x = 42
|
||||
>>> x * 2
|
||||
res1: kotlin.Int = 84
|
||||
>>> :quit
|
||||
```
|
||||
REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или `.main.kts`.
|
||||
|
||||
## Смешанная компиляция вручную
|
||||
Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен:
|
||||
```
|
||||
# 1. Kotlin видит Java-исходники
|
||||
kotlinc -cp . Lib.kt JLib.java -d out
|
||||
|
||||
# 2. javac видит скомпилированные Kotlin-классы
|
||||
javac -cp out -d out Main.java
|
||||
|
||||
# 3. запуск с stdlib в classpath
|
||||
java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main
|
||||
```
|
||||
Kotlin-компилятор принимает `.java`-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает:
|
||||
```
|
||||
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
|
||||
```
|
||||
Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Демон компилятора
|
||||
Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому `kotlinc` на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды).
|
||||
|
||||
Настройка демона в `gradle.properties`:
|
||||
```
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
kotlin.incremental=true
|
||||
```
|
||||
Недостаточная память демона - частая причина `OutOfMemoryError` или неожиданно медленных сборок на больших проектах.
|
||||
|
||||
## Полезные комбинации
|
||||
```
|
||||
# что реально попало в байт-код
|
||||
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
|
||||
|
||||
# аннотации nullability и метаданные
|
||||
kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata"
|
||||
|
||||
# проверить, что код чистый
|
||||
kotlinc -Werror -Wextra Demo.kt -d /dev/null
|
||||
|
||||
# совместимость со старым языком
|
||||
kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out
|
||||
|
||||
# список экспериментальных флагов
|
||||
kotlinc -X
|
||||
```
|
||||
Подробнее про чтение байт-кода: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac Demo.java` | `kotlinc Demo.kt` |
|
||||
> | `java Demo` | `java -jar app.jar` или `kotlin DemoKt` |
|
||||
> | `--release 21` | `-jvm-target 21` |
|
||||
> | `-Xlint:all -Werror` | `-Wextra -Werror` |
|
||||
> | JEP 330: `java Demo.java` | `kotlinc -script` или `.main.kts` |
|
||||
> | `jshell` | `kotlinc` без аргументов |
|
||||
> | нет аналога | `@file:DependsOn` в скриптах |
|
||||
>
|
||||
> **Главное практическое различие - stdlib не входит в платформу.** `javac` компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от `kotlin-stdlib.jar`, поэтому нужен либо `-include-runtime`, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native).
|
||||
>
|
||||
> **`-jvm-target 1.8` по умолчанию - решение о совместимости, а не недоработка.** Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21.
|
||||
>
|
||||
> **Скрипты в Kotlin сильнее, чем однофайловый запуск в Java.** JEP 330 позволяет `java Demo.java`, но только с тем, что есть в JDK: зависимостей нет. `@file:DependsOn` в `.main.kts` скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`-jvm-target` по умолчанию 1.8.** Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет.
|
||||
>
|
||||
> **JAR без `-include-runtime` не запускается:**
|
||||
> ```
|
||||
> $ kotlinc App.kt -d app.jar && java -jar app.jar
|
||||
> Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
|
||||
> ```
|
||||
> Либо добавьте флаг, либо укажите stdlib в `-cp`.
|
||||
>
|
||||
> **`-include-runtime` даёт JAR на ~5 МБ.** Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений.
|
||||
>
|
||||
> **`-e` и `-script` требуют разных форм записи.** `-e` принимает выражение в кавычках, `-script` - путь к `.kts`-файлу. Передать `.kts` в `-e` или выражение в `-script` не получится.
|
||||
>
|
||||
> **Файл скрипта должен называться `*.main.kts`, не `*.kts`.** Расширение `.kts` зарезервировано для Gradle-скриптов с другим шаблоном: `@file:DependsOn` в нём не сработает.
|
||||
>
|
||||
> **Первый запуск `@file:DependsOn` медленный.** Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш `~/.m2`, но в CI без сохранённого кеша задержка повторится на каждом прогоне.
|
||||
>
|
||||
> **`-Werror` падает на предупреждениях из зависимостей.** Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный `@Suppress("DEPRECATION")`, а не отключение флага.
|
||||
>
|
||||
> **REPL не видит зависимостей.** `kotlinc` без аргументов даёт только stdlib: подключить библиотеку нельзя (есть `-cp`, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или `.main.kts`.
|
||||
>
|
||||
> **Kotlin-компилятор не компилирует `.java`-файлы.** Передача `.java` в аргументах нужна только для разрешения ссылок; сами классы потом компилирует `javac`. Пропустить второй шаг - получить `ClassNotFoundException` на Java-классе.
|
||||
>
|
||||
> **`-module-name` влияет на видимость `internal`.** `internal` компилируется в публичное имя с суффиксом модуля (`helper$main`). Компиляция двух частей проекта с разными `-module-name` сделает их `internal`-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало.
|
||||
>
|
||||
> **`-d /dev/null` для проверки работает не везде.** На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять.
|
||||
|
||||
Далее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
@@ -1,205 +0,0 @@
|
||||
---
|
||||
title: Инструментарий
|
||||
description: Обзор инструментов Kotlin — компилятор, сборка, кодогенерация, статический анализ — и как они складываются в проект
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
---
|
||||
|
||||
Язык - это половина дела. Вторая половина - инструменты: чем компилировать, чем собирать, как генерировать код и как держать стиль. В Kotlin эти роли распределены между четырьмя слоями.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["исходники .kt"] --> B["компилятор K2<br/>kotlinc"]
|
||||
B --> C[".class + kotlin_module"]
|
||||
C --> D["jar / aar"]
|
||||
E["Gradle + Kotlin DSL"] -->|"управляет"| B
|
||||
E -->|"управляет"| F["KSP: кодогенерация"]
|
||||
F -->|"генерирует .kt"| A
|
||||
E -->|"управляет"| G["ktlint / detekt"]
|
||||
G -->|"проверяет"| A
|
||||
```
|
||||
|
||||
## Четыре слоя
|
||||
| Слой | Инструмент | Зачем |
|
||||
|---|---|---|
|
||||
| компиляция | `kotlinc` / компилятор K2 | исходники → байт-код |
|
||||
| сборка | Gradle + Kotlin DSL | зависимости, таски, многомодульность |
|
||||
| кодогенерация | KSP | аннотации → сгенерированный код |
|
||||
| качество | ktlint, detekt | формат и статический анализ |
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
// этот файл можно скомпилировать тремя способами:
|
||||
// 1. kotlinc Main.kt -include-runtime -d app.jar
|
||||
// 2. gradle build (с build.gradle.kts)
|
||||
// 3. kotlinc -script Main.main.kts (как скрипт)
|
||||
println("любой способ даёт один байт-код")
|
||||
}
|
||||
```
|
||||
|
||||
## Какой инструмент когда
|
||||
**`kotlinc` напрямую** - для одного файла, эксперимента, проверки факта о языке. Не масштабируется: нет управления зависимостями.
|
||||
|
||||
**Gradle** - стандарт для всего остального. Kotlin DSL (`build.gradle.kts`) - это Kotlin-код, поэтому работает автодополнение и проверка типов. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
**Maven** поддерживается плагином `kotlin-maven-plugin`, но экосистема Kotlin ориентирована на Gradle: Multiplatform, Compose и большинство плагинов существуют только для него.
|
||||
|
||||
**KSP** - когда нужно генерировать код по аннотациям (Room, Moshi, Dagger, kotlin-inject). Заменил `kapt`, который работал через Java-стабы и был в разы медленнее. Подробнее: [KSP](KSP.md)
|
||||
|
||||
## Минимальный проект
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("com.example.MainKt")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
Четыре блока покрывают 90% нужд: плагины, репозитории, зависимости, версия JVM. Остальное - настройка под конкретную задачу.
|
||||
|
||||
## Структура каталогов
|
||||
```
|
||||
project/
|
||||
├── build.gradle.kts # сборка модуля
|
||||
├── settings.gradle.kts # список модулей
|
||||
├── gradle.properties # флаги Gradle и JVM
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml # version catalog
|
||||
│ └── wrapper/ # gradle-wrapper.jar + properties
|
||||
├── gradlew # запуск без установленного Gradle
|
||||
└── src/
|
||||
├── main/kotlin/
|
||||
├── main/resources/
|
||||
├── test/kotlin/
|
||||
└── test/resources/
|
||||
```
|
||||
Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Что делает компилятор K2
|
||||
K2 - фронтенд компилятора, стабильный с Kotlin 2.0 и единственный в 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и даёт более умные приведения. Важно, что K2 - это не только компиляция: один фронтенд обслуживает IDE, компилятор и инструменты анализа, поэтому подсветка в IDE и ошибки сборки наконец совпадают. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Версии: три разных числа
|
||||
| Что | Где задаётся | Что значит |
|
||||
|---|---|---|
|
||||
| версия Kotlin | `kotlin("jvm") version "2.4.0"` | версия компилятора и stdlib |
|
||||
| `languageVersion` | `compilerOptions` | какие возможности языка разрешены |
|
||||
| `jvmTarget` / toolchain | `jvmToolchain(21)` | версия байт-кода и JDK |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins { kotlin("jvm") version "2.4.0" }
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21) // JDK для компиляции и запуска
|
||||
|
||||
compilerOptions {
|
||||
// можно ограничить язык более старой версией
|
||||
// languageVersion.set(KotlinVersion.KOTLIN_2_1)
|
||||
allWarningsAsErrors.set(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
Частая ошибка - считать их одним. Можно компилировать Kotlin 2.4 компилятором, ограничив язык версией 2.1 (для совместимости с библиотекой), и выдавать байт-код для JVM 8.
|
||||
|
||||
## Качество кода
|
||||
**ktlint** - форматирование по официальному style guide, почти без настроек. **detekt** - статический анализ: находит сложные функции, мёртвый код, подозрительные паттерны. Они дополняют друг друга: первый про форму, второй про смысл. Подробнее: [Линтеры и формат](Линтеры%20и%20формат.md)
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [kotlinc](kotlinc.md) | CLI, скрипты, REPL, ключевые флаги |
|
||||
| [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) | конфигурации, version catalog, таски |
|
||||
| [Структура проекта](Структура%20проекта.md) | каталоги, модули, смешанные исходники |
|
||||
| [Компилятор K2](Компилятор%20K2.md) | фазы, что изменилось, флаги |
|
||||
| [KSP](KSP.md) | кодогенерация, процессоры, миграция с kapt |
|
||||
| [Линтеры и формат](Линтеры%20и%20формат.md) | ktlint, detekt, CI |
|
||||
|
||||
## Что ещё есть в экосистеме
|
||||
| Инструмент | Задача |
|
||||
|---|---|
|
||||
| Dokka | документация из KDoc (аналог Javadoc) |
|
||||
| binary-compatibility-validator | контроль публичного ABI библиотеки |
|
||||
| kotlinx-benchmark | микробенчмарки на JMH |
|
||||
| Kover | покрытие тестами |
|
||||
| kotlin-multiplatform | общий код для JVM/JS/Native |
|
||||
| Compose Multiplatform | UI поверх Multiplatform |
|
||||
| kotlinx-serialization | сериализация через плагин компилятора |
|
||||
| Amper | экспериментальная альтернатива Gradle от JetBrains |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts с типичным набором для библиотеки
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
kotlin("plugin.serialization") version "2.4.0"
|
||||
id("org.jetbrains.dokka") version "2.0.0"
|
||||
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.8"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
```
|
||||
Обратите внимание: `kotlinx-serialization` - это плагин компилятора, а не библиотека рефлексии. Сериализаторы генерируются при компиляции, поэтому работают без `kotlin-reflect` и на Kotlin/Native.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac` | `kotlinc` (K2) |
|
||||
> | Maven или Gradle (Groovy) | Gradle + Kotlin DSL |
|
||||
> | annotation processing (APT) | KSP |
|
||||
> | Checkstyle, SpotBugs, PMD | ktlint, detekt |
|
||||
> | Javadoc | Dokka (KDoc) |
|
||||
> | Lombok | возможности языка (`data class`, свойства) |
|
||||
> | один таргет - JVM | JVM, JS, Native, Wasm |
|
||||
>
|
||||
> **Gradle в Kotlin-проектах не альтернатива, а стандарт.** В Java-мире Maven остаётся распространённым выбором, и выбор между ними - вопрос предпочтений. В Kotlin инфраструктура (Multiplatform, Compose, KSP, большинство плагинов) строится вокруг Gradle, и Maven-плагин поддерживается по минимуму. Это сужает выбор, но концентрирует усилия.
|
||||
>
|
||||
> **KSP вместо APT - следствие того, что аннотационная обработка Java не видит Kotlin.** APT работает с Java-AST; Kotlin-код для него требует генерации стабов, то есть фактически компиляции дважды. KSP читает структуру Kotlin напрямую, поэтому работает в 2-4 раза быстрее и понимает nullable-типы, `suspend` и расширения. Цена - процессоры нужно писать заново: API другой.
|
||||
>
|
||||
> **Lombok не нужен.** Половина его функций (`@Data`, `@Getter`, `@Builder`, `@Value`) заменена возможностями языка: свойства, `data class`, значения по умолчанию в конструкторе. Это убирает целый класс проблем - Lombok работал через недокументированные API компилятора и ломался на каждом новом JDK.
|
||||
>
|
||||
> **Одна система сборки обслуживает несколько платформ.** Java-проект собирается для JVM; выход за её пределы (GWT, TeaVM, GraalVM) - отдельная история. В Kotlin JS, Native и Wasm - полноправные таргеты в том же `build.gradle.kts`, с общим исходным кодом. Цена - сложность конфигурации Multiplatform-проекта заметно выше.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Версия плагина Kotlin и версия stdlib должны совпадать.** Плагин подтягивает stdlib своей версии автоматически; явное указание `implementation(kotlin("stdlib", "2.3.0"))` при плагине 2.4.0 даёт конфликт и предупреждение. Не указывайте stdlib вручную вообще.
|
||||
>
|
||||
> **`jvmTarget` без toolchain ломается на другом JDK.** Если локально JDK 21, а в CI - 17, сборка с `jvmTarget = "21"` упадёт. `jvmToolchain(21)` решает: Gradle сам скачает нужный JDK.
|
||||
>
|
||||
> **`jvmTarget` Kotlin и `targetCompatibility` Java должны совпадать.** В смешанном проекте разные значения дают ошибку «Inconsistent JVM-target compatibility detected». `jvmToolchain` настраивает оба.
|
||||
>
|
||||
> **Значение по умолчанию `jvmTarget` - 1.8.** Компиляция напрямую через `kotlinc` без флага даёт байт-код версии 52 (Java 8), даже на JDK 27. Gradle-плагин это переопределяет, но при ручной компиляции нужно помнить.
|
||||
>
|
||||
> **kapt и KSP в одном проекте замедляют сборку.** Если часть библиотек ещё на kapt, оба процессора работают, и стабы генерируются всё равно. Миграцию стоит делать целиком.
|
||||
>
|
||||
> **Gradle без `gradlew` - источник расхождений.** Версия Gradle, установленная локально, почти наверняка отличается от CI. Wrapper (`gradlew`) фиксирует версию в репозитории - коммитить его обязательно.
|
||||
>
|
||||
> **`kotlin.incremental` может давать ложные сборки.** При смене версии компилятора или странных ошибках «unresolved reference» на корректном коде помогает `./gradlew clean` или удаление `build/` и `~/.gradle/caches/build-cache-1`.
|
||||
>
|
||||
> **Плагины компилятора фиксируют версию Kotlin.** `kotlin("plugin.serialization") version "2.4.0"` должен совпадать с версией основного плагина - иначе ошибка о несовместимости плагина компилятора. Используйте version catalog, чтобы версия была в одном месте.
|
||||
|
||||
Далее: [kotlinc](kotlinc.md)
|
||||
@@ -1,386 +0,0 @@
|
||||
---
|
||||
title: Компилятор K2
|
||||
description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- compiler
|
||||
---
|
||||
|
||||
K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки.
|
||||
|
||||
```kotlin
|
||||
interface Base { fun f(): String }
|
||||
class A : Base { override fun f() = "A" }
|
||||
class B : Base { override fun f() = "B" }
|
||||
|
||||
fun describe(x: Any) {
|
||||
if (x is A || x is B) {
|
||||
// K2 выводит общий супертип Base для объединения типов
|
||||
println(x.f())
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
describe(A()) // A
|
||||
describe(B()) // B
|
||||
}
|
||||
```
|
||||
В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения.
|
||||
|
||||
## Фазы компиляции
|
||||
Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`:
|
||||
```
|
||||
$ kotlinc -verbose -Xreport-perf Demo.kt -d out
|
||||
PERF: INIT 271 ms
|
||||
PERF: ANALYZE 530 ms 11.311 loc/s
|
||||
PERF: TRANSLATION to IR 144 ms 41.453 loc/s
|
||||
PERF: KLIB WRITING 0 ms
|
||||
PERF: IR LOWERING 122 ms 48.799 loc/s
|
||||
PERF: BACKEND 75 ms 79.456 loc/s
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[".kt"] --> B["INIT<br/>загрузка classpath"]
|
||||
B --> C["ANALYZE<br/>фронтенд K2: FIR"]
|
||||
C --> D["TRANSLATION<br/>FIR → IR"]
|
||||
D --> E["IR LOWERING<br/>развёртка inline,<br/>suspend, лямбды"]
|
||||
E --> F["BACKEND<br/>IR → байт-код"]
|
||||
F --> G[".class + Metadata"]
|
||||
```
|
||||
|
||||
| Фаза | Что происходит |
|
||||
|---|---|
|
||||
| INIT | чтение classpath, инициализация компилятора |
|
||||
| ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR |
|
||||
| TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов |
|
||||
| IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов |
|
||||
| BACKEND | IR → байт-код JVM (или JS, Native, Wasm) |
|
||||
|
||||
Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель.
|
||||
|
||||
## Что изменилось в умных приведениях
|
||||
K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение.
|
||||
|
||||
**Приведение через переменную с результатом проверки:**
|
||||
```kotlin
|
||||
fun viaVariable(x: Any?) {
|
||||
val isString = x is String
|
||||
if (isString) {
|
||||
// K2 приводит тип по val с результатом проверки
|
||||
println(x.length)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
viaVariable("шесть!") // 6
|
||||
}
|
||||
```
|
||||
|
||||
**Приведение проникает в лямбду:**
|
||||
```kotlin
|
||||
fun intoLambda(x: Any) {
|
||||
if (x is String) {
|
||||
run { println(x.length) } // приведение видно внутри лямбды
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
intoLambda("семь!!!") // 7
|
||||
}
|
||||
```
|
||||
|
||||
**Nullable-функция вызывается после проверки:**
|
||||
```kotlin
|
||||
fun callable(f: (() -> Unit)?) {
|
||||
if (f != null) {
|
||||
f() // без ?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
callable { println("лямбда вызвана") } // лямбда вызвана
|
||||
}
|
||||
```
|
||||
|
||||
**Исчерпывающий `when` по данным потока:**
|
||||
```kotlin
|
||||
sealed interface State
|
||||
data object Loading : State
|
||||
data class Done(val value: String) : State
|
||||
|
||||
fun render(s: State) = when (s) {
|
||||
Loading -> "загрузка"
|
||||
is Done -> "готово: ${s.value}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(render(Loading)) // загрузка
|
||||
println(render(Done("данные"))) // готово: данные
|
||||
}
|
||||
```
|
||||
`else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
|
||||
|
||||
Чего K2 **не** делает - приведения для изменяемого свойства класса:
|
||||
```
|
||||
$ kotlinc Demo.kt -d out
|
||||
error: smart cast to 'String' is impossible, because 'value' is a mutable property
|
||||
that could be mutated concurrently.
|
||||
```
|
||||
Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия:
|
||||
```kotlin
|
||||
class Holder { var value: String? = "есть" }
|
||||
|
||||
fun main() {
|
||||
val h = Holder()
|
||||
val snapshot = h.value
|
||||
if (snapshot != null) {
|
||||
println(snapshot.length) // 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Полезные флаги анализа
|
||||
### -Wextra
|
||||
Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции.
|
||||
```
|
||||
kotlinc -Wextra Demo.kt -d out
|
||||
```
|
||||
|
||||
### -Xreturn-value-checker
|
||||
Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять:
|
||||
```
|
||||
$ kotlinc -Xreturn-value-checker=full Demo.kt -d out
|
||||
warning: unused return value of 'compute'.
|
||||
compute()
|
||||
^^^^^^^
|
||||
```
|
||||
Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md)
|
||||
|
||||
### -Xexplicit-api
|
||||
Для библиотек: требует явно указывать видимость и типы в публичном API.
|
||||
```
|
||||
$ kotlinc -Xexplicit-api=strict Lib.kt -d out
|
||||
error: visibility must be specified in explicit API mode.
|
||||
class Public {
|
||||
^^^^^^^^^^^^
|
||||
error: visibility must be specified in explicit API mode.
|
||||
fun noExplicitType() = 42
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
```
|
||||
```kotlin
|
||||
// так проходит explicit API mode
|
||||
public class Service {
|
||||
public fun compute(): Int = 42
|
||||
private fun internalHelper(): Int = 1
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service().compute()) // 42
|
||||
}
|
||||
```
|
||||
В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации.
|
||||
|
||||
### Производительность
|
||||
| Флаг | Что делает |
|
||||
|---|---|
|
||||
| `-Xbackend-threads=N` | кодогенерация в N потоков |
|
||||
| `-Xreport-perf` | статистика по фазам (с `-verbose`) |
|
||||
| `-Xlambdas={class\|indy}` | схема компиляции лямбд |
|
||||
| `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк |
|
||||
|
||||
`-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд.
|
||||
|
||||
## Экспериментальные возможности языка
|
||||
K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`.
|
||||
|
||||
| Флаг | Возможность |
|
||||
|---|---|
|
||||
| `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) |
|
||||
| `-Xwhen-guards` | `when` с охранными условиями |
|
||||
| `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды |
|
||||
| `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` |
|
||||
| `-Xname-based-destructuring` | деструктуризация по именам |
|
||||
| `-Xnested-type-aliases` | `typealias` внутри класса |
|
||||
| `-Xexplicit-backing-fields` | отдельный тип backing field и свойства |
|
||||
| `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу |
|
||||
| `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока |
|
||||
| `-Xannotation-target-all` | `@all:` для аннотаций на свойствах |
|
||||
|
||||
```kotlin
|
||||
// context parameters — в 2.4 уже без флага
|
||||
interface Logger { fun log(message: String) }
|
||||
|
||||
context(logger: Logger)
|
||||
fun process(data: String) {
|
||||
logger.log("обработка: $data")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val logger = object : Logger {
|
||||
override fun log(message: String) = println(message)
|
||||
}
|
||||
with(logger) {
|
||||
process("запись") // обработка: запись
|
||||
}
|
||||
}
|
||||
```
|
||||
Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
|
||||
|
||||
## Три версии, которые путают
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21) // версия байт-кода
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_4) // возможности языка
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_2) // доступный API stdlib
|
||||
}
|
||||
}
|
||||
```
|
||||
| Опция | Отвечает за | Типичная ошибка |
|
||||
|---|---|---|
|
||||
| `jvmTarget` | формат `.class` | оставить дефолтный 1.8 |
|
||||
| `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении |
|
||||
| `apiVersion` | какие функции stdlib видны | указать устаревшую версию |
|
||||
|
||||
`apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом).
|
||||
|
||||
Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
## Progressive mode
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
progressiveMode.set(true)
|
||||
}
|
||||
}
|
||||
```
|
||||
Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее.
|
||||
|
||||
## Плагины компилятора
|
||||
K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код:
|
||||
|
||||
| Плагин | Что делает |
|
||||
|---|---|
|
||||
| `kotlinx-serialization` | генерирует сериализаторы при компиляции |
|
||||
| `all-open` | делает классы `open` (для Spring, Mockito) |
|
||||
| `no-arg` | добавляет конструктор без аргументов (JPA) |
|
||||
| `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций |
|
||||
| `kotlin-jpa` | предустановленный `no-arg` для JPA |
|
||||
| `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде |
|
||||
| `power-assert` | подробные сообщения в `assert` |
|
||||
| `compose` | оптимизация composable-функций |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
kotlin("plugin.serialization") version "2.4.0"
|
||||
kotlin("plugin.spring") version "2.4.0"
|
||||
kotlin("plugin.jpa") version "2.4.0"
|
||||
}
|
||||
```
|
||||
Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md)
|
||||
|
||||
## Инкрементальная компиляция
|
||||
```
|
||||
# gradle.properties
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.useClasspathSnapshot=true
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
```
|
||||
Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI.
|
||||
|
||||
Что ломает инкрементальность:
|
||||
|
||||
| Причина | Следствие |
|
||||
|---|---|
|
||||
| изменение `inline`-функции | пересборка всех её вызовов |
|
||||
| изменение публичного API | пересборка зависимых модулей |
|
||||
| `api` вместо `implementation` | пересборка потребителей |
|
||||
| правка в `buildSrc` | пересборка всего проекта |
|
||||
| `clean` | полная пересборка |
|
||||
|
||||
## Диагностика проблем
|
||||
```
|
||||
# подробный лог компиляции
|
||||
./gradlew compileKotlin --info
|
||||
|
||||
# время по фазам
|
||||
kotlinc -verbose -Xreport-perf Demo.kt -d out
|
||||
|
||||
# что попало в байт-код
|
||||
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
|
||||
|
||||
# проверить без записи результата
|
||||
./gradlew compileKotlin --rerun-tasks
|
||||
|
||||
# сбросить кеши при странных ошибках
|
||||
./gradlew clean && rm -rf .gradle .kotlin
|
||||
```
|
||||
Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) |
|
||||
> | компилятор только для JVM | JVM, JS, Native, Wasm |
|
||||
> | нет анализа потока для приведений | умные приведения |
|
||||
> | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение |
|
||||
> | аннотационные процессоры (APT) | плагины компилятора + KSP |
|
||||
> | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` |
|
||||
> | IDE имеет свой анализатор | K2 общий для IDE и сборки |
|
||||
>
|
||||
> **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код.
|
||||
>
|
||||
> **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип.
|
||||
>
|
||||
> **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin.
|
||||
>
|
||||
> **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера.
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Умное приведение не работает для `var`-свойства класса:**
|
||||
> ```kotlin
|
||||
> class Holder { var value: String? = "есть" }
|
||||
>
|
||||
> fun main() {
|
||||
> val h = Holder()
|
||||
> // if (h.value != null) println(h.value.length) // ошибка компиляции
|
||||
> val snapshot = h.value
|
||||
> println(if (snapshot != null) snapshot.length else 0) // 4
|
||||
> }
|
||||
> ```
|
||||
> Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия.
|
||||
>
|
||||
> **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда.
|
||||
>
|
||||
> **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности.
|
||||
>
|
||||
> **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск.
|
||||
>
|
||||
> **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий.
|
||||
>
|
||||
> **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI.
|
||||
>
|
||||
> **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше.
|
||||
>
|
||||
> **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением.
|
||||
>
|
||||
> **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches».
|
||||
>
|
||||
> **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает.
|
||||
>
|
||||
> **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами.
|
||||
|
||||
Далее: [KSP](KSP.md)
|
||||
@@ -1,385 +0,0 @@
|
||||
---
|
||||
title: Линтеры и формат
|
||||
description: ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- quality
|
||||
---
|
||||
|
||||
Два инструмента с разными задачами: **ktlint** приводит код к официальному стилю и умеет исправлять сам, **detekt** ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции.
|
||||
|
||||
| | ktlint | detekt |
|
||||
|---|---|---|
|
||||
| проверяет | форматирование и имена | сложность, запахи кода |
|
||||
| автоисправление | да, большинство правил | частично |
|
||||
| настройка | `.editorconfig`, минимум правил | `detekt.yml`, ~200 правил |
|
||||
| нужен ли compile classpath | нет | нет (есть опциональный type resolution) |
|
||||
| скорость | быстро | медленнее |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "12.1.2"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.8"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
detekt {
|
||||
buildUponDefaultConfig = true
|
||||
allRules = false
|
||||
}
|
||||
```
|
||||
|
||||
## ktlint
|
||||
Реализует официальный Kotlin coding conventions. Настроек почти нет - это и есть идея: не спорить о стиле.
|
||||
|
||||
На намеренно плохом файле:
|
||||
```kotlin
|
||||
// неформатированный код
|
||||
// class bad_name {
|
||||
// fun TooComplex( a:Int,b:Int ) : Int {
|
||||
// var r = 0
|
||||
// if (a>0) { if (b>0) { if (a>b) { r=1 } else { r=2 } } else { r=3 } } else { r=4 }
|
||||
// return r
|
||||
// }
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("пример неформатированного кода выше")
|
||||
}
|
||||
```
|
||||
```
|
||||
$ ./gradlew ktlintMainSourceSetCheck
|
||||
Bad.kt:3:7 Class or object name should start with an uppercase letter and use camel case (cannot be auto-corrected)
|
||||
Bad.kt:4:9 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
|
||||
Bad.kt:4:20 Unexpected spacing after "("
|
||||
Bad.kt:4:23 Missing spacing after ":"
|
||||
Bad.kt:4:27 Missing spacing after ","
|
||||
Bad.kt:6:14 Missing spacing around ">"
|
||||
Bad.kt:6:18 A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block. (cannot be auto-corrected)
|
||||
Bad.kt:6:20 Missing newline after '{'
|
||||
```
|
||||
`./gradlew ktlintFormat` исправляет всё, что помечено как исправимое:
|
||||
```kotlin
|
||||
// после ktlintFormat
|
||||
class BadName {
|
||||
fun tooComplex(
|
||||
a: Int,
|
||||
b: Int,
|
||||
): Int {
|
||||
var r = 0
|
||||
if (a > 0) {
|
||||
if (b > 0) {
|
||||
r = if (a > b) 1 else 2
|
||||
} else {
|
||||
r = 3
|
||||
}
|
||||
} else {
|
||||
r = 4
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(BadName().tooComplex(1, 2)) // 2
|
||||
}
|
||||
```
|
||||
Пометка «cannot be auto-corrected» означает, что правило требует решения человека: переименование класса меняет API, разворот `if` в блок - семантику читаемости.
|
||||
|
||||
## Настройка ktlint
|
||||
Через `.editorconfig` в корне проекта:
|
||||
```
|
||||
root = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
max_line_length = 120
|
||||
insert_final_newline = true
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
ktlint_standard_function-naming = disabled
|
||||
ktlint_code_style = ktlint_official
|
||||
```
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| `ktlint_code_style` | `ktlint_official`, `intellij_idea`, `android_studio` |
|
||||
| `max_line_length` | длина строки (`off` отключает) |
|
||||
| `ktlint_standard_<rule> = disabled` | отключить конкретное правило |
|
||||
| `ij_kotlin_allow_trailing_comma` | разрешить висячую запятую |
|
||||
|
||||
Важно, что `.editorconfig` читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за `.editorconfig` вместо настроек плагина.
|
||||
|
||||
Отключение правила в коде:
|
||||
```kotlin
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
val longLine = "очень длинная строка, которую нельзя разбить по смыслу, например регулярное выражение"
|
||||
|
||||
fun main() {
|
||||
println(longLine.length) // 85
|
||||
}
|
||||
```
|
||||
|
||||
## detekt
|
||||
Около 200 правил, разбитых на наборы. Запуск на том же плохом файле:
|
||||
```
|
||||
$ ./gradlew detekt
|
||||
Bad.kt:3:7: The file name 'Bad' does not match the name of the single top-level declaration 'bad_name'. [MatchingDeclarationName]
|
||||
Bad.kt:1:1: The package declaration does not match the actual file location. [InvalidPackageDeclaration]
|
||||
Bad.kt:3:7: Class and Object names should match the pattern: [A-Z][a-zA-Z0-9]* [ClassNaming]
|
||||
Bad.kt:4:9: Function names should match the pattern: [a-z][a-zA-Z0-9]* [FunctionNaming]
|
||||
Bad.kt:6:72: This expression contains a magic number. [MagicNumber]
|
||||
Bad.kt:9:24: Private property `x` is unused. [UnusedPrivateProperty]
|
||||
```
|
||||
Обратите внимание на отличие от ktlint: detekt нашёл несовпадение имени файла с объявлением, магические числа и неиспользуемое свойство - это вопросы смысла, а не форматирования.
|
||||
|
||||
| Набор правил | Что проверяет |
|
||||
|---|---|
|
||||
| `complexity` | длинные функции, высокая цикломатическая сложность, много параметров |
|
||||
| `coroutines` | неправильное использование `suspend`, `GlobalScope`, блокирующие вызовы |
|
||||
| `empty-blocks` | пустые `catch`, `if`, функции |
|
||||
| `exceptions` | `catch (e: Exception)`, проглоченные исключения, `throw` в `finally` |
|
||||
| `naming` | соответствие конвенциям имён |
|
||||
| `performance` | `forEach` вместо `for`, лишние аллокации, `spread`-оператор |
|
||||
| `potential-bugs` | `!!`, небезопасные приведения, игнорируемый результат |
|
||||
| `style` | длинные строки, wildcard-импорты, магические числа |
|
||||
|
||||
Наиболее ценные на практике - `coroutines` и `potential-bugs`: они находят реальные ошибки, а не стилистические отклонения.
|
||||
|
||||
## Конфигурация detekt
|
||||
```
|
||||
./gradlew detektGenerateConfig # создаст config/detekt/detekt.yml
|
||||
```
|
||||
```yaml
|
||||
# config/detekt/detekt.yml
|
||||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: false
|
||||
|
||||
complexity:
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 60
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 6
|
||||
ignoreDefaultParameters: true
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
active: true
|
||||
ignoreNumbers: ['-1', '0', '1', '2']
|
||||
ignoreHashCodeFunction: true
|
||||
excludes: ['**/test/**']
|
||||
MaxLineLength:
|
||||
maxLineLength: 120
|
||||
WildcardImport:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
|
||||
coroutines:
|
||||
GlobalCoroutineUsage:
|
||||
active: true
|
||||
InjectDispatcher:
|
||||
active: true
|
||||
```
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
detekt {
|
||||
buildUponDefaultConfig = true // своя конфигурация дополняет дефолтную
|
||||
config.setFrom("$projectDir/config/detekt/detekt.yml")
|
||||
baseline = file("$projectDir/config/detekt/baseline.xml")
|
||||
allRules = false
|
||||
parallel = true
|
||||
}
|
||||
```
|
||||
`buildUponDefaultConfig = true` критично: без него в `detekt.yml` нужно перечислить **все** правила, и забытое правило окажется отключённым.
|
||||
|
||||
## Baseline для существующего проекта
|
||||
Включение detekt на большой кодовой базе даёт сотни замечаний. Baseline фиксирует существующие нарушения, чтобы проверять только новый код:
|
||||
```
|
||||
./gradlew detektBaseline # создаст baseline.xml
|
||||
```
|
||||
```xml
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MagicNumber:Bad.kt$6:72</ID>
|
||||
<ID>UnusedPrivateProperty:Bad.kt$private val x</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
```
|
||||
Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда.
|
||||
|
||||
## Подавление правил
|
||||
```kotlin
|
||||
@Suppress("MagicNumber")
|
||||
fun httpTimeout() = 30_000
|
||||
|
||||
class Legacy {
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
fun risky() {
|
||||
try {
|
||||
error("сбой")
|
||||
} catch (e: Exception) {
|
||||
// осознанно проглочено: вызывающий код не должен знать о сбое
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(httpTimeout()) // 30000
|
||||
Legacy().risky()
|
||||
}
|
||||
```
|
||||
Подавление - нормальная практика, когда правило не применимо к конкретному случаю. Плохая практика - подавлять правило целиком в `detekt.yml`, если оно нарушается в двух местах из тысячи.
|
||||
|
||||
## Type resolution
|
||||
detekt может работать с информацией о типах - тогда доступны правила, требующие знания classpath:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
jvmTarget = "21"
|
||||
}
|
||||
|
||||
tasks.register<io.gitlab.arturbosch.detekt.Detekt>("detektWithTypes") {
|
||||
description = "detekt с разрешением типов"
|
||||
setSource(files("src/main/kotlin"))
|
||||
classpath.setFrom(
|
||||
configurations.getByName("detekt"),
|
||||
sourceSets["main"].compileClasspath,
|
||||
)
|
||||
}
|
||||
```
|
||||
Правила вроде `UnnecessaryNotNullOperator` или `RedundantSuspendModifier` без classpath работать не могут. Цена - заметно медленнее.
|
||||
|
||||
## Интеграция в CI
|
||||
```yaml
|
||||
# .github/workflows/check.yml
|
||||
name: check
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
- run: ./gradlew ktlintCheck detekt --continue
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: reports
|
||||
path: |
|
||||
build/reports/detekt/
|
||||
build/reports/ktlint/
|
||||
```
|
||||
`--continue` важен: без него сборка остановится на первой проваленной проверке, и вы не увидите остальные замечания.
|
||||
|
||||
## Git-хук
|
||||
```bash
|
||||
# .git/hooks/pre-commit
|
||||
#!/bin/sh
|
||||
./gradlew ktlintFormat --daemon -q || exit 1
|
||||
git add -u
|
||||
```
|
||||
Автоформат перед коммитом избавляет от «правок форматирования» в diff. `detekt` в хук обычно не ставят - он слишком медленный для каждого коммита.
|
||||
|
||||
Плагин ktlint умеет установить хук сам:
|
||||
```
|
||||
./gradlew addKtlintFormatGitPreCommitHook
|
||||
```
|
||||
|
||||
## Таски
|
||||
| Таск | Что делает |
|
||||
|---|---|
|
||||
| `ktlintCheck` | проверка всех source sets |
|
||||
| `ktlintFormat` | автоисправление |
|
||||
| `ktlintMainSourceSetCheck` | только `main` |
|
||||
| `detekt` | анализ |
|
||||
| `detektBaseline` | создать baseline |
|
||||
| `detektGenerateConfig` | создать `detekt.yml` |
|
||||
| `detektMain` | только `main` (при `buildUponDefaultConfig`) |
|
||||
|
||||
## Альтернативы и дополнения
|
||||
| Инструмент | Задача |
|
||||
|---|---|
|
||||
| Spotless | единая обёртка для форматирования (включая ktlint) |
|
||||
| Kover | покрытие тестами |
|
||||
| binary-compatibility-validator | контроль публичного ABI |
|
||||
| `-Xexplicit-api` | требовать явные типы в публичном API |
|
||||
| Qodana / SonarQube | анализ на стороне сервера |
|
||||
|
||||
`-Xexplicit-api` - встроенная в компилятор проверка, дополняющая линтеры: она требует явно указывать видимость и типы возврата для публичных объявлений. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Разумный минимум
|
||||
1. `.editorconfig` с `ktlint_official` - согласованный стиль в IDE и CI.
|
||||
2. `ktlintFormat` в pre-commit хуке - форматирование не попадает в diff.
|
||||
3. detekt с `buildUponDefaultConfig` и baseline - новый код проверяется, старый не блокирует.
|
||||
4. `ktlintCheck detekt --continue` в CI с публикацией отчётов.
|
||||
5. `-Xexplicit-api=strict` для библиотек.
|
||||
|
||||
Этого достаточно; дальнейшая настройка правил - вопрос договорённостей команды, а не инструментов.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | Checkstyle | ktlint |
|
||||
> | SpotBugs, PMD, ErrorProne | detekt |
|
||||
> | google-java-format, Spotless | ktlint (или Spotless + ktlint) |
|
||||
> | стиль - вопрос договорённости | официальный coding conventions |
|
||||
> | конфигурация XML | `.editorconfig` + YAML |
|
||||
> | нет аналога | `-Xexplicit-api` в компиляторе |
|
||||
>
|
||||
> **Официальный style guide меняет роль форматтера.** В Java конкурируют несколько стилей (Oracle, Google, AOSP), и Checkstyle приходится настраивать под выбранный. Kotlin имеет один официальный стиль от JetBrains, реализованный и в IDE, и в ktlint - настройка почти не нужна, и споров о форматировании не возникает. Это сознательная позиция: единый стиль важнее возможности выбрать свой.
|
||||
>
|
||||
> **`.editorconfig` как единый источник правды - важнее, чем кажется.** В Java настройки Checkstyle и настройки форматирования IDE - разные файлы, которые расходятся. Файл `.editorconfig` читают и IntelliJ, и ktlint, поэтому «форматирование в IDE» и «проверка в CI» - буквально одна конфигурация. Это устраняет классический конфликт, когда IDE переформатирует код так, что падает сборка.
|
||||
>
|
||||
> **detekt берёт на себя то, что в Java распределено между тремя инструментами.** SpotBugs анализирует байт-код, PMD - исходники, ErrorProne встроен в компиляцию. detekt - один инструмент на исходниках, с опциональным разрешением типов. Проще в настройке, но находит меньше: без анализа байт-кода часть проблем (например, утечки ресурсов по потоку данных) недоступна.
|
||||
>
|
||||
> **Правила для корутин - специфика, которой нет аналога.** Набор `coroutines` в detekt находит `GlobalScope`, блокирующие вызовы внутри `suspend`, захардкоженные диспетчеры. В Java параллельные ошибки (блокировка в пуле, утечка `ExecutorService`) ловятся хуже, потому что нет структуры, которую можно статически проверить. Структурированная конкурентность Kotlin даёт линтеру опору. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md)
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`buildUponDefaultConfig = false` (по умолчанию) отключает все неперечисленные правила.** Свой `detekt.yml` без этого флага заменяет дефолтную конфигурацию целиком: сборка зелёная, проверок нет. Это самая частая ошибка настройки detekt.
|
||||
>
|
||||
> **detekt падает на слишком новом JDK.** Версия 1.23.8 на JDK 26 завершается с `GradleException: 26.0.1` - встроенный парсер не знает такой версии байт-кода. Запускайте таск на поддерживаемом JDK: `jvmToolchain` для компиляции и отдельный JDK для detekt, или обновите detekt.
|
||||
>
|
||||
> **ktlint и detekt могут конфликтовать по правилам.** Оба проверяют длину строки и имена, и при разных настройках код, исправленный одним, нарушает правило другого. Задавайте `max_line_length` в `.editorconfig`, а в detekt - `MaxLineLength.maxLineLength` с тем же значением.
|
||||
>
|
||||
> **`ktlintFormat` может изменить семантику в редких случаях.** Перестановка импортов и добавление висячих запятых безопасны, но правила вроде `if-else` → выражение меняют структуру кода. После массового автоформата запускайте тесты.
|
||||
>
|
||||
> **Baseline легко превращается в свалку.** Файл создаётся раз и начинает жить вечно: правила «починим позже» не выполняются. Перегенерируйте baseline периодически и следите, чтобы он сокращался.
|
||||
>
|
||||
> **Правила с type resolution требуют classpath и замедляют анализ.** Без `classpath.setFrom(...)` правила вроде `UnnecessaryNotNullOperator` молча не работают. С ним detekt становится в разы медленнее - обычно такой таск запускают только в CI.
|
||||
>
|
||||
> **`MagicNumber` шумит в тестах.** Тесты полны литералов, и правило даёт десятки замечаний. Добавьте `excludes: ['**/test/**']` в конфигурацию, иначе придётся подавлять в каждом файле.
|
||||
>
|
||||
> **Висячие запятые требуют согласованности.** `ij_kotlin_allow_trailing_comma = true` в `.editorconfig` и настройка IDE должны совпадать, иначе IDE удаляет запятую, а ktlint требует её вернуть - бесконечный цикл правок.
|
||||
>
|
||||
> **Версия плагина ktlint и версия самого ktlint - разные числа.** `org.jlleitschuh.gradle.ktlint` версии 12.x по умолчанию использует ktlint 1.x; при необходимости версия задаётся через `ktlint { version.set("1.5.0") }`. Несогласованность приводит к разным результатам локально и в CI.
|
||||
>
|
||||
> **Проверка в CI без `--continue` скрывает часть проблем.** Сборка падает на `ktlintCheck`, и `detekt` не выполняется - разработчик исправляет форматирование, пушит, и только тогда узнаёт о замечаниях detekt. Два цикла вместо одного.
|
||||
>
|
||||
> **Git-хук с detekt делает коммиты медленными.** Полный анализ среднего проекта - десятки секунд. В хук ставьте только `ktlintFormat`, detekt оставьте для CI.
|
||||
>
|
||||
> **`@Suppress` с неверным именем правила молча не работает.** Имена правил detekt (`MagicNumber`) и ktlint (`ktlint:standard:max-line-length`) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется.
|
||||
|
||||
Далее: [Тестирование](../Тестирование/Тестирование.md)
|
||||
@@ -1,379 +0,0 @@
|
||||
---
|
||||
title: Структура проекта
|
||||
description: Каталоги исходников, пакеты, многомодульность, смешанный Kotlin+Java код и convention plugins
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- gradle
|
||||
---
|
||||
|
||||
Структура Kotlin-проекта наследует соглашения Maven, но с двумя отличиями: каталог называется `kotlin`, а требование «путь пакета = путь каталога» необязательно.
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
├── gradle.properties
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml
|
||||
│ └── wrapper/
|
||||
├── gradlew
|
||||
└── src/
|
||||
├── main/
|
||||
│ ├── kotlin/com/example/
|
||||
│ └── resources/
|
||||
└── test/
|
||||
├── kotlin/com/example/
|
||||
└── resources/
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// src/main/kotlin/com/example/Main.kt
|
||||
package com.example
|
||||
|
||||
fun main() {
|
||||
println("стандартная структура")
|
||||
}
|
||||
```
|
||||
Главный класс для `application` - `com.example.MainKt`: имя файла с суффиксом `Kt`. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
## Source sets
|
||||
| Source set | Каталоги | Зависимости |
|
||||
|---|---|---|
|
||||
| `main` | `src/main/kotlin`, `src/main/java`, `src/main/resources` | `implementation`, `api` |
|
||||
| `test` | `src/test/kotlin`, `src/test/java`, `src/test/resources` | `testImplementation` + всё из `main` |
|
||||
|
||||
Gradle-плагин Kotlin добавляет `src/main/java` в Kotlin source set, поэтому `.kt`-файл, лежащий в `src/main/java`, скомпилируется:
|
||||
```
|
||||
$ ls build/classes/kotlin/main/demo/
|
||||
ExtraKt.class # из src/main/java/demo/Extra.kt
|
||||
KotlinImpl.class # из src/main/kotlin/demo/KotlinImpl.kt
|
||||
```
|
||||
Работает, но вводит в заблуждение - держите Kotlin в `src/main/kotlin`.
|
||||
|
||||
Добавить свой source set:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
sourceSets {
|
||||
val integrationTest by creating {
|
||||
kotlin.srcDir("src/integrationTest/kotlin")
|
||||
resources.srcDir("src/integrationTest/resources")
|
||||
compileClasspath += sourceSets["main"].output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
val integrationTest by tasks.registering(Test::class) {
|
||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
Для этой задачи есть и готовое решение - плагин `jvm-test-suite`:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
testing {
|
||||
suites {
|
||||
val test by getting(JvmTestSuite::class) {
|
||||
useKotlinTest()
|
||||
}
|
||||
register<JvmTestSuite>("integrationTest") {
|
||||
useKotlinTest()
|
||||
dependencies {
|
||||
implementation(project())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Пакеты и каталоги
|
||||
В Kotlin объявленный пакет не обязан совпадать с путём каталога:
|
||||
```kotlin
|
||||
// файл src/main/kotlin/Utils.kt — лежит в корне
|
||||
package com.example.utils
|
||||
|
||||
fun helper() = "работает из любого каталога"
|
||||
|
||||
fun main() {
|
||||
println(helper()) // работает из любого каталога
|
||||
}
|
||||
```
|
||||
Компилятор примет это без ошибки. Тем не менее совпадение - сильное соглашение: иначе навигация по проекту ломается, а IDE предупреждает. Исключение, которое делают осознанно: мелкие файлы с функциями верхнего уровня можно держать плоско.
|
||||
|
||||
Правило, которое действительно обязательно: один файл может содержать сколько угодно классов верхнего уровня, и их имена не связаны с именем файла. Это заметно отличается от Java. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
|
||||
|
||||
## Как раскладывать файлы
|
||||
```
|
||||
com/example/
|
||||
├── Main.kt # точка входа
|
||||
├── domain/
|
||||
│ ├── User.kt # data class User + связанные типы
|
||||
│ ├── Order.kt
|
||||
│ └── OrderStatus.kt # enum или sealed
|
||||
├── repository/
|
||||
│ ├── UserRepository.kt # interface
|
||||
│ └── UserRepositoryImpl.kt
|
||||
├── service/
|
||||
│ └── OrderService.kt
|
||||
├── api/
|
||||
│ ├── Routes.kt # функции верхнего уровня
|
||||
│ └── Dto.kt # несколько data class в одном файле
|
||||
└── util/
|
||||
├── Strings.kt # функции-расширения
|
||||
└── Dates.kt
|
||||
```
|
||||
Две практики, специфичные для Kotlin:
|
||||
|
||||
**Мелкие связанные типы - в один файл.** `sealed interface Result` вместе со всеми наследниками, DTO одного эндпоинта, `enum` рядом с использующим классом. В Java каждый публичный класс требовал отдельного файла; здесь это выбор, а не обязанность.
|
||||
|
||||
**Расширения группируются по типу-получателю, а не по назначению.** `Strings.kt` со всеми расширениями `String`, `Collections.kt` - для коллекций. Это заменяет утилитные классы вроде `StringUtils`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
## Смешанный Kotlin + Java
|
||||
Один модуль может содержать оба языка, причём зависимости допускаются в обе стороны одновременно:
|
||||
```
|
||||
src/main/
|
||||
├── kotlin/demo/KotlinImpl.kt
|
||||
└── java/demo/
|
||||
├── JavaBase.java
|
||||
└── UsesKotlin.java
|
||||
```
|
||||
```java
|
||||
// src/main/java/demo/JavaBase.java
|
||||
package demo;
|
||||
|
||||
public abstract class JavaBase {
|
||||
public abstract String name();
|
||||
public String describe() { return "Java-база: " + name(); }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// src/main/kotlin/demo/KotlinImpl.kt
|
||||
// Kotlin наследуется от Java-класса
|
||||
// class KotlinImpl : JavaBase() {
|
||||
// override fun name() = "из Kotlin"
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("Java-база: из Kotlin") // Java-база: из Kotlin
|
||||
}
|
||||
```
|
||||
```java
|
||||
// src/main/java/demo/UsesKotlin.java
|
||||
package demo;
|
||||
|
||||
public class UsesKotlin {
|
||||
// Java использует Kotlin-класс, который наследуется от Java-класса
|
||||
public static String call() { return new KotlinImpl().describe(); }
|
||||
}
|
||||
```
|
||||
Это работает, хотя выглядит как циклическая зависимость. Порядок компиляции:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Java-исходники"] -->|"читаются как стабы"| B["kotlinc:<br/>компиляция Kotlin"]
|
||||
B --> C["Kotlin .class"]
|
||||
C -->|"в classpath"| D["javac:<br/>компиляция Java"]
|
||||
D --> E["Java .class"]
|
||||
```
|
||||
|
||||
`kotlinc` получает `.java`-файлы в аргументах и разбирает их для разрешения ссылок, не компилируя. Затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java может наследоваться от Kotlin-класса, который наследуется от Java-класса. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
|
||||
|
||||
Ограничение: эта схема работает в пределах одного модуля. Между модулями циклов быть не может.
|
||||
|
||||
## Многомодульный проект
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
├── gradle/libs.versions.toml
|
||||
├── build-logic/ # convention plugins
|
||||
│ ├── settings.gradle.kts
|
||||
│ └── src/main/kotlin/
|
||||
│ └── kotlin-conventions.gradle.kts
|
||||
├── domain/ # чистая логика, без зависимостей
|
||||
├── data/ # БД, HTTP
|
||||
├── service/ # бизнес-логика
|
||||
└── app/ # точка входа, DI
|
||||
```
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
rootProject.name = "myapp"
|
||||
|
||||
include("domain", "data", "service", "app")
|
||||
```
|
||||
```kotlin
|
||||
// корневой build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0" apply false
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
"testImplementation"(kotlin("test"))
|
||||
}
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
dependencies {
|
||||
implementation(project(":service"))
|
||||
implementation(project(":data"))
|
||||
}
|
||||
```
|
||||
Направление зависимостей важно: `app` → `service` → `data` → `domain`, и `domain` не зависит ни от чего. Gradle обнаружит цикл между модулями и откажется собирать.
|
||||
|
||||
## Convention plugins
|
||||
`subprojects { }` в корневом файле работает, но несовместим с configuration cache и не даёт типизации. Правильное решение - convention plugin:
|
||||
```
|
||||
build-logic/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
└── src/main/kotlin/kotlin-conventions.gradle.kts
|
||||
```
|
||||
```kotlin
|
||||
// build-logic/build.gradle.kts
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0")
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// build-logic/src/main/kotlin/kotlin-conventions.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"testImplementation"(kotlin("test"))
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
pluginManagement {
|
||||
includeBuild("build-logic")
|
||||
}
|
||||
|
||||
rootProject.name = "myapp"
|
||||
include("domain", "data", "service", "app")
|
||||
```
|
||||
```kotlin
|
||||
// domain/build.gradle.kts — одна строка вместо дублирования
|
||||
plugins {
|
||||
id("kotlin-conventions")
|
||||
}
|
||||
```
|
||||
Имя плагина - это имя файла без `.gradle.kts`. Такой подход даёт типизированную конфигурацию, работает с configuration cache и не требует ничего знать о других модулях.
|
||||
|
||||
Исторически ту же роль играл каталог `buildSrc/`. Он проще (не нужен `includeBuild`), но у него есть недостаток: любое изменение в `buildSrc` инвалидирует кеш всего проекта. Для больших проектов предпочтителен `build-logic` как included build.
|
||||
|
||||
## Ресурсы
|
||||
```kotlin
|
||||
fun main() {
|
||||
// чтение из src/main/resources
|
||||
val stream = object {}.javaClass.getResourceAsStream("/config.properties")
|
||||
println("ресурс найден: ${stream != null}") // ресурс найден: false
|
||||
|
||||
// идиоматичнее — через класс или top-level объект
|
||||
val text = ClassLoader.getSystemResourceAsStream("missing.txt")
|
||||
?.bufferedReader()?.readText() ?: "ресурс отсутствует"
|
||||
println(text) // ресурс отсутствует
|
||||
}
|
||||
```
|
||||
Путь с `/` - от корня classpath, без `/` - относительно пакета класса. Файлы из `src/main/resources` попадают в корень JAR. Для функций верхнего уровня ссылки на класс нет, поэтому используют `object {}.javaClass` или `ClassLoader`.
|
||||
|
||||
## Файлы, которые нужны в репозитории
|
||||
| Путь | Коммитить |
|
||||
|---|---|
|
||||
| `gradlew`, `gradlew.bat` | да |
|
||||
| `gradle/wrapper/gradle-wrapper.jar` | да |
|
||||
| `gradle/wrapper/gradle-wrapper.properties` | да |
|
||||
| `gradle/libs.versions.toml` | да |
|
||||
| `gradle.properties` | да |
|
||||
| `.gradle/`, `build/` | нет |
|
||||
| `local.properties` | нет |
|
||||
| `.idea/` | частично (`codeStyles`, `inspectionProfiles`) |
|
||||
| `*.iml` | нет |
|
||||
|
||||
```
|
||||
# .gitignore
|
||||
build/
|
||||
.gradle/
|
||||
local.properties
|
||||
*.iml
|
||||
.idea/*
|
||||
!.idea/codeStyles/
|
||||
!.idea/inspectionProfiles/
|
||||
.kotlin/
|
||||
```
|
||||
Каталог `.kotlin/` появился в Kotlin 2.x - это служебные данные компилятора, их не коммитят.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `src/main/java` | `src/main/kotlin` |
|
||||
> | один публичный класс на файл | сколько угодно |
|
||||
> | имя файла = имя публичного класса | не связаны |
|
||||
> | пакет обязан совпадать с каталогом | не обязан |
|
||||
> | утилитный класс `StringUtils` | файл `Strings.kt` с расширениями |
|
||||
> | `Main.class` | `MainKt.class` |
|
||||
>
|
||||
> **Освобождение от «один класс на файл» меняет раскладку кода.** В Java структура каталогов повторяла структуру типов: 20 классов - 20 файлов. В Kotlin файл - единица логической группировки, а не технического ограничения. `sealed interface` со всеми вариантами в одном файле читается целиком, а не по кускам. Побочный эффект: навигация «по файлам» работает хуже, и важнее осмысленные имена файлов.
|
||||
>
|
||||
> **Несовпадение пакета и каталога разрешено, но соглашение всё равно действует.** Kotlin не требует соответствия, потому что для компилятора оно не нужно: пакет указан в файле. Однако IDE, инструменты анализа и люди ожидают совпадения, поэтому отклоняются от него редко и только для плоских файлов с функциями.
|
||||
>
|
||||
> **Смешанная компиляция в одном модуле - то, чего нет в однодходовом `javac`.** Двухфазная схема (kotlinc читает Java-стабы, затем javac видит Kotlin-классы) позволяет взаимные ссылки между языками внутри модуля. Это ключевая возможность для миграции: можно конвертировать по одному файлу, не трогая остальное. Ограничение - только в пределах модуля.
|
||||
>
|
||||
> **Convention plugins - ответ Gradle на отсутствие наследования `pom.xml`.** В Maven общая конфигурация выносится в parent POM, и это декларативно. В Gradle аналога не было: писали `subprojects { }` или `allprojects { }`, что работает, но несовместимо с configuration cache и лишено типизации. Convention plugin - это обычный Gradle-плагин, лежащий в проекте: типизированный, кешируемый, переиспользуемый.
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [jar](../../Программирование%20на%20Java/JDK/jar.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Главный класс для `application` - не имя файла.** Для `src/main/kotlin/com/example/Main.kt` это `com.example.MainKt`, а не `com.example.Main`. Ошибка проявляется как `ClassNotFoundException` при `./gradlew run`. Если в файле стоит `@file:JvmName("App")`, то класс - `com.example.App`.
|
||||
>
|
||||
> **Kotlin-файлы в `src/main/java` компилируются молча.** Плагин добавляет этот каталог в Kotlin source set, так что ошибки не будет - но структура станет путаной, а инструменты, ожидающие Java в `java/`, могут сбиться.
|
||||
>
|
||||
> **`buildSrc` инвалидирует кеш всего проекта.** Любая правка в `buildSrc` заставляет Gradle пересобрать все модули. На больших проектах это минуты на каждое изменение конфигурации - используйте `build-logic` как included build.
|
||||
>
|
||||
> **`subprojects { }` несовместим с configuration cache.** Он обращается к другим проектам в фазе конфигурации, что кеш запрещает. При включении `org.gradle.configuration-cache=true` появятся ошибки «invocation of 'Project.subprojects'». Решение - convention plugins.
|
||||
>
|
||||
> **Циклы между модулями запрещены.** Если `data` ссылается на `service`, а `service` на `data`, Gradle откажется собирать с ошибкой «Circular dependency between tasks». Внутри одного модуля цикл Kotlin↔Java допустим, между модулями - нет. Общие типы выносят в третий модуль.
|
||||
>
|
||||
> **Ресурс не найден из функции верхнего уровня.** `javaClass` недоступен вне класса; `this::class.java` в top-level функции не скомпилируется. Используйте `object {}.javaClass.getResourceAsStream(...)` или `ClassLoader.getSystemResourceAsStream(...)`.
|
||||
>
|
||||
> **Путь к ресурсу чувствителен к ведущему слэшу.** `getResourceAsStream("config.txt")` ищет относительно пакета класса, `getResourceAsStream("/config.txt")` - от корня classpath. Первый вариант неожиданно возвращает `null`, если класс лежит в пакете.
|
||||
>
|
||||
> **`gradle-wrapper.jar` обязателен в репозитории.** Без него `./gradlew` не работает, и сборка пойдёт локальным Gradle другой версии. Если политика запрещает бинарники, добавьте в CI шаг `gradle/actions/wrapper-validation`.
|
||||
>
|
||||
> **Переименование файла меняет имя класса.** `Utils.kt` даёт `UtilsKt`, после переименования в `Helpers.kt` - `HelpersKt`. Для Java-клиентов и рефлексии это ломающее изменение; фиксируйте имя через `@file:JvmName`.
|
||||
>
|
||||
> **Тесты не видят `internal` из другого модуля.** `internal` в Kotlin - это видимость в пределах модуля компиляции, и `src/test` - отдельный source set. Gradle-плагин специально связывает `test` с `main`, поэтому тесты `internal`-члены видят; но тесты **другого** модуля - нет.
|
||||
>
|
||||
> **Файл без `package` попадает в корневой пакет.** Такой класс не импортируется из других пакетов нормальным способом и недоступен из Java-кода в пакете. Всегда объявляйте пакет, кроме скриптов.
|
||||
|
||||
Далее: [Компилятор K2](Компилятор%20K2.md)
|
||||
@@ -1,336 +0,0 @@
|
||||
---
|
||||
title: Result и runCatching
|
||||
description: Ошибка как значение: Result, runCatching, свои sealed-типы и когда что выбирать
|
||||
tags:
|
||||
- kotlin
|
||||
- exceptions
|
||||
---
|
||||
|
||||
Исключение - не единственный способ сообщить об ошибке. Ошибку можно вернуть как значение, и тогда компилятор заставит её обработать.
|
||||
|
||||
Три уровня, от простого к гибкому:
|
||||
|
||||
| Способ | Что несёт ошибка | Когда |
|
||||
|---|---|---|
|
||||
| `T?` | ничего, только «не получилось» | причина не важна |
|
||||
| `Result<T>` | `Throwable` | причина нужна, но типизировать её незачем |
|
||||
| свой `sealed`-тип | что угодно | причин несколько и они разные |
|
||||
|
||||
## Nullable как простейший Result
|
||||
Для одной очевидной причины сбоя `null` достаточно:
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("42".toIntOrNull()) // 42
|
||||
println("abc".toIntOrNull()) // null
|
||||
println("abc".toIntOrNull() ?: 0) // 0
|
||||
}
|
||||
```
|
||||
Стандартная библиотека придерживается соглашения: `toInt()` бросает, `toIntOrNull()` возвращает `null`; `first()` бросает, `firstOrNull()` - нет.
|
||||
```kotlin
|
||||
fun main() {
|
||||
val items = listOf(1, 2, 3)
|
||||
println(items.getOrNull(10)) // null
|
||||
println(items.firstOrNull { it > 5 }) // null
|
||||
println(mapOf("a" to 1)["b"]) // null
|
||||
}
|
||||
```
|
||||
Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
|
||||
|
||||
## Result
|
||||
`Result<T>` хранит либо значение, либо `Throwable`.
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ok: Result<Int> = Result.success(42)
|
||||
val fail: Result<Int> = Result.failure(IllegalStateException("сбой"))
|
||||
|
||||
println(ok.isSuccess) // true
|
||||
println(fail.isFailure) // true
|
||||
println(ok.getOrNull()) // 42
|
||||
println(fail.getOrNull()) // null
|
||||
println(fail.exceptionOrNull()?.message) // сбой
|
||||
}
|
||||
```
|
||||
|
||||
### runCatching
|
||||
Оборачивает блок: результат - `Result`, исключение - `failure`.
|
||||
```kotlin
|
||||
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
|
||||
|
||||
fun main() {
|
||||
println(parse("42")) // Success(42)
|
||||
println(parse("abc").exceptionOrNull()?.javaClass?.simpleName) // NumberFormatException
|
||||
}
|
||||
```
|
||||
Есть вариант-расширение с получателем:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val length = "привет".runCatching { length }
|
||||
println(length.getOrNull()) // 6
|
||||
}
|
||||
```
|
||||
|
||||
### Извлечение значения
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ok = runCatching { 42 }
|
||||
val fail = runCatching<Int> { error("сбой") }
|
||||
|
||||
println(ok.getOrNull()) // 42
|
||||
println(fail.getOrDefault(0)) // 0
|
||||
println(fail.getOrElse { e -> e.message!!.length }) // 4
|
||||
println(ok.getOrThrow()) // 42
|
||||
|
||||
try {
|
||||
fail.getOrThrow()
|
||||
} catch (e: IllegalStateException) {
|
||||
println("пробросилось: ${e.message}") // пробросилось: сбой
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Преобразования
|
||||
```kotlin
|
||||
fun main() {
|
||||
val r = runCatching { "42" }
|
||||
.map { it.toInt() } // только при успехе
|
||||
.map { it * 2 }
|
||||
println(r.getOrNull()) // 84
|
||||
|
||||
// mapCatching перехватывает исключение внутри преобразования
|
||||
val bad = runCatching { "abc" }.mapCatching { it.toInt() }
|
||||
println(bad.isFailure) // true
|
||||
|
||||
// map не перехватывает — исключение улетит наружу
|
||||
val thrown = runCatching { runCatching { "abc" }.map { it.toInt() } }
|
||||
println(thrown.isFailure) // true
|
||||
}
|
||||
```
|
||||
Разница существенная: `map` предполагает, что преобразование не падает, `mapCatching` - что может.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val r = runCatching<Int> { error("сбой") }
|
||||
.recover { 0 } // заменяем ошибку значением
|
||||
println(r.getOrNull()) // 0
|
||||
|
||||
val folded = runCatching { "42".toInt() }
|
||||
.fold(
|
||||
onSuccess = { "значение $it" },
|
||||
onFailure = { "ошибка ${it.message}" },
|
||||
)
|
||||
println(folded) // значение 42
|
||||
}
|
||||
```
|
||||
|
||||
### onSuccess и onFailure
|
||||
Побочные эффекты без изменения значения - они возвращают сам `Result`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
runCatching { "abc".toInt() }
|
||||
.onSuccess { println("получили $it") }
|
||||
.onFailure { println("не вышло: ${it.message}") }
|
||||
.getOrDefault(0)
|
||||
.let { println("итог: $it") }
|
||||
// не вышло: For input string: "abc"
|
||||
// итог: 0
|
||||
}
|
||||
```
|
||||
|
||||
## Свой sealed-тип
|
||||
Когда причин несколько и клиент должен их различать, `Result` слишком беден - в нём любая ошибка это `Throwable`.
|
||||
```kotlin
|
||||
sealed interface LoadResult {
|
||||
data class Success(val user: String) : LoadResult
|
||||
data object NotFound : LoadResult
|
||||
data class Forbidden(val reason: String) : LoadResult
|
||||
data class NetworkError(val retryAfter: Int) : LoadResult
|
||||
}
|
||||
|
||||
fun load(id: Int): LoadResult = when {
|
||||
id == 1 -> LoadResult.Success("Иван")
|
||||
id == 2 -> LoadResult.Forbidden("нет прав")
|
||||
id < 0 -> LoadResult.NetworkError(retryAfter = 5)
|
||||
else -> LoadResult.NotFound
|
||||
}
|
||||
|
||||
fun describe(r: LoadResult) = when (r) { // else не нужен
|
||||
is LoadResult.Success -> "загружен ${r.user}"
|
||||
LoadResult.NotFound -> "не найден"
|
||||
is LoadResult.Forbidden -> "запрещено: ${r.reason}"
|
||||
is LoadResult.NetworkError -> "повтор через ${r.retryAfter} с"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
listOf(1, 2, 3, -1).forEach { println(describe(load(it))) }
|
||||
// загружен Иван
|
||||
// запрещено: нет прав
|
||||
// не найден
|
||||
// повтор через 5 с
|
||||
}
|
||||
```
|
||||
Главное преимущество - компилятор проверяет, что обработаны все варианты. Добавили новый - все `when` перестанут компилироваться, пока его не учтут. С `Result` такой гарантии нет.
|
||||
|
||||
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
### Свой Result с типизированной ошибкой
|
||||
```kotlin
|
||||
sealed interface Outcome<out T, out E> {
|
||||
data class Ok<T>(val value: T) : Outcome<T, Nothing>
|
||||
data class Err<E>(val error: E) : Outcome<Nothing, E>
|
||||
}
|
||||
|
||||
enum class ParseError { EMPTY, NOT_A_NUMBER, NEGATIVE }
|
||||
|
||||
fun parsePositive(s: String): Outcome<Int, ParseError> = when {
|
||||
s.isBlank() -> Outcome.Err(ParseError.EMPTY)
|
||||
s.toIntOrNull() == null -> Outcome.Err(ParseError.NOT_A_NUMBER)
|
||||
s.toInt() < 0 -> Outcome.Err(ParseError.NEGATIVE)
|
||||
else -> Outcome.Ok(s.toInt())
|
||||
}
|
||||
|
||||
fun main() {
|
||||
listOf("42", "", "abc", "-1").forEach { input ->
|
||||
when (val r = parsePositive(input)) {
|
||||
is Outcome.Ok -> println("'$input' -> ${r.value}")
|
||||
is Outcome.Err -> println("'$input' -> ошибка ${r.error}")
|
||||
}
|
||||
}
|
||||
// '42' -> 42
|
||||
// '' -> ошибка EMPTY
|
||||
// 'abc' -> ошибка NOT_A_NUMBER
|
||||
// '-1' -> ошибка NEGATIVE
|
||||
}
|
||||
```
|
||||
`Nothing` в позиции второго параметра позволяет `Ok` подходить под любой тип ошибки. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), [Вариантность](../Дженерики/Вариантность.md)
|
||||
|
||||
## Что выбрать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Ошибка возможна"] --> B{"Вызывающий может<br/>что-то сделать?"}
|
||||
B -->|"нет, это баг"| C["исключение:<br/>require, check, error"]
|
||||
B -->|"да"| D{"Причину нужно<br/>различать?"}
|
||||
D -->|"нет"| E["T?"]
|
||||
D -->|"да, но обработка общая"| F["Result<T>"]
|
||||
D -->|"да, по-разному"| G["свой sealed-тип"]
|
||||
```
|
||||
|
||||
Практическое разделение:
|
||||
- **исключение** - для нарушенных предпосылок: неверный аргумент, недопустимое состояние. Это ошибка программиста, её исправляют в коде, а не обрабатывают. Подробнее: [Проверки и контракты](Проверки%20и%20контракты.md)
|
||||
- **`T?`, `Result`, sealed** - для ожидаемых ситуаций: нет записи в БД, сеть недоступна, ввод пользователя некорректен
|
||||
|
||||
## Граница с исключениями
|
||||
`runCatching` удобен на границе: внешний код бросает, ваш возвращает значение.
|
||||
```kotlin
|
||||
class User7(val id: Int, val name: String)
|
||||
|
||||
fun fetchFromDb(id: Int): User7 =
|
||||
if (id == 1) User7(1, "Иван") else throw NoSuchElementException("нет пользователя $id")
|
||||
|
||||
fun findUser(id: Int): User7? = runCatching { fetchFromDb(id) }.getOrNull()
|
||||
|
||||
fun main() {
|
||||
println(findUser(1)?.name) // Иван
|
||||
println(findUser(99)) // null
|
||||
}
|
||||
```
|
||||
Обратное направление - когда API требует исключений, а внутри `Result`:
|
||||
```kotlin
|
||||
fun loadOrThrow(id: Int): String = runCatching {
|
||||
require(id > 0) { "id должен быть положительным" }
|
||||
"данные $id"
|
||||
}.getOrThrow()
|
||||
|
||||
fun main() {
|
||||
println(loadOrThrow(1)) // данные 1
|
||||
try {
|
||||
loadOrThrow(-1)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // id должен быть положительным
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | checked exceptions в сигнатуре | `Result<T>` или sealed-тип |
|
||||
> | `Optional<T>` | `T?` - часть системы типов |
|
||||
> | `Optional.map`/`orElse` | `?.let`/`?:` |
|
||||
> | нет аналога `Result` в JDK | `Result<T>` в stdlib |
|
||||
> | `try`/`catch` для управления | `runCatching` как выражение |
|
||||
>
|
||||
> **`Result` частично возвращает то, что отняло отсутствие checked exceptions.** Сигнатура `fun load(): Result<User>` сообщает, что функция может не справиться, и компилятор не даст использовать значение без разбора. Разница с checked exceptions в том, что это выбор автора функции, а не навязанное правило, и такие значения свободно проходят через `map`, `filter` и другие обобщённые функции.
|
||||
>
|
||||
> **`T?` вместо `Optional<T>`.** `Optional` - обёртка-объект: аллокация, два уровня значений (`null` и пустой `Optional`), неудобство в полях. Nullable-тип в Kotlin существует только при компиляции, а в байт-коде это обычная ссылка - накладных расходов нет:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val name: String? = null
|
||||
> println(name?.uppercase() ?: "<нет>") // <нет>
|
||||
> }
|
||||
> ```
|
||||
> ```java
|
||||
> Optional<String> name = Optional.empty();
|
||||
> System.out.println(name.map(String::toUpperCase).orElse("<нет>"));
|
||||
> ```
|
||||
>
|
||||
> **Sealed-типы вместо иерархии исключений.** В Java разные причины сбоя обычно выражают классами исключений и ловят их по типу. Sealed-тип даёт то же различение, но с проверкой полноты при компиляции и без стоимости стектрейса.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`runCatching` перехватывает всё, включая то, что перехватывать нельзя.** Он ловит `Throwable`, то есть `OutOfMemoryError`, `StackOverflowError` и - самое опасное - `CancellationException` в корутинах:
|
||||
> ```kotlin
|
||||
> // Требует kotlinx-coroutines-core.
|
||||
> // suspend fun broken() {
|
||||
> // runCatching { delay(1000) } // проглотит отмену корутины
|
||||
> // }
|
||||
> //
|
||||
> // suspend fun fixed() = runCatching { delay(1000) }
|
||||
> // .onFailure { if (it is CancellationException) throw it }
|
||||
> ```
|
||||
> В корутинах либо проверяйте тип вручную, либо используйте обычный `try`/`catch` с перехватом конкретных типов. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
|
||||
>
|
||||
> **`Result` нельзя вернуть из функции в публичном API без оговорок.** До Kotlin 1.5 это было запрещено совсем; сейчас разрешено, но тип по-прежнему `value class`, поэтому из Java он виден странно и его нельзя использовать как тип свойства:
|
||||
> ```kotlin
|
||||
> class Holder {
|
||||
> // val r: Result<Int> = Result.success(1) // компилируется, но Result как поле боксируется
|
||||
> private val value: Int = 1
|
||||
> fun get(): Result<Int> = Result.success(value)
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Holder().get().getOrNull()) } // 1
|
||||
> ```
|
||||
>
|
||||
> **`Result` стирает тип ошибки.** Все причины сбоя - `Throwable`, поэтому различать их приходится через `is`, и компилятор полноту не проверяет:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val r = runCatching<Int> { error("сбой") }
|
||||
> val msg = when (val e = r.exceptionOrNull()) {
|
||||
> null -> "успех"
|
||||
> is IllegalStateException -> "состояние: ${e.message}"
|
||||
> else -> "прочее" // else обязателен
|
||||
> }
|
||||
> println(msg) // состояние: сбой
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Создание `failure` всё равно создаёт исключение со стектрейсом** - то есть `Result` не даёт экономии на производительности, если ошибку формирует `runCatching`. Дешёвый вариант - sealed-тип без `Throwable` внутри.
|
||||
>
|
||||
> **Проигнорированный `Result` не вызывает предупреждения:**
|
||||
> ```kotlin
|
||||
> fun save(): Result<Unit> = runCatching { error("не записалось") }
|
||||
>
|
||||
> fun main() {
|
||||
> save() // ошибка потеряна молча
|
||||
> println(save().isFailure) // true — так правильно
|
||||
> }
|
||||
> ```
|
||||
> В отличие от исключения, которое всплывёт само, забытый `Result` ничего не делает. Это основной риск подхода.
|
||||
>
|
||||
> **Цепочка `map` на `Result` может бросить.** `map` не перехватывает исключения из своей лямбды - для этого есть `mapCatching`.
|
||||
>
|
||||
> **`getOrThrow` на чужом `Result` бросает исходное исключение,** а не обёртку, поэтому стектрейс указывает на место первоначального сбоя, а не на `getOrThrow`. Для диагностики это хорошо, для чтения логов - неожиданно.
|
||||
>
|
||||
> **Смешение подходов в одном слое запутывает.** Если часть функций репозитория возвращает `null`, часть `Result`, а часть бросает, клиентский код обрастает тремя видами обработки. Выберите одно соглашение на слой.
|
||||
|
||||
Далее: [Проверки и контракты](Проверки%20и%20контракты.md)
|
||||
@@ -1,403 +0,0 @@
|
||||
---
|
||||
title: Проверки и контракты
|
||||
description: require, check, error, assert и контракты компилятора для умного приведения
|
||||
tags:
|
||||
- kotlin
|
||||
- exceptions
|
||||
---
|
||||
|
||||
Стандартная библиотека даёт четыре функции для проверки предпосылок. Выбор между ними определяется тем, **кто виноват** в нарушении.
|
||||
|
||||
| Функция | Бросает | Виноват | Что проверяет |
|
||||
|---|---|---|---|
|
||||
| `require` | `IllegalArgumentException` | вызывающий | аргументы |
|
||||
| `check` | `IllegalStateException` | вызывающий или сам объект | состояние |
|
||||
| `error` | `IllegalStateException` | - | недостижимая ветка |
|
||||
| `assert` | `AssertionError` | - | инвариант при отладке |
|
||||
|
||||
## require - проверка аргументов
|
||||
```kotlin
|
||||
fun withdraw(balance: Long, amount: Long): Long {
|
||||
require(amount > 0) { "сумма должна быть положительной, получено $amount" }
|
||||
require(amount <= balance) { "недостаточно средств: $amount > $balance" }
|
||||
return balance - amount
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(withdraw(100, 30)) // 70
|
||||
try {
|
||||
withdraw(100, -5)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // сумма должна быть положительной, получено -5
|
||||
}
|
||||
}
|
||||
```
|
||||
Сообщение передаётся лямбдой, поэтому строка собирается только при нарушении - в успешном случае интерполяция не выполняется.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val n = 5
|
||||
require(n > 0) // без сообщения: "Failed requirement."
|
||||
require(n > 0) { "дорогое сообщение: ${(1..1000).sum()}" } // лямбда не вызовется
|
||||
println("проверки пройдены")
|
||||
}
|
||||
```
|
||||
|
||||
### requireNotNull
|
||||
Проверяет и **возвращает** non-null значение:
|
||||
```kotlin
|
||||
fun greet(name: String?): String {
|
||||
val safe = requireNotNull(name) { "имя обязательно" }
|
||||
return "Привет, ${safe.uppercase()}" // safe имеет тип String
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(greet("иван")) // Привет, ИВАН
|
||||
try {
|
||||
greet(null)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // имя обязательно
|
||||
}
|
||||
}
|
||||
```
|
||||
Работает и как инструкция - после вызова переменная умно приводится:
|
||||
```kotlin
|
||||
fun process(input: String?): Int {
|
||||
requireNotNull(input)
|
||||
return input.length // smart cast: String? -> String
|
||||
}
|
||||
|
||||
fun main() { println(process("abc")) } // 3
|
||||
```
|
||||
Это работает благодаря контракту, см. ниже.
|
||||
|
||||
## check - проверка состояния
|
||||
```kotlin
|
||||
class Connection2 {
|
||||
private var open = false
|
||||
|
||||
fun connect() { open = true }
|
||||
|
||||
fun send(data: String): String {
|
||||
check(open) { "соединение не открыто" }
|
||||
return "отправлено: $data"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = Connection2()
|
||||
try {
|
||||
c.send("данные")
|
||||
} catch (e: IllegalStateException) {
|
||||
println(e.message) // соединение не открыто
|
||||
}
|
||||
c.connect()
|
||||
println(c.send("данные")) // отправлено: данные
|
||||
}
|
||||
```
|
||||
Разница с `require` только в типе исключения, но она информативна: `IllegalArgumentException` говорит «ты передал не то», `IllegalStateException` - «ты вызвал не вовремя».
|
||||
|
||||
`checkNotNull` - аналог `requireNotNull` для состояния:
|
||||
```kotlin
|
||||
class Service18 {
|
||||
private var config: String? = null
|
||||
|
||||
fun init2(value: String) { config = value }
|
||||
|
||||
fun run2(): String {
|
||||
val cfg = checkNotNull(config) { "сначала вызовите init2()" }
|
||||
return "работаю с $cfg"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service18()
|
||||
try {
|
||||
s.run2()
|
||||
} catch (e: IllegalStateException) {
|
||||
println(e.message) // сначала вызовите init2()
|
||||
}
|
||||
s.init2("настройки")
|
||||
println(s.run2()) // работаю с настройки
|
||||
}
|
||||
```
|
||||
|
||||
## error - недостижимая ветка
|
||||
`error(msg)` бросает `IllegalStateException`. Её тип - `Nothing`, поэтому она подходит в любое выражение:
|
||||
```kotlin
|
||||
fun describe(code: Int): String = when (code) {
|
||||
200 -> "ок"
|
||||
404 -> "не найдено"
|
||||
500 -> "ошибка сервера"
|
||||
else -> error("неизвестный код: $code")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe(200)) // ок
|
||||
try {
|
||||
describe(999)
|
||||
} catch (e: IllegalStateException) {
|
||||
println(e.message) // неизвестный код: 999
|
||||
}
|
||||
}
|
||||
```
|
||||
Благодаря типу `Nothing` компилятор понимает, что ветка не возвращает значение, и `when` остаётся выражением. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
`TODO()` - то же для незаписанного кода:
|
||||
```kotlin
|
||||
fun futureFeature(): String = TODO("реализовать после релиза")
|
||||
|
||||
fun main() {
|
||||
try {
|
||||
futureFeature()
|
||||
} catch (e: NotImplementedError) {
|
||||
println(e.message) // An operation is not implemented: реализовать после релиза
|
||||
}
|
||||
}
|
||||
```
|
||||
`TODO()` возвращает `Nothing`, поэтому подходит на место любого типа - код компилируется, пока функция не написана.
|
||||
|
||||
## assert - только для отладки
|
||||
```kotlin
|
||||
fun average(values: List<Int>): Double {
|
||||
val result = values.sum().toDouble() / values.size
|
||||
assert(result >= values.min()) { "среднее меньше минимума — ошибка в вычислении" }
|
||||
return result
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(average(listOf(1, 2, 3))) // 2.0
|
||||
}
|
||||
```
|
||||
`assert` проверяется **только** при запуске JVM с флагом `-ea` (`-enableassertions`). Без него вызов ничего не делает, но лямбда-сообщение всё равно остаётся объектом в байт-коде.
|
||||
|
||||
Практический вывод: `assert` в Kotlin почти не используют. Для проверок, которые должны работать в продакшене, берут `require` и `check`.
|
||||
|
||||
## Контракты
|
||||
Контракт - способ сообщить компилятору то, что он не может вывести сам. Именно поэтому `requireNotNull` даёт умное приведение.
|
||||
|
||||
```kotlin
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun String?.isNotNullOrBlank(): Boolean {
|
||||
contract { returns(true) implies (this@isNotNullOrBlank != null) }
|
||||
return !this.isNullOrBlank()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val input: String? = "текст"
|
||||
if (input.isNotNullOrBlank()) {
|
||||
println(input.length) // smart cast: String? -> String
|
||||
}
|
||||
}
|
||||
```
|
||||
Без контракта `input.length` потребовал бы `!!` или `?.` - компилятор не знает, что `true` из функции означает non-null.
|
||||
|
||||
Стандартная библиотека использует контракты в `isNullOrEmpty`, `isNullOrBlank`, `require`, `check`, `requireNotNull`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val s: String? = "abc"
|
||||
if (!s.isNullOrEmpty()) {
|
||||
println(s.length) // smart cast работает благодаря контракту
|
||||
}
|
||||
|
||||
val n: Int? = 5
|
||||
check(n != null)
|
||||
println(n + 1) // 6, smart cast после check
|
||||
}
|
||||
```
|
||||
|
||||
### Виды контрактов
|
||||
```kotlin
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun requirePositive(value: Int?) {
|
||||
contract { returns() implies (value != null) } // вернулась -> value не null
|
||||
require(value != null && value > 0) { "нужно положительное число" }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> runOnce(block: () -> T): T {
|
||||
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
|
||||
return block()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val x: Int? = 5
|
||||
requirePositive(x)
|
||||
println(x + 1) // 6 — smart cast
|
||||
|
||||
val value: String // можно не инициализировать...
|
||||
runOnce { value = "присвоено" } // ...потому что контракт гарантирует один вызов
|
||||
println(value) // присвоено
|
||||
}
|
||||
```
|
||||
`callsInPlace` с `EXACTLY_ONCE` позволяет присваивать `val` внутри лямбды - компилятор знает, что присваивание произойдёт ровно один раз. На этом работают `run`, `let`, `apply` и `with`.
|
||||
|
||||
| Контракт | Что обещает |
|
||||
|---|---|
|
||||
| `returns() implies (x)` | если функция вернулась нормально, условие `x` истинно |
|
||||
| `returns(true) implies (x)` | если вернула `true`, условие `x` истинно |
|
||||
| `returns(null) implies (x)` | если вернула `null`, условие `x` истинно |
|
||||
| `callsInPlace(f, AT_MOST_ONCE)` | лямбда вызовется не больше одного раза |
|
||||
| `callsInPlace(f, EXACTLY_ONCE)` | ровно один раз |
|
||||
| `callsInPlace(f, AT_LEAST_ONCE)` | хотя бы один раз |
|
||||
|
||||
Требования к контракту:
|
||||
- только для функций верхнего уровня (не для методов класса)
|
||||
- блок `contract` - **первая** инструкция в теле
|
||||
- API экспериментальный: нужен `@OptIn(ExperimentalContracts::class)`
|
||||
- компилятор **не проверяет**, что контракт соответствует коду
|
||||
|
||||
Последний пункт - главный риск: контракт это обещание на доверии.
|
||||
|
||||
## Валидация данных: исключение или значение
|
||||
`require` подходит для нарушений, которые означают баг. Для пользовательского ввода лучше вернуть ошибку как значение:
|
||||
```kotlin
|
||||
data class Form4(val email: String, val age: Int)
|
||||
|
||||
fun validate(email: String, age: Int): List<String> = buildList {
|
||||
if ("@" !in email) add("email без @")
|
||||
if (age !in 0..150) add("недопустимый возраст: $age")
|
||||
}
|
||||
|
||||
fun create(email: String, age: Int): Result<Form4> {
|
||||
val errors = validate(email, age)
|
||||
return if (errors.isEmpty()) Result.success(Form4(email, age))
|
||||
else Result.failure(IllegalArgumentException(errors.joinToString("; ")))
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(create("a@b.ru", 20).getOrNull()) // Form4(email=a@b.ru, age=20)
|
||||
println(create("нет", 200).exceptionOrNull()?.message) // email без @; недопустимый возраст: 200
|
||||
}
|
||||
```
|
||||
Разница: `require` падает на первой ошибке, а пользователю обычно нужен список всех. Подробнее: [Result и runCatching](Result%20и%20runCatching.md)
|
||||
|
||||
## Инварианты в конструкторе
|
||||
```kotlin
|
||||
class Range4(val from: Int, val to: Int) {
|
||||
init {
|
||||
require(from <= to) { "неверный диапазон: $from..$to" }
|
||||
}
|
||||
|
||||
val size get() = to - from + 1
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class Email(val value: String) {
|
||||
init {
|
||||
require("@" in value) { "не похоже на email: $value" }
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Range4(1, 5).size) // 5
|
||||
println(Email("a@b.ru").value) // a@b.ru
|
||||
try {
|
||||
Range4(5, 1)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // неверный диапазон: 5..1
|
||||
}
|
||||
}
|
||||
```
|
||||
Проверка в конструкторе гарантирует, что невалидного объекта не существует, - дальше по коду проверять не нужно. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md), [Инициализация](../ООП/Инициализация.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `if (x < 0) throw new IllegalArgumentException(...)` | `require(x >= 0) { ... }` |
|
||||
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` + smart cast |
|
||||
> | `assert x > 0 : "msg"` | `assert(x > 0) { "msg" }` |
|
||||
> | Guava `Preconditions.checkArgument` | `require` в stdlib |
|
||||
> | `throw new UnsupportedOperationException()` | `TODO()` |
|
||||
> | нет аналога | контракты для smart cast |
|
||||
>
|
||||
> **`require` и `check` - это то, за чем в Java идут в Guava.** `Preconditions.checkArgument` и `checkState` из Guava выполняют ту же роль, но требуют внешней зависимости. В Kotlin они в stdlib и вдобавок `inline`, то есть вызова функции в байт-коде не остаётся.
|
||||
>
|
||||
> **Сообщение в лямбде экономит работу.** В Java `checkArgument("плохое значение: " + value)` собирает строку всегда, даже когда проверка пройдена. Передача лямбды плюс `inline` означает, что конкатенация происходит только при нарушении:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val value = 5
|
||||
> require(value > 0) { "дорогая строка: ${value.toString().repeat(1000).length}" }
|
||||
> println("лямбда не вызывалась")
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`requireNotNull` даёт smart cast, `Objects.requireNonNull` - нет.** В Java результат нужно присвоить: `this.name = Objects.requireNonNull(name)`. В Kotlin после проверки компилятор сам считает переменную non-null.
|
||||
>
|
||||
> **Контрактов в Java нет совсем.** Ближайший аналог - аннотации `@Nullable`/`@NotNull` и внешние анализаторы, которые работают вне компилятора и не влияют на типизацию.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`assert` по умолчанию выключен.** Без `-ea` при запуске JVM проверка не выполняется совсем:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> assert(1 == 2) { "это сообщение вы не увидите без -ea" }
|
||||
> println("программа продолжила работу")
|
||||
> }
|
||||
> ```
|
||||
> Код, который полагается на `assert` для защиты инвариантов в продакшене, не защищён ничем. Используйте `check`.
|
||||
>
|
||||
> **Путаница `require` и `check`.** Тип исключения - часть контракта: вызывающий может ловить `IllegalArgumentException`, чтобы отличить «я передал мусор» от «объект не готов». Перепутанные функции делают эту информацию ложной.
|
||||
>
|
||||
> **Сообщение без контекста бесполезно:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val size = -1
|
||||
> try {
|
||||
> require(size >= 0) { "неверный размер" } // какой именно?
|
||||
> } catch (e: IllegalArgumentException) {
|
||||
> println(e.message)
|
||||
> }
|
||||
> try {
|
||||
> require(size >= 0) { "размер должен быть >= 0, получено $size" }
|
||||
> } catch (e: IllegalArgumentException) {
|
||||
> println(e.message) // размер должен быть >= 0, получено -1
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`require` для пользовательского ввода - ошибка проектирования.** Некорректный ввод - штатная ситуация, а не баг. Исключение в этом случае заставляет клиентский код оборачивать каждый вызов в `try`/`catch` и сообщает только о первой проблеме.
|
||||
>
|
||||
> **Контракт не проверяется компилятором.** Неверный контракт ломает проверки типов - компилятор поверит обещанию и уберёт нужные проверки на `null`:
|
||||
> ```kotlin
|
||||
> import kotlin.contracts.ExperimentalContracts
|
||||
> import kotlin.contracts.contract
|
||||
>
|
||||
> @OptIn(ExperimentalContracts::class)
|
||||
> fun String?.lies(): Boolean {
|
||||
> contract { returns(true) implies (this@lies != null) }
|
||||
> return true // врёт: true даже для null
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s: String? = null
|
||||
> if (s.lies()) {
|
||||
> try {
|
||||
> println(s.length) // компилятор уверен, что s != null
|
||||
> } catch (e: NullPointerException) {
|
||||
> println("NPE в non-null коде из-за неверного контракта")
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Контракт нельзя объявить в методе класса** - только в функции верхнего уровня. Это ограничение текущей реализации.
|
||||
>
|
||||
> **Контракты экспериментальны** - `@OptIn(ExperimentalContracts::class)` обязателен, API может измениться.
|
||||
>
|
||||
> **`TODO()` компилируется и попадает в релиз.** Он возвращает `Nothing`, поэтому ошибок сборки не вызывает - только падение в рантайме. Перед релизом стоит проверять: `grep -r 'TODO(' src/`.
|
||||
>
|
||||
> **Проверки в `init` не спасают `data class` от `copy`** в версиях до Kotlin 2.1: `copy()` вызывает конструктор, то есть `init` выполнится, но саму валидацию обойти через `copy` нельзя - а вот приватный конструктор обойти можно. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
>
|
||||
> **`require` внутри цикла по большой коллекции стоит дорого** не из-за проверки, а из-за того, что при нарушении создаётся исключение со стектрейсом. Для валидации списков собирайте список ошибок, а не бросайте на первой.
|
||||
|
||||
Дополнительно: [Система исключений](Система%20исключений.md)
|
||||
Далее: [Коллекции](../Коллекции/Коллекции.md)
|
||||
@@ -1,370 +0,0 @@
|
||||
---
|
||||
title: Система исключений
|
||||
description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка
|
||||
tags:
|
||||
- kotlin
|
||||
- exceptions
|
||||
---
|
||||
|
||||
Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их.
|
||||
|
||||
```kotlin
|
||||
fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен
|
||||
|
||||
fun main() {
|
||||
println(parse("42")) // 42
|
||||
try {
|
||||
parse("abc")
|
||||
} catch (e: NumberFormatException) {
|
||||
println("не число: ${e.message}") // не число: For input string: "abc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Иерархия
|
||||
```mermaid
|
||||
graph TD
|
||||
T["Throwable"] --> E["Exception"]
|
||||
T --> R["Error"]
|
||||
E --> RE["RuntimeException"]
|
||||
E --> IOE["IOException"]
|
||||
RE --> NPE["NullPointerException"]
|
||||
RE --> IAE["IllegalArgumentException"]
|
||||
RE --> ISE["IllegalStateException"]
|
||||
RE --> CCE["ClassCastException"]
|
||||
R --> OOM["OutOfMemoryError"]
|
||||
R --> SOE["StackOverflowError"]
|
||||
```
|
||||
Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые.
|
||||
|
||||
| Класс | Смысл | Перехватывать |
|
||||
|---|---|---|
|
||||
| `Error` | сбой среды: нет памяти, переполнен стек | нет |
|
||||
| `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код |
|
||||
| прочие `Exception` | внешний сбой: сеть, файл, БД | да |
|
||||
|
||||
## try как выражение
|
||||
`try` возвращает значение - это важное отличие от Java:
|
||||
```kotlin
|
||||
fun parseOrZero(s: String): Int = try {
|
||||
s.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
0
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parseOrZero("42")) // 42
|
||||
println(parseOrZero("abc")) // 0
|
||||
}
|
||||
```
|
||||
Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val result = try {
|
||||
"из try"
|
||||
} finally {
|
||||
"из finally" // игнорируется
|
||||
}
|
||||
println(result) // из try
|
||||
}
|
||||
```
|
||||
|
||||
## throw - тоже выражение
|
||||
Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию:
|
||||
```kotlin
|
||||
fun findUser(id: Int): String =
|
||||
if (id > 0) "пользователь $id"
|
||||
else throw IllegalArgumentException("неверный id: $id")
|
||||
|
||||
fun main() {
|
||||
println(findUser(1)) // пользователь 1
|
||||
val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто")
|
||||
println(name) // пользователь 1
|
||||
}
|
||||
```
|
||||
Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении.
|
||||
|
||||
## Перехват
|
||||
```kotlin
|
||||
fun risky(mode: Int): String = when (mode) {
|
||||
1 -> throw IllegalArgumentException("плохой аргумент")
|
||||
2 -> throw IllegalStateException("плохое состояние")
|
||||
else -> "ок"
|
||||
}
|
||||
|
||||
fun handle(mode: Int): String = try {
|
||||
risky(mode)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
"аргумент: ${e.message}"
|
||||
} catch (e: IllegalStateException) {
|
||||
"состояние: ${e.message}"
|
||||
} catch (e: Exception) {
|
||||
"прочее: ${e.message}"
|
||||
} finally {
|
||||
println("finally выполняется всегда")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle(1)) // аргумент: плохой аргумент
|
||||
println(handle(3)) // ок
|
||||
}
|
||||
```
|
||||
Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми.
|
||||
|
||||
Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты:
|
||||
```kotlin
|
||||
fun main() {
|
||||
try {
|
||||
throw IllegalStateException("сбой")
|
||||
} catch (e: RuntimeException) { // общий супертип
|
||||
println("перехвачено: ${e::class.simpleName}")
|
||||
}
|
||||
|
||||
try {
|
||||
throw NumberFormatException("плохое число")
|
||||
} catch (e: Exception) {
|
||||
when (e) { // или when по типу
|
||||
is NumberFormatException, is IllegalStateException ->
|
||||
println("ожидаемая ошибка: ${e.message}")
|
||||
else -> throw e // остальное пробрасываем
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Свои исключения
|
||||
```kotlin
|
||||
class InsufficientFundsException(
|
||||
val required: Long,
|
||||
val available: Long,
|
||||
) : Exception("нужно $required, доступно $available")
|
||||
|
||||
class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
fun withdraw(balance: Long, amount: Long): Long {
|
||||
if (amount > balance) throw InsufficientFundsException(amount, balance)
|
||||
return balance - amount
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(withdraw(100, 30)) // 70
|
||||
try {
|
||||
withdraw(100, 500)
|
||||
} catch (e: InsufficientFundsException) {
|
||||
println("не хватает ${e.required - e.available}") // не хватает 400
|
||||
}
|
||||
}
|
||||
```
|
||||
Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`.
|
||||
|
||||
## Цепочка причин
|
||||
```kotlin
|
||||
class RepositoryException(message: String, cause: Throwable) : Exception(message, cause)
|
||||
|
||||
fun loadUser(id: String): String = try {
|
||||
"пользователь ${id.toInt()}"
|
||||
} catch (e: NumberFormatException) {
|
||||
throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину
|
||||
}
|
||||
|
||||
fun main() {
|
||||
try {
|
||||
loadUser("abc")
|
||||
} catch (e: RepositoryException) {
|
||||
println(e.message) // не удалось загрузить 'abc'
|
||||
println("причина: ${e.cause?.message}") // причина: For input string: "abc"
|
||||
}
|
||||
}
|
||||
```
|
||||
Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя.
|
||||
|
||||
## use - автоматическое закрытие
|
||||
Аналог try-with-resources:
|
||||
```kotlin
|
||||
import java.io.StringReader
|
||||
|
||||
fun main() {
|
||||
val content = StringReader("данные").use { reader ->
|
||||
reader.readText() // close() вызовется автоматически
|
||||
}
|
||||
println(content) // данные
|
||||
}
|
||||
```
|
||||
`use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно.
|
||||
|
||||
```kotlin
|
||||
class Resource(val name: String) : AutoCloseable {
|
||||
fun work() = "работаю с $name"
|
||||
override fun close() = println("$name закрыт")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val result = Resource("файл").use { it.work() }
|
||||
println(result)
|
||||
// файл закрыт
|
||||
// работаю с файл
|
||||
|
||||
try {
|
||||
Resource("другой").use { throw IllegalStateException("сбой") }
|
||||
} catch (e: IllegalStateException) {
|
||||
println("ресурс всё равно закрыт: ${e.message}")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @Throws для Java
|
||||
Kotlin не объявляет исключения, но Java-клиенту это может быть нужно:
|
||||
```kotlin
|
||||
import java.io.IOException
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readFile(path: String): String {
|
||||
if (path.isEmpty()) throw IOException("пустой путь")
|
||||
return "содержимое $path"
|
||||
}
|
||||
|
||||
fun main() { println(readFile("a.txt")) } // содержимое a.txt
|
||||
```
|
||||
Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
## Чего не делать
|
||||
```kotlin
|
||||
fun main() {
|
||||
// проглатывание: ошибка исчезает
|
||||
try {
|
||||
error("сбой")
|
||||
} catch (e: Exception) {
|
||||
// ничего — худшая практика
|
||||
}
|
||||
|
||||
// перехват Throwable: ловит OutOfMemoryError и StackOverflowError
|
||||
try {
|
||||
error("сбой")
|
||||
} catch (e: Exception) { // не Throwable
|
||||
println("перехвачено корректно")
|
||||
}
|
||||
|
||||
// исключение как управление потоком
|
||||
val items = listOf(1, 2, 3)
|
||||
println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch
|
||||
}
|
||||
```
|
||||
Правила:
|
||||
- не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше
|
||||
- не перехватывать `Throwable` и `Error`
|
||||
- не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы
|
||||
- всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя
|
||||
|
||||
Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md)
|
||||
|
||||
## Исключения в корутинах
|
||||
Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя:
|
||||
```kotlin
|
||||
// Требует kotlinx-coroutines-core.
|
||||
// suspend fun work() {
|
||||
// try {
|
||||
// delay(1000)
|
||||
// } catch (e: CancellationException) {
|
||||
// throw e // обязательно пробросить!
|
||||
// } catch (e: Exception) {
|
||||
// println("настоящая ошибка")
|
||||
// }
|
||||
// }
|
||||
```
|
||||
`catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | checked exceptions | **нет**, все непроверяемые |
|
||||
> | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов |
|
||||
> | `try` - инструкция | `try` - выражение |
|
||||
> | `catch (A \| B e)` | нет аналога, нужен `when` |
|
||||
> | try-with-resources | `use { }` |
|
||||
> | `throw` - инструкция | `throw` - выражение типа `Nothing` |
|
||||
>
|
||||
> **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin:
|
||||
>
|
||||
> 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями.
|
||||
> 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки.
|
||||
> 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте.
|
||||
>
|
||||
> Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает.
|
||||
>
|
||||
> **`try` как выражение убирает `var`:**
|
||||
> ```java
|
||||
> int value; // Java: нужна изменяемая переменная
|
||||
> try { value = Integer.parseInt(s); }
|
||||
> catch (NumberFormatException e) { value = 0; }
|
||||
> ```
|
||||
> ```kotlin
|
||||
> fun parseOrZero(s: String): Int =
|
||||
> try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var
|
||||
>
|
||||
> fun main() { println(parseOrZero("abc")) } // 0
|
||||
> ```
|
||||
>
|
||||
> **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc:
|
||||
> ```kotlin
|
||||
> /**
|
||||
> * @throws IllegalArgumentException если id не положительный
|
||||
> */
|
||||
> fun load(id: Int): String {
|
||||
> require(id > 0) { "id должен быть положительным" }
|
||||
> return "данные $id"
|
||||
> }
|
||||
>
|
||||
> fun main() { println(load(1)) } // данные 1
|
||||
> ```
|
||||
>
|
||||
> **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается.
|
||||
>
|
||||
> **`return` в `finally` проглатывает исключение:**
|
||||
> ```kotlin
|
||||
> fun bad(): String {
|
||||
> try {
|
||||
> throw IllegalStateException("потеряется")
|
||||
> } finally {
|
||||
> return "из finally" // исключение исчезло!
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { println(bad()) } // из finally
|
||||
> ```
|
||||
> Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя.
|
||||
>
|
||||
> **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку».
|
||||
>
|
||||
> **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать.
|
||||
>
|
||||
> **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md)
|
||||
>
|
||||
> **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println("abc".toIntOrNull() ?: 0) // без исключения
|
||||
> // println("abc".toInt()) // исключение + стектрейс
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val results = mutableListOf<Int>()
|
||||
> try {
|
||||
> listOf("1", "abc", "3").forEach { results.add(it.toInt()) }
|
||||
> } catch (e: NumberFormatException) {
|
||||
> println("прервано на второй строке, собрано: $results") // [1]
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`.
|
||||
>
|
||||
> **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот.
|
||||
|
||||
Далее: [Result и runCatching](Result%20и%20runCatching.md)
|
||||
@@ -1,365 +0,0 @@
|
||||
---
|
||||
title: List, Set, Map
|
||||
description: Три семейства коллекций, их гарантии, стоимость операций и выбор между ними
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
---
|
||||
|
||||
| | `List` | `Set` | `Map` |
|
||||
|---|---|---|---|
|
||||
| порядок | по индексу | вставки (у `LinkedHashSet`) | вставки (у `LinkedHashMap`) |
|
||||
| дубликаты | разрешены | запрещены | ключи уникальны |
|
||||
| доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) |
|
||||
| поиск элемента | O(n) | O(1) | O(1) по ключу |
|
||||
| основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» |
|
||||
|
||||
## List
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf("a", "b", "c", "b")
|
||||
|
||||
println(list[1]) // b — доступ по индексу
|
||||
println(list.getOrNull(10)) // null
|
||||
println(list.getOrElse(10) { "?" }) // ?
|
||||
println(list.indexOf("b")) // 1 — первое вхождение
|
||||
println(list.lastIndexOf("b")) // 3
|
||||
println(list.subList(1, 3)) // [b, c]
|
||||
println(list.size) // 4 — дубликат сохранён
|
||||
}
|
||||
```
|
||||
|
||||
Доступ к краям и срезы:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4, 5)
|
||||
|
||||
println("${list.first()} ${list.last()}") // 1 5
|
||||
println(list.firstOrNull { it > 3 }) // 4
|
||||
println(list.take(2)) // [1, 2]
|
||||
println(list.drop(2)) // [3, 4, 5]
|
||||
println(list.takeLast(2)) // [4, 5]
|
||||
println(list.slice(1..3)) // [2, 3, 4]
|
||||
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
|
||||
println(list.windowed(2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
|
||||
println(list.reversed()) // [5, 4, 3, 2, 1]
|
||||
}
|
||||
```
|
||||
|
||||
Изменение через `MutableList`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = mutableListOf(1, 2, 3)
|
||||
|
||||
list.add(4)
|
||||
list.add(0, 0) // по индексу
|
||||
list[1] = 10
|
||||
list.removeAt(0)
|
||||
list.remove(4) // по значению
|
||||
println(list) // [10, 2, 3]
|
||||
|
||||
list += 5 // оператор += работает как add
|
||||
list -= 2
|
||||
println(list) // [10, 3, 5]
|
||||
|
||||
list.sortDescending() // сортировка на месте
|
||||
println(list) // [10, 5, 3]
|
||||
}
|
||||
```
|
||||
|
||||
## Set
|
||||
```kotlin
|
||||
fun main() {
|
||||
val set = setOf(1, 2, 3)
|
||||
|
||||
println(2 in set) // true — O(1)
|
||||
println(set.contains(5)) // false
|
||||
println(setOf(1, 2, 2, 3).size) // 3 — дубликат отброшен
|
||||
}
|
||||
```
|
||||
|
||||
Операции над множествами:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = setOf(1, 2, 3)
|
||||
val b = setOf(3, 4, 5)
|
||||
|
||||
println(a union b) // [1, 2, 3, 4, 5]
|
||||
println(a intersect b) // [3]
|
||||
println(a subtract b) // [1, 2]
|
||||
println(a + b) // [1, 2, 3, 4, 5] — как union
|
||||
println(a - b) // [1, 2]
|
||||
}
|
||||
```
|
||||
Эти же функции работают на `List`, но результат всегда `Set`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3]
|
||||
println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List
|
||||
}
|
||||
```
|
||||
|
||||
### Три реализации Set
|
||||
```kotlin
|
||||
fun main() {
|
||||
val linked = linkedSetOf(3, 1, 2) // порядок вставки
|
||||
val hash = hashSetOf(3, 1, 2) // порядок не определён
|
||||
val sorted = sortedSetOf(3, 1, 2) // отсортирован
|
||||
|
||||
println(linked) // [3, 1, 2]
|
||||
println(sorted) // [1, 2, 3]
|
||||
println(hash.size) // 3
|
||||
}
|
||||
```
|
||||
|
||||
| Реализация | Порядок | Поиск | Когда |
|
||||
|---|---|---|---|
|
||||
| `LinkedHashSet` (`setOf`) | вставки | O(1) | по умолчанию |
|
||||
| `HashSet` (`hashSetOf`) | не определён | O(1) | экономия памяти, порядок не нужен |
|
||||
| `TreeSet` (`sortedSetOf`) | сортировки | O(log n) | нужен отсортированный обход |
|
||||
|
||||
Элементы `Set` обязаны корректно реализовывать `equals` и `hashCode` - иначе множество работает неправильно. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
|
||||
## Map
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
|
||||
println(map["a"]) // 1
|
||||
println(map["z"]) // null
|
||||
println(map.getOrDefault("z", 0)) // 0
|
||||
println(map.getOrElse("z") { -1 }) // -1
|
||||
println(map.getValue("a")) // 1, бросает при отсутствии
|
||||
println("a" in map) // true — проверка ключа
|
||||
println(map.containsValue(2)) // true
|
||||
println("${map.keys} ${map.values}") // [a, b] [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
`to` - инфиксная функция, создающая `Pair`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val pair: Pair<String, Int> = "a" to 1
|
||||
println("${pair.first} ${pair.second}") // a 1
|
||||
|
||||
val (k, v) = pair // деструктуризация
|
||||
println("$k=$v") // a=1
|
||||
|
||||
println(listOf("a" to 1, "b" to 2).toMap()) // {a=1, b=2}
|
||||
}
|
||||
```
|
||||
|
||||
Изменение через `MutableMap`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mutableMapOf("a" to 1)
|
||||
|
||||
map["b"] = 2
|
||||
map.put("c", 3)
|
||||
map.putAll(mapOf("d" to 4))
|
||||
map.remove("a")
|
||||
println(map) // {b=2, c=3, d=4}
|
||||
|
||||
println(map.getOrPut("e") { 5 }) // 5 — вычислит и положит
|
||||
println(map.getOrPut("e") { 99 }) // 5 — уже есть, лямбда не вызвана
|
||||
map.merge("b", 10) { old, new -> old + new } // метод JDK
|
||||
println(map) // {b=12, c=3, d=4, e=5}
|
||||
}
|
||||
```
|
||||
`getOrPut` - основной инструмент кеширования: одна операция вместо проверки и вставки.
|
||||
|
||||
Обход:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
|
||||
for ((key, value) in map) print("$key=$value ") // a=1 b=2
|
||||
println()
|
||||
map.forEach { (key, value) -> print("$key$value ") } // a1 b2
|
||||
println()
|
||||
println(map.entries.first().key) // a
|
||||
}
|
||||
```
|
||||
Деструктуризация в `for` и в лямбде - главная причина, почему обход `Map` в Kotlin короче, чем в Java.
|
||||
|
||||
Преобразования:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
|
||||
|
||||
println(map.filter { it.value > 1 }) // {b=2, c=3}
|
||||
println(map.filterKeys { it != "a" }) // {b=2, c=3}
|
||||
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
|
||||
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
|
||||
println(map.map { "${it.key}${it.value}" }) // [a1, b2, c3] — это List
|
||||
println(map.toList()) // [(a, 1), (b, 2), (c, 3)]
|
||||
println(map.entries.sortedByDescending { it.value }.first().key) // c
|
||||
}
|
||||
```
|
||||
|
||||
### Три реализации Map
|
||||
| Реализация | Порядок | Доступ | Когда |
|
||||
|---|---|---|---|
|
||||
| `LinkedHashMap` (`mapOf`) | вставки | O(1) | по умолчанию |
|
||||
| `HashMap` (`hashMapOf`) | не определён | O(1) | экономия памяти |
|
||||
| `TreeMap` (`sortedMapOf`) | сортировки ключей | O(log n) | диапазонные запросы |
|
||||
|
||||
## Выбор структуры
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужна коллекция"] --> B{"Пары ключ-значение?"}
|
||||
B -->|да| M["Map"]
|
||||
B -->|нет| C{"Дубликаты важны<br/>или нужен индекс?"}
|
||||
C -->|да| L["List"]
|
||||
C -->|нет| D{"Частый поиск<br/>элемента?"}
|
||||
D -->|да| S["Set"]
|
||||
D -->|нет| L
|
||||
```
|
||||
|
||||
Практический критерий: если в коде есть `list.contains(x)` внутри цикла - это `Set`. Такая замена превращает O(n²) в O(n):
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ids = List(1000) { it }
|
||||
val allowedList = List(1000) { it * 2 }
|
||||
val allowedSet = allowedList.toSet()
|
||||
|
||||
println(ids.count { it in allowedList }) // O(n*m) — 1000*1000 сравнений
|
||||
println(ids.count { it in allowedSet }) // O(n) — то же 500, но быстро
|
||||
}
|
||||
```
|
||||
|
||||
## Специальные коллекции
|
||||
```kotlin
|
||||
fun main() {
|
||||
val deque = ArrayDeque(listOf(2, 3))
|
||||
deque.addFirst(1)
|
||||
deque.addLast(4)
|
||||
println("$deque ${deque.removeFirst()} ${deque.removeLast()}") // [1, 2, 3, 4] 1 4
|
||||
|
||||
val grouped = listOf("яблоко", "арбуз", "банан").groupBy { it.first() }
|
||||
println(grouped) // {я=[яблоко], а=[арбуз], б=[банан]}
|
||||
|
||||
val assoc = listOf("a", "bb", "ccc").associateWith { it.length }
|
||||
println(assoc) // {a=1, bb=2, ccc=3}
|
||||
}
|
||||
```
|
||||
`ArrayDeque` в Kotlin - своя реализация (не `java.util.ArrayDeque`), работает как стек и как очередь. Подробнее об `groupBy` и `associate`: [Операции](Операции.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `map.get(k)` возвращает `V` | `map[k]` возвращает `V?` |
|
||||
> | `map.put(k, v)` | `map[k] = v` |
|
||||
> | `new AbstractMap.SimpleEntry<>(k, v)` | `k to v` |
|
||||
> | `for (Map.Entry<K,V> e : map.entrySet())` | `for ((k, v) in map)` |
|
||||
> | `map.computeIfAbsent(k, f)` | `map.getOrPut(k) { }` |
|
||||
> | `Collections.unmodifiableList` | тип `List` |
|
||||
> | `set.retainAll(other)` | `a intersect b` |
|
||||
> | `list.get(i)` | `list[i]` |
|
||||
>
|
||||
> **`map[k]` возвращает nullable - и это правильно.** В Java `map.get(k)` имеет тип `V`, поэтому `int x = map.get("нет")` компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val map = mapOf("a" to 1)
|
||||
> val x: Int = map["z"] ?: 0 // компилятор требует обработать null
|
||||
> println(x) // 0
|
||||
> }
|
||||
> ```
|
||||
> Обратная сторона: `map[k]!!` появляется в коде часто. Когда ключ гарантированно есть, честнее `getValue(k)` - он бросит `NoSuchElementException` с внятным сообщением.
|
||||
>
|
||||
> **Деструктуризация `Map.Entry`.** Java требует `e.getKey()`/`e.getValue()`; в Kotlin `for ((k, v) in map)` работает потому, что для `Map.Entry` объявлены функции-расширения `component1` и `component2`. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
|
||||
>
|
||||
> **Операции над множествами как инфиксные функции.** В Java `union` - это `addAll` на копии, `intersect` - `retainAll`, то есть операции мутирующие и требуют ручного копирования. В Kotlin `a union b` возвращает новое множество и читается как математическая запись.
|
||||
>
|
||||
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`map[k]` не различает «нет ключа» и «значение null»:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val map = mapOf("a" to null, "b" to 1)
|
||||
> println(map["a"]) // null
|
||||
> println(map["z"]) // null — то же самое!
|
||||
> println("a" in map) // true
|
||||
> println("z" in map) // false — вот так различать
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`getValue` бросает, `get` - нет.** Это легко перепутать:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val map = mapOf("a" to 1)
|
||||
> println(map["z"]) // null
|
||||
> try {
|
||||
> map.getValue("z")
|
||||
> } catch (e: NoSuchElementException) {
|
||||
> println("getValue бросает") // getValue бросает
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Изменяемый элемент в `Set` или ключ в `Map` ломает коллекцию:**
|
||||
> ```kotlin
|
||||
> data class Key(var id: Int)
|
||||
>
|
||||
> fun main() {
|
||||
> val key = Key(1)
|
||||
> val set = mutableSetOf(key)
|
||||
> println(key in set) // true
|
||||
> key.id = 2 // hashCode изменился!
|
||||
> println(key in set) // false — элемент потерян
|
||||
> println(set.size) // 1 — но он всё ещё внутри
|
||||
> }
|
||||
> ```
|
||||
> Ключи и элементы множеств обязаны быть неизменяемыми. `data class` с `var` в этой роли - ошибка.
|
||||
>
|
||||
> **`subList` - это вид на исходный список, а не копия:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = mutableListOf(1, 2, 3, 4)
|
||||
> val sub = list.subList(1, 3)
|
||||
> list[1] = 99
|
||||
> println(sub) // [99, 3] — изменился
|
||||
> list.add(5)
|
||||
> try {
|
||||
> println(sub)
|
||||
> } catch (e: ConcurrentModificationException) {
|
||||
> println("структурное изменение списка сломало subList")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Для независимой копии нужен `slice` или `toList()`.
|
||||
>
|
||||
> **`remove` на `MutableList<Int>` неоднозначен:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = mutableListOf(10, 20, 30)
|
||||
> list.removeAt(1) // по индексу
|
||||
> println(list) // [10, 30]
|
||||
> list.remove(10) // по значению
|
||||
> println(list) // [30]
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin, в отличие от Java, перегрузки по индексу у `remove` нет - только `removeAt`. Это как раз устраняет классическую Java-ловушку `list.remove(1)`.
|
||||
>
|
||||
> **`list.contains(x)` в цикле - это O(n²).** Самая частая проблема производительности с коллекциями. Если проверка повторяется, предварительно постройте `Set`.
|
||||
>
|
||||
> **`hashSetOf` и `hashMapOf` не сохраняют порядок** - вывод может отличаться между запусками и версиями JDK. Тест, завязанный на порядок, будет нестабильным.
|
||||
>
|
||||
> **`sortedSetOf` требует `Comparable` или компаратор** и бросит `ClassCastException` при вставке несравнимого элемента - в рантайме, а не при компиляции.
|
||||
>
|
||||
> **`+=` на `List` работает, но создаёт новую коллекцию:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> var list = listOf(1, 2) // var!
|
||||
> list += 3 // новый список, не add
|
||||
> println(list) // [1, 2, 3]
|
||||
>
|
||||
> val mutable = mutableListOf(1, 2)
|
||||
> mutable += 3 // здесь настоящий add
|
||||
> println(mutable) // [1, 2, 3]
|
||||
> }
|
||||
> ```
|
||||
> На `val list: List<Int>` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований.
|
||||
|
||||
Далее: [Изменяемость](Изменяемость.md)
|
||||
@@ -1,368 +0,0 @@
|
||||
---
|
||||
title: Sequence
|
||||
description: Ленивые вычисления, порядок обхода элементов и когда Sequence выигрывает у List
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
---
|
||||
|
||||
`Sequence<T>` - ленивый аналог коллекции. Операции не выполняются до запроса результата, и элементы проходят всю цепочку по одному.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4, 5)
|
||||
|
||||
println("-- List:")
|
||||
val eager = list
|
||||
.map { println(" map $it"); it * 2 }
|
||||
.filter { println(" filter $it"); it > 4 }
|
||||
.first()
|
||||
println(" результат $eager")
|
||||
|
||||
println("-- Sequence:")
|
||||
val lazy = list.asSequence()
|
||||
.map { println(" map $it"); it * 2 }
|
||||
.filter { println(" filter $it"); it > 4 }
|
||||
.first()
|
||||
println(" результат $lazy")
|
||||
}
|
||||
```
|
||||
`List` выполнит 5 `map` и 5 `filter` - всего 10 операций, создав два промежуточных списка. `Sequence` выполнит 3 `map` и 3 `filter` и остановится, как только найдёт первый подходящий элемент.
|
||||
|
||||
## Два порядка обхода
|
||||
Это ключ к пониманию `Sequence`:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph List["List: по операциям"]
|
||||
L1["map всех 5"] --> L2["filter всех 5"] --> L3["first"]
|
||||
end
|
||||
subgraph Seq["Sequence: по элементам"]
|
||||
S1["1: map, filter"] --> S2["2: map, filter"] --> S3["3: map, filter → стоп"]
|
||||
end
|
||||
```
|
||||
|
||||
| | `List` | `Sequence` |
|
||||
|---|---|---|
|
||||
| порядок | операция за операцией | элемент за элементом |
|
||||
| промежуточные коллекции | на каждом шаге | нет |
|
||||
| вычисление | сразу | при терминальной операции |
|
||||
| можно ли прервать | нет | да |
|
||||
| бесконечные данные | нет | да |
|
||||
|
||||
## Создание
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).asSequence().toList()) // [1, 2, 3]
|
||||
println(sequenceOf(1, 2, 3).toList()) // [1, 2, 3]
|
||||
println(emptySequence<Int>().toList()) // []
|
||||
|
||||
println(generateSequence(1) { it * 2 }.take(5).toList()) // [1, 2, 4, 8, 16]
|
||||
println(generateSequence(1) { if (it < 5) it + 1 else null }.toList()) // [1, 2, 3, 4, 5]
|
||||
|
||||
val built = sequence {
|
||||
yield(1)
|
||||
yieldAll(listOf(2, 3))
|
||||
yield(4)
|
||||
}
|
||||
println(built.toList()) // [1, 2, 3, 4]
|
||||
}
|
||||
```
|
||||
`generateSequence` с `null` в лямбде - способ задать конец. `sequence { }` - билдер с `yield`, внутри него можно писать любую логику, включая циклы.
|
||||
|
||||
## Промежуточные и терминальные операции
|
||||
```kotlin
|
||||
fun main() {
|
||||
val seq = sequenceOf(1, 2, 3)
|
||||
|
||||
// промежуточные: возвращают Sequence, ничего не считают
|
||||
val chain = seq.map { it * 2 }.filter { it > 2 }.take(2)
|
||||
println("цепочка построена, вычислений не было")
|
||||
|
||||
// терминальные: запускают вычисление
|
||||
println(chain.toList()) // [4, 6]
|
||||
println(seq.sum()) // 6
|
||||
println(seq.count()) // 3
|
||||
println(seq.first()) // 1
|
||||
println(seq.any { it > 2 }) // true
|
||||
}
|
||||
```
|
||||
|
||||
| Промежуточные | Терминальные |
|
||||
|---|---|
|
||||
| `map`, `filter`, `flatMap` | `toList`, `toSet`, `toMap` |
|
||||
| `take`, `drop`, `distinct` | `first`, `last`, `single` |
|
||||
| `sorted`, `chunked`, `windowed` | `sum`, `count`, `average`, `max` |
|
||||
| `onEach`, `mapNotNull` | `any`, `all`, `none`, `forEach` |
|
||||
| `zip`, `plus` | `fold`, `reduce`, `joinToString` |
|
||||
|
||||
## Бесконечные последовательности
|
||||
То, чего `List` не умеет в принципе:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val naturals = generateSequence(1) { it + 1 }
|
||||
|
||||
println(naturals.take(5).toList()) // [1, 2, 3, 4, 5]
|
||||
println(naturals.filter { it % 7 == 0 }.first()) // 7
|
||||
println(naturals.map { it * it }.takeWhile { it < 50 }.toList()) // [1, 4, 9, 16, 25, 36, 49]
|
||||
|
||||
val fib = generateSequence(0 to 1) { (a, b) -> b to (a + b) }.map { it.first }
|
||||
println(fib.take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
||||
}
|
||||
```
|
||||
Важно: терминальная операция без ограничения на бесконечной последовательности зависнет. `naturals.toList()` не завершится никогда.
|
||||
|
||||
## Sequence в цепочке с ранним выходом
|
||||
```kotlin
|
||||
data class Record3(val id: Int, val active: Boolean, val score: Int)
|
||||
|
||||
fun main() {
|
||||
val records = List(1_000_000) { Record3(it, it % 100 == 0, it % 7) }
|
||||
|
||||
val found = records.asSequence()
|
||||
.filter { it.active }
|
||||
.map { it.score * 2 }
|
||||
.firstOrNull { it > 10 }
|
||||
println(found) // 12
|
||||
|
||||
val top3 = records.asSequence()
|
||||
.filter { it.active }
|
||||
.take(3)
|
||||
.map { it.id }
|
||||
.toList()
|
||||
println(top3) // [0, 100, 200]
|
||||
}
|
||||
```
|
||||
Здесь `Sequence` обрабатывает несколько сотен элементов вместо миллиона. На `List` каждый `filter`/`map` прошёл бы весь миллион и создал два списка.
|
||||
|
||||
## Когда Sequence не выигрывает
|
||||
```kotlin
|
||||
fun main() {
|
||||
val small = listOf(1, 2, 3, 4, 5)
|
||||
|
||||
// Sequence здесь только добавляет накладные расходы
|
||||
println(small.map { it * 2 }.sum()) // 30
|
||||
println(small.asSequence().map { it * 2 }.sum()) // 30, но медленнее
|
||||
}
|
||||
```
|
||||
`Sequence` создаёт объект-обёртку на каждую операцию и вызывает лямбды через интерфейс итератора. На маленьких коллекциях и коротких цепочках это дороже, чем прямой проход.
|
||||
|
||||
Практическое правило:
|
||||
|
||||
| Условие | Выбор |
|
||||
|---|---|
|
||||
| меньше ~1000 элементов, 1-2 операции | `List` |
|
||||
| много элементов и 3+ операций | `Sequence` |
|
||||
| есть `first`, `find`, `take`, `any` после цепочки | `Sequence` |
|
||||
| источник бесконечный или потоковый (файл, БД) | `Sequence` |
|
||||
| нужен результат целиком, операций мало | `List` |
|
||||
| нужен индексный доступ к результату | `List` |
|
||||
|
||||
## Операции, которые ломают ленивость
|
||||
Некоторые операции обязаны прочитать всё, прежде чем выдать первый элемент:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val seq = sequenceOf(3, 1, 2)
|
||||
.onEach { println("читаю $it") }
|
||||
.sorted() // нужны все элементы
|
||||
println("до first ничего не прочитано")
|
||||
println(seq.first()) // здесь прочитаются все три
|
||||
}
|
||||
```
|
||||
Такие операции - `sorted`, `sortedBy`, `distinct` (частично), `groupBy`, `chunked`. Ставьте их как можно позже в цепочке, а `filter` - как можно раньше.
|
||||
|
||||
## Sequence одноразов
|
||||
```kotlin
|
||||
fun main() {
|
||||
val seq = listOf(1, 2, 3).asSequence().map { it * 2 }
|
||||
println(seq.toList()) // [2, 4, 6]
|
||||
println(seq.toList()) // [2, 4, 6] — здесь повезло: источник List
|
||||
|
||||
val once = sequence { yield(1); yield(2) }
|
||||
println(once.toList()) // [1, 2]
|
||||
println(once.toList()) // [1, 2] — тоже работает, лямбда вызвана заново
|
||||
|
||||
val fromIterator = listOf(1, 2, 3).iterator().asSequence()
|
||||
println(fromIterator.toList()) // [1, 2, 3]
|
||||
try {
|
||||
fromIterator.toList()
|
||||
} catch (e: IllegalStateException) {
|
||||
println(e.message) // This sequence can be consumed only once.
|
||||
}
|
||||
}
|
||||
```
|
||||
Последовательность из итератора обходится один раз: `Iterator.asSequence()` оборачивает результат в `constrainOnce()`, который фиксирует это ограничение и бросает `IllegalStateException` при повторной попытке. Тот же механизм доступен явно:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val seq = sequenceOf(1, 2).constrainOnce()
|
||||
println(seq.toList()) // [1, 2]
|
||||
try {
|
||||
seq.toList()
|
||||
} catch (e: IllegalStateException) {
|
||||
println("повторный обход запрещён")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Чтение файла
|
||||
Главный практический случай: данные, которые не влезают в память.
|
||||
```kotlin
|
||||
import java.io.File
|
||||
|
||||
fun main() {
|
||||
val tmp = File.createTempFile("demo", ".txt")
|
||||
tmp.writeText((1..100).joinToString("\n"))
|
||||
|
||||
val sum = tmp.useLines { lines -> // Sequence<String>
|
||||
lines.map { it.toInt() }.filter { it % 2 == 0 }.sum()
|
||||
}
|
||||
println(sum) // 2550
|
||||
|
||||
val firstBig = tmp.useLines { lines -> lines.first { it.toInt() > 50 } }
|
||||
println(firstBig) // 51 — файл прочитан до 51-й строки, не целиком
|
||||
|
||||
tmp.delete()
|
||||
}
|
||||
```
|
||||
`useLines` даёт `Sequence<String>` и закрывает файл после работы. Альтернатива `readLines()` загрузила бы весь файл в `List<String>`.
|
||||
|
||||
## Sequence и Flow
|
||||
`Sequence` - ленивый, но **синхронный**: получение следующего элемента блокирует поток. Для асинхронных источников нужен `Flow`:
|
||||
|
||||
| | `Sequence` | `Flow` |
|
||||
|---|---|---|
|
||||
| вычисление | лениво | лениво |
|
||||
| блокировка | да | нет, `suspend` |
|
||||
| внутри можно `delay` | нет | да |
|
||||
| источник | память, файл, генератор | сеть, БД, события |
|
||||
|
||||
Подробнее: [Flow](../Корутины/Flow.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java Stream | Kotlin Sequence |
|
||||
> |---|---|
|
||||
> | `list.stream()` | `list.asSequence()` |
|
||||
> | `Stream.of(1, 2)` | `sequenceOf(1, 2)` |
|
||||
> | `Stream.iterate(1, f)` | `generateSequence(1, f)` |
|
||||
> | `collect(toList())` | `toList()` |
|
||||
> | `.parallel()` | **нет аналога** |
|
||||
> | `IntStream`, `LongStream` | нет, только `Sequence<T>` |
|
||||
> | `Files.lines(path)` | `file.useLines { }` |
|
||||
> | нет генератора с `yield` | `sequence { yield(x) }` |
|
||||
>
|
||||
> **`Sequence` и `Stream` - почти одно и то же.** Ленивость, промежуточные и терминальные операции, короткое замыкание на `findFirst`/`first` - всё совпадает. Поэтому опыт работы со стримами переносится напрямую, и наоборот: если в Java вы написали бы `.stream()`, в Kotlin нужен `.asSequence()`.
|
||||
>
|
||||
> **Главное отличие - нет параллелизма.** `Stream.parallel()` распределяет работу по `ForkJoinPool`; у `Sequence` такого нет вообще. Причина в том, что для конкурентности в Kotlin предусмотрены корутины, а не параллельные стримы. Если нужна параллельная обработка - либо используйте Java-стримы напрямую (они доступны), либо корутины:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> // Java Stream доступен из Kotlin
|
||||
> val sum = listOf(1, 2, 3).stream().parallel().mapToInt { it * 2 }.sum()
|
||||
> println(sum) // 12
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Нет специализаций для примитивов.** В Java `IntStream` избегает упаковки: `list.stream().mapToInt(x -> x).sum()` работает с `int`. `Sequence<Int>` упаковывает каждый элемент в `Integer`. На числовых расчётах по большим данным Java-стримы могут оказаться быстрее.
|
||||
>
|
||||
> **`sequence { yield(x) }` - генератор, которого в Java нет.** Он реализован на том же механизме приостановки, что и корутины, поэтому внутри можно писать обычные циклы и условия:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val primes = sequence {
|
||||
> var n = 2
|
||||
> while (true) {
|
||||
> if ((2 until n).none { n % it == 0 }) yield(n)
|
||||
> n++
|
||||
> }
|
||||
> }
|
||||
> println(primes.take(5).toList()) // [2, 3, 5, 7, 11]
|
||||
> }
|
||||
> ```
|
||||
> В Java это требует либо отдельного потока, либо ручной реализации `Iterator` с сохранением состояния.
|
||||
>
|
||||
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Без терминальной операции не произойдёт ничего:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> listOf(1, 2, 3).asSequence().map { println("это не выведется"); it }
|
||||
> println("конец") // только "конец"
|
||||
> }
|
||||
> ```
|
||||
> Это частая ошибка при замене `forEach` на цепочку: если забыть терминальную операцию, код просто не выполнится, и компилятор не предупредит.
|
||||
>
|
||||
> **`Sequence` на маленьких коллекциях медленнее.** Обёртки и вызовы через итератор стоят дороже прямого цикла. Для списка из 10 элементов `asSequence()` - пессимизация.
|
||||
>
|
||||
> **Терминальная операция на бесконечной последовательности зависает:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val naturals = generateSequence(1) { it + 1 }
|
||||
> println(naturals.take(3).toList()) // [1, 2, 3] — ок
|
||||
> // naturals.toList() // зависнет навсегда
|
||||
> // naturals.count() // тоже
|
||||
> // naturals.filter { it > 10 }.toList() // и это
|
||||
> }
|
||||
> ```
|
||||
> Любая цепочка из бесконечного источника обязана содержать `take`, `takeWhile`, `first` или `find`.
|
||||
>
|
||||
> **`sorted` и `groupBy` убивают ленивость** - они читают всё до первого результата. Цепочка `asSequence().sorted().first()` не быстрее, чем на `List`.
|
||||
>
|
||||
> **Последовательность из итератора обходится один раз** - второй вызов падает:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val seq = listOf(1, 2).iterator().asSequence()
|
||||
> println(seq.count()) // 2
|
||||
> try {
|
||||
> seq.count()
|
||||
> } catch (e: IllegalStateException) {
|
||||
> println("повторный обход: ${e.message}")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Исключение здесь лучше молчаливого пустого результата, но возникает оно в рантайме и только на втором обходе - в редкой ветке кода его легко не заметить. Если последовательность может понадобиться дважды, сохраните `toList()`.
|
||||
>
|
||||
> **Побочные эффекты в промежуточных операциях выполняются в неожиданном порядке:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val log = mutableListOf<String>()
|
||||
> sequenceOf(1, 2)
|
||||
> .onEach { log.add("A$it") }
|
||||
> .onEach { log.add("B$it") }
|
||||
> .toList()
|
||||
> println(log) // [A1, B1, A2, B2] — не [A1, A2, B1, B2]
|
||||
> }
|
||||
> ```
|
||||
> На `List` вывод был бы `[A1, A2, B1, B2]`. Код, зависящий от порядка побочных эффектов, при переходе на `Sequence` сломается.
|
||||
>
|
||||
> **Повторный обход пересчитывает всё заново:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> var calls = 0
|
||||
> val seq = listOf(1, 2, 3).asSequence().map { calls++; it * 2 }
|
||||
> seq.toList()
|
||||
> seq.toList()
|
||||
> println(calls) // 6 — лямбда вызвана дважды для каждого элемента
|
||||
> }
|
||||
> ```
|
||||
> Если результат нужен несколько раз, сохраните его в `List`.
|
||||
>
|
||||
> **`Sequence` синхронна.** Внутри её лямбд нельзя вызывать `suspend`-функции - для этого существует `Flow`. Попытка использовать `Sequence` для сетевых запросов приведёт к блокировке потока.
|
||||
>
|
||||
> **`useLines` закрывает файл по выходу из лямбды.** Вернуть `Sequence` наружу нельзя - она окажется на закрытом ресурсе:
|
||||
> ```kotlin
|
||||
> import java.io.File
|
||||
>
|
||||
> fun main() {
|
||||
> val tmp = File.createTempFile("leak", ".txt")
|
||||
> tmp.writeText("1\n2")
|
||||
> val leaked = tmp.useLines { it } // Sequence на закрытом файле
|
||||
> try {
|
||||
> println(leaked.toList())
|
||||
> } catch (e: java.io.IOException) {
|
||||
> println("поток закрыт: ${e.message}") // поток закрыт: Stream closed
|
||||
> }
|
||||
> tmp.delete()
|
||||
> }
|
||||
> ```
|
||||
> Внутри лямбды нужно получить готовое значение: `useLines { it.toList() }` или сразу агрегат.
|
||||
|
||||
Далее: [Коллекции под капотом](Коллекции%20под%20капотом.md)
|
||||
@@ -1,323 +0,0 @@
|
||||
---
|
||||
title: Изменяемость
|
||||
description: Read-only против immutable, защитное копирование и персистентные коллекции
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
---
|
||||
|
||||
`List` в Kotlin - **read-only**, а не immutable. Это разные вещи, и их смешение - источник самых неприятных ошибок с коллекциями.
|
||||
|
||||
| | read-only (`List`) | immutable |
|
||||
|---|---|---|
|
||||
| изменить через эту ссылку | нельзя | нельзя |
|
||||
| может измениться вообще | **да** | нет |
|
||||
| гарантия | интерфейс без мутаторов | сам объект неизменяем |
|
||||
| есть в Kotlin stdlib | да | нет (только внешняя библиотека) |
|
||||
|
||||
## Почему read-only не immutable
|
||||
```kotlin
|
||||
fun main() {
|
||||
val mutable = mutableListOf(1, 2)
|
||||
val readOnly: List<Int> = mutable // тот же объект, другой интерфейс
|
||||
|
||||
mutable.add(3)
|
||||
println(readOnly) // [1, 2, 3] — изменился под вами
|
||||
}
|
||||
```
|
||||
`MutableList` наследует `List`, поэтому любая изменяемая коллекция подходит там, где ожидается `List`. Приведение типа объект не копирует.
|
||||
|
||||
Это создаёт конкретную проблему в API:
|
||||
```kotlin
|
||||
class Repository4 {
|
||||
private val items = mutableListOf(1, 2, 3)
|
||||
|
||||
fun getItems(): List<Int> = items // утечка: вернули ссылку на изменяемое
|
||||
fun add(x: Int) { items.add(x) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = Repository4()
|
||||
val snapshot = repo.getItems()
|
||||
println(snapshot) // [1, 2, 3]
|
||||
repo.add(4)
|
||||
println(snapshot) // [1, 2, 3, 4] — «снимок» изменился
|
||||
}
|
||||
```
|
||||
Клиент получил `List` и вправе считать, что значение стабильно - например, положить его в `Map` как ключ или обойти в другом потоке. Ни то ни другое здесь не безопасно.
|
||||
|
||||
## Защитное копирование
|
||||
```kotlin
|
||||
class Repository5 {
|
||||
private val items = mutableListOf(1, 2, 3)
|
||||
|
||||
fun getItems(): List<Int> = items.toList() // копия
|
||||
fun add(x: Int) { items.add(x) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = Repository5()
|
||||
val snapshot = repo.getItems()
|
||||
repo.add(4)
|
||||
println("$snapshot / ${repo.getItems()}") // [1, 2, 3] / [1, 2, 3, 4]
|
||||
}
|
||||
```
|
||||
`toList()` создаёт новый `ArrayList`. Цена - O(n) и лишний объект; выгода - значение, на которое можно полагаться.
|
||||
|
||||
Варианты копирования:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val source = mutableListOf(1, 2, 3)
|
||||
|
||||
println(source.toList()::class.simpleName) // ArrayList
|
||||
println(source.toMutableList()::class.simpleName) // ArrayList
|
||||
println(source.toSet()::class.simpleName) // LinkedHashSet
|
||||
println(ArrayList(source)::class.simpleName) // ArrayList
|
||||
println(List(source.size) { source[it] }) // [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
Когда копировать:
|
||||
- коллекция уходит за границу класса или модуля и должна быть стабильной
|
||||
- коллекция попадает в `Map`/`Set` как ключ или элемент
|
||||
- значение читается из другого потока
|
||||
|
||||
Когда не копировать:
|
||||
- коллекция создана тут же и больше нигде не хранится
|
||||
- вызывающий - код того же класса
|
||||
- коллекция огромная, а изменения точно не будет (документируйте это)
|
||||
|
||||
## Неизменяемость на уровне содержимого
|
||||
Даже честная копия списка не защищает от изменения **элементов**:
|
||||
```kotlin
|
||||
class Item(var name: String)
|
||||
|
||||
fun main() {
|
||||
val items = listOf(Item("a"), Item("b"))
|
||||
val copy = items.toList()
|
||||
|
||||
items[0].name = "изменено"
|
||||
println(copy[0].name) // изменено — копия поверхностная
|
||||
}
|
||||
```
|
||||
`toList()` копирует только ссылки. Настоящая защита - неизменяемые элементы:
|
||||
```kotlin
|
||||
data class ItemRo(val name: String)
|
||||
|
||||
fun main() {
|
||||
val items = listOf(ItemRo("a"))
|
||||
// items[0].name = "x" // ошибка: val
|
||||
val changed = items.map { it.copy(name = "изменено") }
|
||||
println("${items[0].name} / ${changed[0].name}") // a / изменено
|
||||
}
|
||||
```
|
||||
Это общее правило: неизменяемость полезна, когда она рекурсивна. `List<T>` с изменяемым `T` даёт лишь половину гарантии.
|
||||
|
||||
## val и изменяемость
|
||||
Два независимых понятия, которые постоянно путают:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = mutableListOf(1) // ссылку сменить нельзя, содержимое — можно
|
||||
a.add(2)
|
||||
println(a) // [1, 2]
|
||||
// a = mutableListOf(3) // ошибка
|
||||
|
||||
var b = listOf(1) // ссылку сменить можно, содержимое — нельзя
|
||||
b = listOf(1, 2)
|
||||
println(b) // [1, 2]
|
||||
// b.add(3) // ошибка
|
||||
}
|
||||
```
|
||||
|
||||
| | `val` | `var` |
|
||||
|---|---|---|
|
||||
| `List` | ничего не изменить | можно заменить целиком |
|
||||
| `MutableList` | можно менять содержимое | и то и другое |
|
||||
|
||||
Самый частый правильный выбор - `val` + `List`. Подробнее: [Переменные](../Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
|
||||
|
||||
## Функциональное обновление
|
||||
Вместо изменения на месте - новое значение:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val original = listOf(1, 2, 3)
|
||||
|
||||
println(original + 4) // [1, 2, 3, 4]
|
||||
println(original - 2) // [1, 3]
|
||||
println(original.map { it * 10 }) // [10, 20, 30]
|
||||
println(original.filter { it != 2 }) // [1, 3]
|
||||
println(original.toMutableList().apply { this[0] = 99 }) // [99, 2, 3]
|
||||
println(original) // [1, 2, 3] — не тронут
|
||||
}
|
||||
```
|
||||
Для `Map` то же:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val config = mapOf("host" to "localhost", "port" to "8080")
|
||||
|
||||
println(config + ("port" to "9090")) // {host=localhost, port=9090}
|
||||
println(config - "port") // {host=localhost}
|
||||
println(config + mapOf("tls" to "on")) // {host=localhost, port=8080, tls=on}
|
||||
}
|
||||
```
|
||||
Цена - копирование на каждую операцию. Для небольших коллекций и нечастых обновлений это правильный выбор по умолчанию: такой код проще отлаживать, потому что значение не меняется за спиной.
|
||||
|
||||
## Изменяемое состояние внутри, read-only снаружи
|
||||
Рабочий шаблон для класса с коллекцией:
|
||||
```kotlin
|
||||
class Playlist {
|
||||
private val _tracks = mutableListOf<String>()
|
||||
val tracks: List<String> get() = _tracks.toList() // копия при чтении
|
||||
|
||||
fun add(track: String) { _tracks.add(track) }
|
||||
fun size() = _tracks.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val p = Playlist()
|
||||
p.add("песня 1")
|
||||
val snapshot = p.tracks
|
||||
p.add("песня 2")
|
||||
println("$snapshot / ${p.tracks}") // [песня 1] / [песня 1, песня 2]
|
||||
}
|
||||
```
|
||||
Если копия на каждое чтение слишком дорога, альтернатива - возвращать `_tracks` напрямую и документировать, что значение живое. Тогда тип должен быть осмысленным: `List` здесь обманывает читателя, и честнее отдавать `Sequence` или явный метод `snapshot()`.
|
||||
|
||||
## Неизменяемость и многопоточность
|
||||
```kotlin
|
||||
fun main() {
|
||||
val shared = mutableListOf(1, 2, 3)
|
||||
|
||||
// Небезопасно: обход в одном потоке + add в другом = ConcurrentModificationException
|
||||
// Безопасно: работать с копией
|
||||
val safe = shared.toList()
|
||||
println(safe.sum()) // 6
|
||||
|
||||
val sync = java.util.Collections.synchronizedList(mutableListOf(1, 2))
|
||||
println(sync.size) // 2 — но обход всё равно нужно синхронизировать вручную
|
||||
val concurrent = java.util.concurrent.CopyOnWriteArrayList(listOf(1, 2))
|
||||
println(concurrent.size) // 2
|
||||
}
|
||||
```
|
||||
Неизменяемое значение потокобезопасно по определению - именно поэтому в конкурентном коде предпочитают read-only копии, а не блокировки. Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
|
||||
|
||||
## Персистентные коллекции
|
||||
В stdlib настоящих immutable-коллекций нет. Их даёт библиотека `kotlinx.collections.immutable`:
|
||||
```kotlin
|
||||
// Требует зависимости org.jetbrains.kotlinx:kotlinx-collections-immutable.
|
||||
// val list: PersistentList<Int> = persistentListOf(1, 2, 3)
|
||||
// val updated = list.add(4) // новый список, старый цел
|
||||
// println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4]
|
||||
//
|
||||
// Тип ImmutableList<T> гарантирует, что коллекция не изменится вообще,
|
||||
// а PersistentList<T> добавляет эффективное «обновление» без полного копирования.
|
||||
```
|
||||
Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Collections.unmodifiableList(x)` | тип `List<T>` |
|
||||
> | `List.copyOf(x)` (Java 10+) | `x.toList()` |
|
||||
> | `List.of(1, 2)` - immutable | `listOf(1, 2)` - read-only |
|
||||
> | нарушение видно в рантайме | видно при компиляции |
|
||||
> | `final List` | `val list` |
|
||||
> | Guava `ImmutableList` | `kotlinx.collections.immutable` |
|
||||
>
|
||||
> **`unmodifiableList` и `List` решают одну задачу по-разному.** Java-обёртка - объект-декоратор: тип остаётся `List`, метод `add` есть, но бросает исключение в рантайме. Kotlin убирает метод из интерфейса, поэтому ошибка видна компилятору и обёртка не нужна:
|
||||
> ```java
|
||||
> List<Integer> ro = Collections.unmodifiableList(src);
|
||||
> ro.add(1); // компилируется, падает в рантайме
|
||||
> ```
|
||||
> Но у Java-обёртки есть преимущество, которого нет у Kotlin: она действительно запрещает изменение через **эту** ссылку даже при касте. Kotlin-ский `List` каст к `MutableList` может пережить, если под ним `ArrayList`.
|
||||
>
|
||||
> **`List.of` в Java сильнее, чем `listOf` в Kotlin.** `List.of(1, 2)` создаёт по-настоящему неизменяемую коллекцию: любая попытка изменения, в том числе из другого места, бросит исключение. `listOf(1, 2)` даёт неизменяемую реализацию только случайно - это деталь, на которую нельзя полагаться, потому что `listOf` на самом деле описывает интерфейс, а не гарантию объекта.
|
||||
>
|
||||
> **Защитное копирование нужно в обоих языках.** `private final List` в Java и `private val` в Kotlin одинаково не защищают содержимое. Разница в том, что в Kotlin ошибку легче не заметить: тип `List` в сигнатуре выглядит как обещание неизменяемости, которого он не даёт.
|
||||
>
|
||||
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Возврат `MutableList` под типом `List` - утечка:**
|
||||
> ```kotlin
|
||||
> class Leaky2 {
|
||||
> private val items = mutableListOf(1, 2)
|
||||
> fun items(): List<Int> = items
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val l = Leaky2()
|
||||
> val view = l.items()
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> (view as MutableList<Int>).add(3) // клиент изменил приватное состояние
|
||||
> println(l.items()) // [1, 2, 3]
|
||||
> }
|
||||
> ```
|
||||
> Каст сработал, потому что под ссылкой настоящий `ArrayList`. Защита - `toList()`.
|
||||
>
|
||||
> **`toList()` копирует поверхностно** - элементы остаются общими. Для вложенных структур нужна глубокая копия:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val nested = listOf(mutableListOf(1), mutableListOf(2))
|
||||
> val shallow = nested.toList()
|
||||
> nested[0].add(99)
|
||||
> println(shallow[0]) // [1, 99] — внутренний список общий
|
||||
>
|
||||
> val deep = nested.map { it.toList() }
|
||||
> nested[0].add(100)
|
||||
> println(deep[0]) // [1, 99] — а этот цел
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`val` не делает коллекцию неизменяемой** - только ссылку. `val items = mutableListOf<Int>()` изменяется свободно.
|
||||
>
|
||||
> **Из Java read-only коллекция изменяема:**
|
||||
> ```kotlin
|
||||
> fun provide(): List<Int> = mutableListOf(1, 2)
|
||||
>
|
||||
> fun main() { println(provide()) } // [1, 2]
|
||||
> ```
|
||||
> Java-код получит `java.util.List` и вызовет `add` без ошибок компиляции. Для API, доступного из Java, копируйте или используйте `Collections.unmodifiableList`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> **Изменяемый объект как ключ `Map` теряется:**
|
||||
> ```kotlin
|
||||
> data class MutableKey(var id: Int)
|
||||
>
|
||||
> fun main() {
|
||||
> val key = MutableKey(1)
|
||||
> val map = mutableMapOf(key to "значение")
|
||||
> key.id = 2
|
||||
> println(map[key]) // null — хеш изменился
|
||||
> println(map.size) // 1 — запись на месте, но недостижима
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`+=` на `var list: List<T>` в цикле - это O(n²):**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> var slow = listOf<Int>()
|
||||
> repeat(1000) { slow += it } // 1000 копирований списка
|
||||
> println(slow.size) // 1000
|
||||
>
|
||||
> val fast = buildList { repeat(1000) { add(it) } }
|
||||
> println(fast.size) // 1000, одна коллекция
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`ConcurrentModificationException` при изменении во время обхода:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = mutableListOf(1, 2, 3)
|
||||
> try {
|
||||
> for (x in list) if (x == 2) list.remove(x)
|
||||
> } catch (e: ConcurrentModificationException) {
|
||||
> println("нельзя изменять во время обхода")
|
||||
> }
|
||||
> list.removeAll { it == 2 } // так правильно
|
||||
> println(list) // [1, 3]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Обёртка `synchronizedList` не делает обход потокобезопасным** - атомарны только отдельные методы. Итерацию нужно оборачивать в `synchronized` вручную.
|
||||
|
||||
Далее: [Операции](Операции.md)
|
||||
@@ -1,282 +0,0 @@
|
||||
---
|
||||
title: Коллекции под капотом
|
||||
description: Что лежит в байт-коде, сложность операций, расход памяти и упаковка примитивов
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
- performance
|
||||
---
|
||||
|
||||
Отдельных классов коллекций у Kotlin нет. `List`, `MutableList`, `Map` - это интерфейсы, которые при компиляции отображаются в типы `java.util`.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).javaClass.name) // java.util.Arrays$ArrayList
|
||||
println(mutableListOf(1, 2).javaClass.name) // java.util.ArrayList
|
||||
println(setOf(1, 2).javaClass.name) // java.util.LinkedHashSet
|
||||
println(mapOf("a" to 1, "b" to 2).javaClass.name) // java.util.LinkedHashMap
|
||||
println(emptyList<Int>().javaClass.name) // kotlin.collections.EmptyList
|
||||
println(listOf(1).javaClass.name) // java.util.Collections$SingletonList
|
||||
println(mapOf("a" to 1).javaClass.name) // java.util.Collections$SingletonMap
|
||||
}
|
||||
```
|
||||
|
||||
Обратите внимание: `listOf` возвращает **три разные** реализации в зависимости от числа аргументов, и `mapOf` ведёт себя так же. Это оптимизация под частые случаи - пустая коллекция и коллекция из одного элемента не требуют ни массива, ни хеш-таблицы.
|
||||
|
||||
## Соответствие типов
|
||||
| Kotlin | JVM | Примечание |
|
||||
|---|---|---|
|
||||
| `List<T>` | `java.util.List` | тот же интерфейс |
|
||||
| `MutableList<T>` | `java.util.List` | **тот же**, различия только в компиляторе |
|
||||
| `Map<K, V>` | `java.util.Map` | |
|
||||
| `Array<T>` | `T[]` | настоящий массив |
|
||||
| `IntArray` | `int[]` | без упаковки |
|
||||
| `Array<Int>` | `Integer[]` | с упаковкой |
|
||||
|
||||
`List` и `MutableList` - это один и тот же `java.util.List` в байт-коде. Разделение существует только при компиляции Kotlin, что объясняет две вещи: из Java read-only коллекция изменяема, и каст `List` в `MutableList` компилируется.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val ro: List<Int> = mutableListOf(1, 2)
|
||||
println(ro is MutableList<*>) // true — в рантайме это один тип
|
||||
}
|
||||
```
|
||||
|
||||
## Сложность операций
|
||||
| Операция | `ArrayList` | `LinkedHashSet` | `LinkedHashMap` | `TreeMap` |
|
||||
|---|---|---|---|---|
|
||||
| доступ по индексу | O(1) | - | - | - |
|
||||
| поиск элемента | O(n) | O(1) | O(1) по ключу | O(log n) |
|
||||
| вставка в конец | O(1)* | O(1) | O(1) | O(log n) |
|
||||
| вставка в начало | O(n) | O(1) | O(1) | O(log n) |
|
||||
| удаление по индексу | O(n) | - | - | - |
|
||||
| удаление по значению | O(n) | O(1) | O(1) | O(log n) |
|
||||
|
||||
\* амортизированная: при переполнении массив копируется в новый, вдвое больший.
|
||||
|
||||
Практические следствия:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = List(10_000) { it }
|
||||
val set = list.toSet()
|
||||
|
||||
// O(n) против O(1)
|
||||
println(9999 in list) // true, но обошёл весь список
|
||||
println(9999 in set) // true, один хеш
|
||||
|
||||
// вставка в начало ArrayList — сдвиг всего массива
|
||||
val deque = ArrayDeque(list)
|
||||
deque.addFirst(-1) // O(1)
|
||||
println(deque.first()) // -1
|
||||
}
|
||||
```
|
||||
|
||||
## Упаковка примитивов
|
||||
Главный источник неожиданных расходов памяти:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val boxed: List<Int> = List(5) { it } // каждый элемент — объект Integer
|
||||
val primitive: IntArray = IntArray(5) { it } // настоящий int[]
|
||||
|
||||
println(boxed.sum()) // 10
|
||||
println(primitive.sum()) // 10
|
||||
println(primitive.javaClass.name) // [I — массив int
|
||||
}
|
||||
```
|
||||
|
||||
Разница в памяти на 1 млн элементов:
|
||||
|
||||
| Структура | Примерный размер |
|
||||
|---|---|
|
||||
| `IntArray(1_000_000)` | ~4 МБ |
|
||||
| `Array<Int>(1_000_000)` | ~20 МБ (ссылки + объекты) |
|
||||
| `List<Int>` из 1 млн | ~20 МБ |
|
||||
|
||||
Кеш `Integer` смягчает это для малых чисел:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a: Int? = 127
|
||||
val b: Int? = 127
|
||||
println(a === b) // true — из кеша -128..127
|
||||
|
||||
val c: Int? = 128
|
||||
val d: Int? = 128
|
||||
println(c === d) // false — разные объекты
|
||||
println(c == d) // true — значения равны
|
||||
}
|
||||
```
|
||||
Это ровно та же ловушка, что с `Integer` в Java. Правило: для чисел всегда `==`, никогда `===`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
|
||||
|
||||
Специализированные массивы без упаковки:
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(intArrayOf(1, 2, 3).sum()) // 6
|
||||
println(doubleArrayOf(1.0, 2.0).average()) // 1.5
|
||||
println(booleanArrayOf(true, false).count { it }) // 1
|
||||
println(longArrayOf(1L).size) // 1
|
||||
}
|
||||
```
|
||||
Их стоит использовать, когда элементов много и они числовые: матрицы, буферы, обработка сигналов. В обычном бизнес-коде разница несущественна.
|
||||
|
||||
## Рост ArrayList
|
||||
```kotlin
|
||||
fun main() {
|
||||
val withoutCapacity = ArrayList<Int>() // начальная ёмкость 10
|
||||
repeat(1000) { withoutCapacity.add(it) } // ~8 перевыделений массива
|
||||
|
||||
val withCapacity = ArrayList<Int>(1000) // сразу нужный размер
|
||||
repeat(1000) { withCapacity.add(it) } // перевыделений нет
|
||||
|
||||
println("${withoutCapacity.size} ${withCapacity.size}") // 1000 1000
|
||||
}
|
||||
```
|
||||
При каждом переполнении создаётся массив в 1.5 раза больше и копируется содержимое. Для заранее известного размера задавайте ёмкость - это убирает лог n копирований.
|
||||
|
||||
## Что делает цепочка операций
|
||||
```kotlin
|
||||
fun main() {
|
||||
val source = List(1000) { it }
|
||||
|
||||
val result = source
|
||||
.map { it * 2 } // новый ArrayList на 1000
|
||||
.filter { it > 100 } // новый ArrayList на ~950
|
||||
.take(5) // новый ArrayList на 5
|
||||
println(result) // [102, 104, 106, 108, 110]
|
||||
}
|
||||
```
|
||||
Три промежуточных списка, 1000 + 950 операций ради 5 результатов. Та же цепочка на `Sequence` не создаст ни одного промежуточного списка и выполнит ~56 операций. Подробнее: [Sequence](Sequence.md)
|
||||
|
||||
Измерить разницу можно так:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val source = List(1_000_000) { it }
|
||||
|
||||
val t1 = System.nanoTime()
|
||||
val a = source.map { it * 2 }.filter { it > 100 }.first()
|
||||
val eager = (System.nanoTime() - t1) / 1_000_000
|
||||
|
||||
val t2 = System.nanoTime()
|
||||
val b = source.asSequence().map { it * 2 }.filter { it > 100 }.first()
|
||||
val lazy = (System.nanoTime() - t2) / 1_000_000
|
||||
|
||||
println("$a == $b") // 102 == 102
|
||||
println("list ${eager}мс, sequence ${lazy}мс — ленивый быстрее: ${lazy < eager}")
|
||||
}
|
||||
```
|
||||
|
||||
## Inline и отсутствие объектов-лямбд
|
||||
Операции над коллекциями объявлены `inline`, поэтому лямбда не создаёт объект:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 }
|
||||
println(sum) // 10
|
||||
}
|
||||
```
|
||||
Компилятор подставляет тело лямбды прямо в цикл - в байт-коде нет ни объекта `Function1`, ни виртуального вызова. Это то, чего не даёт Java-стрим: там каждая лямбда - объект, а цепочка строит конвейер из `Spliterator`-ов.
|
||||
|
||||
Исключение - `Sequence`: там операции не могут быть полностью inline, потому что элементы обрабатываются лениво через итератор. Это и есть источник накладных расходов `Sequence` на малых данных. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
|
||||
|
||||
## Как выбирают структуру
|
||||
```kotlin
|
||||
fun main() {
|
||||
// частый поиск -> Set
|
||||
val allowed = setOf("a", "b", "c")
|
||||
println(listOf("a", "x").count { it in allowed }) // 1
|
||||
|
||||
// частое добавление в начало и конец -> ArrayDeque
|
||||
val queue = ArrayDeque<Int>()
|
||||
queue.addLast(1); queue.addFirst(0)
|
||||
println(queue) // [0, 1]
|
||||
|
||||
// нужен отсортированный обход и диапазоны -> TreeMap
|
||||
val sorted = sortedMapOf(3 to "c", 1 to "a", 2 to "b")
|
||||
println(sorted.headMap(3)) // {1=a, 2=b}
|
||||
|
||||
// много числовых данных -> примитивный массив
|
||||
val buffer = IntArray(1000)
|
||||
println(buffer.size) // 1000
|
||||
}
|
||||
```
|
||||
|
||||
## Потокобезопасность
|
||||
```kotlin
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
fun main() {
|
||||
val map = ConcurrentHashMap<String, Int>()
|
||||
map["a"] = 1
|
||||
map.compute("a") { _, v -> (v ?: 0) + 1 }
|
||||
println(map) // {a=2}
|
||||
|
||||
val sync = java.util.Collections.synchronizedMap(mutableMapOf("a" to 1))
|
||||
println(sync.size) // 1
|
||||
}
|
||||
```
|
||||
Обычные коллекции Kotlin не потокобезопасны - это те же `ArrayList` и `HashMap`. Варианты:
|
||||
- read-only копия на каждый поток (`toList()`) - проще всего
|
||||
- `ConcurrentHashMap` и `CopyOnWriteArrayList` из JDK
|
||||
- корутины с ограничением доступа одним потоком или `Mutex`
|
||||
|
||||
Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `ArrayList` явно | `mutableListOf()` -> `ArrayList` |
|
||||
> | `new HashMap<>()` | `mutableMapOf()` -> `LinkedHashMap` |
|
||||
> | `IntStream` без упаковки | `IntArray` или Java-стрим |
|
||||
> | лямбда - объект `Function` | лямбда инлайнится |
|
||||
> | `Stream` ленив | `Iterable`-операции энергичны |
|
||||
> | `new ArrayList<>(100)` | `ArrayList(100)` |
|
||||
>
|
||||
> **Коллекции те же самые - это не «Kotlin-коллекции».** Любая `List<T>` из Kotlin принимается Java-методом, принимающим `java.util.List`, без конвертации и накладных расходов. Это принципиальное отличие от языков со своей библиотекой коллекций (например, Scala), где на границе нужны обёртки.
|
||||
>
|
||||
> **Умолчание `LinkedHashMap` вместо `HashMap`.** `mutableMapOf()` создаёт `LinkedHashMap`, который на ~20% тяжелее по памяти (хранит ссылки на соседей), но даёт предсказуемый порядок обхода. Если порядок не нужен и записей миллионы, `hashMapOf()` экономнее.
|
||||
>
|
||||
> **Инлайн лямбд - преимущество перед стримами.** В Java `list.stream().filter(x -> x > 1)` создаёт объект лямбды и конвейер стрима. В Kotlin `list.filter { it > 1 }` компилируется в обычный цикл `for` с телом лямбды внутри - объектов не создаётся вовсе. Для коротких цепочек на небольших коллекциях Kotlin обычно быстрее Java-стримов именно поэтому.
|
||||
>
|
||||
> **Нет `IntStream`.** Java даёт специализации для `int`, `long`, `double`, избегающие упаковки в цепочках. В Kotlin эквивалента нет: `Sequence<Int>` упаковывает. Для числовых расчётов на больших объёмах используйте `IntArray` с обычным циклом или вызывайте Java-стримы.
|
||||
>
|
||||
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`listOf` возвращает разные классы** - полагаться на конкретный тип нельзя:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println(listOf<Int>().javaClass.simpleName) // EmptyList
|
||||
> println(listOf(1).javaClass.simpleName) // SingletonList
|
||||
> println(listOf(1, 2).javaClass.simpleName) // ArrayList (Arrays$ArrayList)
|
||||
> }
|
||||
> ```
|
||||
> Ни один из них не поддерживает `add`, но по разным причинам, и сообщения об ошибках будут разными.
|
||||
>
|
||||
> **`Arrays$ArrayList` - не `java.util.ArrayList`.** Это обёртка над массивом с фиксированным размером: `set` работает, `add` бросает `UnsupportedOperationException`.
|
||||
>
|
||||
> **`List<Int>` упаковывает каждое число.** Для миллиона элементов это ~20 МБ вместо 4 МБ и нагрузка на сборщик мусора. В горячем численном коде используйте `IntArray`.
|
||||
>
|
||||
> **`===` на числах даёт разные результаты в зависимости от значения** - из-за кеша `Integer` в диапазоне -128..127. Сравнивайте только через `==`.
|
||||
>
|
||||
> **`list.contains` в цикле - O(n²).** Самая частая проблема производительности:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a = List(2000) { it }
|
||||
> val b = List(2000) { it * 2 }
|
||||
> println(a.count { it in b }) // 4 млн сравнений
|
||||
> val bSet = b.toSet()
|
||||
> println(a.count { it in bSet }) // 2000 операций, тот же результат
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Каждая операция создаёт коллекцию.** Цепочка из пяти операций по большому списку - пять списков в памяти одновременно. Для больших данных нужен `Sequence`.
|
||||
>
|
||||
> **`ArrayList` без начальной ёмкости перевыделяет массив.** При известном размере задавайте его: `ArrayList(n)` или `buildList(n) { }`.
|
||||
>
|
||||
> **`subList` держит ссылку на исходный список** - он не будет собран сборщиком мусора, даже если нужен только маленький срез большого списка. Для среза, который переживёт оригинал, используйте `slice` или `toList`.
|
||||
>
|
||||
> **Коллекции Kotlin не потокобезопасны.** `mutableListOf()` - обычный `ArrayList`. Одновременное изменение из двух потоков повреждает структуру или бросает `ConcurrentModificationException`.
|
||||
>
|
||||
> **Изменяемый ключ ломает `HashMap`.** Запись становится недостижимой, хотя `size` её считает. Ключи должны быть неизменяемыми - `data class` с `val`-полями. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
>
|
||||
> **Измерения производительности на JVM без прогрева врут.** Пример с `System.nanoTime` выше показывает порядок величины, но JIT компилирует горячий код только после тысяч вызовов. Для настоящих измерений нужен JMH.
|
||||
|
||||
Далее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md)
|
||||
@@ -1,234 +0,0 @@
|
||||
---
|
||||
title: Коллекции
|
||||
description: Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
---
|
||||
|
||||
Коллекции Kotlin - это **те же** классы JDK (`ArrayList`, `HashMap`, `LinkedHashSet`), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val readOnly: List<Int> = listOf(1, 2, 3)
|
||||
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
|
||||
|
||||
mutable.add(4)
|
||||
// readOnly.add(4) // ошибка: у List нет метода add
|
||||
|
||||
println("$readOnly / $mutable") // [1, 2, 3] / [1, 2, 3, 4]
|
||||
println(readOnly.javaClass.name) // java.util.Arrays$ArrayList
|
||||
println(mutable.javaClass.name) // java.util.ArrayList
|
||||
}
|
||||
```
|
||||
|
||||
## Иерархия
|
||||
```mermaid
|
||||
graph TD
|
||||
I["Iterable<out T>"] --> C["Collection<out T>"]
|
||||
C --> L["List<out T>"]
|
||||
C --> S["Set<out T>"]
|
||||
I --> MI["MutableIterable<T>"]
|
||||
MI --> MC["MutableCollection<T>"]
|
||||
C --> MC
|
||||
MC --> ML["MutableList<T>"]
|
||||
L --> ML
|
||||
MC --> MS["MutableSet<T>"]
|
||||
S --> MS
|
||||
M["Map<K, out V>"] --> MM["MutableMap<K, V>"]
|
||||
```
|
||||
|
||||
Два важных факта из этой схемы:
|
||||
- `MutableList` **наследует** `List`, то есть изменяемую коллекцию можно передать туда, где ожидается читаемая. Обратное невозможно.
|
||||
- `Map` не входит в `Collection` - это отдельная иерархия, как и в Java.
|
||||
|
||||
| Интерфейс | Чтение | Изменение | Реализация по умолчанию |
|
||||
|---|---|---|---|
|
||||
| `List<T>` | да | нет | `ArrayList` под капотом |
|
||||
| `MutableList<T>` | да | да | `ArrayList` |
|
||||
| `Set<T>` | да | нет | `LinkedHashSet` |
|
||||
| `MutableSet<T>` | да | да | `LinkedHashSet` |
|
||||
| `Map<K, V>` | да | нет | `LinkedHashMap` |
|
||||
| `MutableMap<K, V>` | да | да | `LinkedHashMap` |
|
||||
|
||||
Обратите внимание: умолчание для `Set` и `Map` - **`Linked`**-версии, то есть порядок вставки сохраняется. В Java `new HashSet<>()` порядок не гарантирует.
|
||||
|
||||
## Создание
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = listOf(1, 2, 3) // read-only
|
||||
val b = mutableListOf(1, 2, 3) // изменяемый
|
||||
val c = emptyList<Int>() // пустой
|
||||
val d = listOfNotNull(1, null, 3) // null отфильтрованы
|
||||
val e = List(3) { it * it } // по индексу
|
||||
|
||||
println("$a $b $c $d $e") // [1, 2, 3] [1, 2, 3] [] [1, 3] [0, 1, 4]
|
||||
|
||||
val s = setOf(1, 2, 2, 3) // дубликаты убраны
|
||||
val m = mapOf("a" to 1, "b" to 2)
|
||||
println("$s $m") // [1, 2, 3] {a=1, b=2}
|
||||
}
|
||||
```
|
||||
|
||||
Специализированные варианты для разных реализаций:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val sorted = sortedSetOf(3, 1, 2) // TreeSet
|
||||
val sortedMap = sortedMapOf("b" to 2, "a" to 1)
|
||||
val linked = linkedSetOf(3, 1, 2) // LinkedHashSet
|
||||
val hash = hashSetOf(3, 1, 2) // HashSet, порядок не гарантирован
|
||||
val arrayDeque = ArrayDeque(listOf(1, 2, 3))
|
||||
|
||||
println("$sorted $sortedMap $linked") // [1, 2, 3] {a=1, b=2} [3, 1, 2]
|
||||
println(hash.size) // 3
|
||||
arrayDeque.addFirst(0)
|
||||
println(arrayDeque) // [0, 1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
## Билдеры
|
||||
Когда коллекцию нужно собрать по условию, а результат отдать как read-only:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = buildList {
|
||||
add(1)
|
||||
if (true) add(2)
|
||||
addAll(listOf(3, 4))
|
||||
}
|
||||
println(list) // [1, 2, 3, 4]
|
||||
|
||||
val map = buildMap {
|
||||
put("a", 1)
|
||||
putAll(mapOf("b" to 2))
|
||||
}
|
||||
println(map) // {a=1, b=2}
|
||||
|
||||
val set = buildSet { repeat(3) { add(it % 2) } }
|
||||
println(set) // [0, 1]
|
||||
}
|
||||
```
|
||||
Внутри лямбды коллекция изменяемая, снаружи возвращается `List<T>`. Это чище, чем `mutableListOf` с последующим приведением.
|
||||
|
||||
## Пустая коллекция и null
|
||||
```kotlin
|
||||
fun main() {
|
||||
val nullable: List<Int>? = null
|
||||
println(nullable?.size ?: 0) // 0
|
||||
println(nullable.orEmpty()) // []
|
||||
println(nullable.isNullOrEmpty()) // true
|
||||
|
||||
val empty = emptyList<Int>()
|
||||
println(empty.firstOrNull()) // null
|
||||
println(empty.sum()) // 0
|
||||
}
|
||||
```
|
||||
Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List<T>?` заставляет каждого клиента писать `?.` или `orEmpty()`, не давая взамен никакой информации - пустой список уже означает «ничего нет».
|
||||
|
||||
## Массив не коллекция
|
||||
```kotlin
|
||||
fun main() {
|
||||
val array = arrayOf(1, 2, 3)
|
||||
// array.filter — есть, потому что Array получает расширения из Iterable
|
||||
println(array.filter { it > 1 }) // [2, 3] — это уже List
|
||||
|
||||
println(array.toList()::class.simpleName) // ArrayList
|
||||
println(listOf(1, 2).toTypedArray().size) // 2
|
||||
}
|
||||
```
|
||||
`Array<T>` - отдельный тип, не входящий в `Collection`. У него фиксированный размер и инвариантность. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [List, Set, Map](List,%20Set,%20Map.md) | три семейства, их гарантии и выбор между ними |
|
||||
| [Изменяемость](Изменяемость.md) | read-only против immutable, защита от изменения |
|
||||
| [Операции](Операции.md) | `map`, `filter`, `groupBy`, `fold` и остальной арсенал |
|
||||
| [Sequence](Sequence.md) | ленивые вычисления и когда они выигрывают |
|
||||
| [Коллекции под капотом](Коллекции%20под%20капотом.md) | что лежит в байт-коде, сложность операций, память |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `new ArrayList<>()` | `mutableListOf()` |
|
||||
> | `List.of(1, 2)` (Java 9+) | `listOf(1, 2)` |
|
||||
> | `Collections.emptyList()` | `emptyList()` |
|
||||
> | изменяемость не в типе | `List` против `MutableList` |
|
||||
> | `UnsupportedOperationException` в рантайме | ошибка компиляции |
|
||||
> | `Stream` для операций | расширения прямо на коллекции |
|
||||
> | `new HashSet<>()` - порядок не гарантирован | `setOf()` - порядок вставки |
|
||||
>
|
||||
> **Разделение на `List` и `MutableList` - главное нововведение.** В Java `List` - один интерфейс с методом `add`, и `List.of(1, 2)` его тоже имеет: вызов просто бросает `UnsupportedOperationException` в рантайме.
|
||||
> ```java
|
||||
> List<Integer> list = List.of(1, 2);
|
||||
> list.add(3); // компилируется, падает в рантайме
|
||||
> ```
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1, 2)
|
||||
> // list.add(3) // не компилируется
|
||||
> println(list.size)
|
||||
> }
|
||||
> ```
|
||||
> Ошибка переехала из рантайма в компиляцию - это главная практическая польза.
|
||||
>
|
||||
> **Операции не требуют `Stream`.** В Java цепочка выглядит как `list.stream().filter(...).map(...).collect(Collectors.toList())`. В Kotlin `filter` и `map` - функции-расширения прямо на `Iterable`, они сразу возвращают `List`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }) // [20, 30]
|
||||
> }
|
||||
> ```
|
||||
> Цена - промежуточный список на каждом шаге. Ленивый аналог `Stream` - это [Sequence](Sequence.md).
|
||||
>
|
||||
> **`setOf` и `mapOf` сохраняют порядок вставки**, потому что под ними `LinkedHashSet` и `LinkedHashMap`. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как `Set`, а не как «упорядоченное множество».
|
||||
>
|
||||
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`List` - это read-only, а не immutable.** Объект под ним может меняться через другую ссылку:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val mutable = mutableListOf(1, 2)
|
||||
> val readOnly: List<Int> = mutable // та же коллекция!
|
||||
> mutable.add(3)
|
||||
> println(readOnly) // [1, 2, 3] — изменился
|
||||
> }
|
||||
> ```
|
||||
> `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md)
|
||||
>
|
||||
> **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает:
|
||||
> ```kotlin
|
||||
> fun giveList(): List<Int> = mutableListOf(1, 2)
|
||||
>
|
||||
> fun main() { println(giveList().size) } // 2
|
||||
> ```
|
||||
> Java-клиент вызовет `add` без всяких ошибок компиляции, и если под ссылкой `ArrayList` - элемент добавится. Для настоящей защиты нужна копия.
|
||||
>
|
||||
> **`listOf()` возвращает разные реализации.** `listOf()` без аргументов - `EmptyList`, `listOf(x)` - `SingletonList`, с несколькими - `Arrays$ArrayList`. То же у `mapOf` и `setOf`: на одном элементе это `SingletonMap`/`SingletonSet`, а не `Linked`-версия. Последний не поддерживает `add` даже через каст:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1, 2)
|
||||
> try {
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> (list as MutableList<Int>).add(3)
|
||||
> } catch (e: UnsupportedOperationException) {
|
||||
> println("каст скомпилировался, но add упал")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Приводить `List` к `MutableList` нельзя: иногда это сработает, иногда бросит исключение.
|
||||
>
|
||||
> **`Map` не `Collection`** - у него нет `size` через `Collection`-интерфейс, `filter` работает с `Map.Entry`, а `map` возвращает `List`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val m = mapOf("a" to 1, "b" to 2)
|
||||
> println(m.filter { it.value > 1 }) // {b=2} — остался Map
|
||||
> println(m.map { it.key }) // [a, b] — стал List
|
||||
> println(m.mapValues { it.value * 10 }) // {a=10, b=20}
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Операции создают новую коллекцию на каждом шаге.** Цепочка из пяти `map`/`filter` по списку в миллион элементов создаст пять списков. Для длинных цепочек и больших данных нужен [Sequence](Sequence.md).
|
||||
>
|
||||
> **`hashSetOf` и `hashMapOf` не гарантируют порядок.** Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте `setOf`/`mapOf` или сортируйте явно.
|
||||
|
||||
Далее: [List, Set, Map](List,%20Set,%20Map.md)
|
||||
@@ -1,424 +0,0 @@
|
||||
---
|
||||
title: Операции
|
||||
description: map, filter, groupBy, fold, zip и остальной арсенал операций над коллекциями
|
||||
tags:
|
||||
- kotlin
|
||||
- collections
|
||||
---
|
||||
|
||||
Операции над коллекциями - функции-расширения на `Iterable<T>`. Их около 150; здесь - те, что покрывают почти всю практику.
|
||||
|
||||
Все они делятся на две группы:
|
||||
- **возвращают коллекцию**: `map`, `filter`, `sorted` - можно строить цепочки
|
||||
- **возвращают одно значение**: `sum`, `first`, `count`, `fold` - завершают цепочку
|
||||
|
||||
## Преобразование
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3)
|
||||
|
||||
println(list.map { it * 2 }) // [2, 4, 6]
|
||||
println(list.mapIndexed { i, x -> "$i:$x" }) // [0:1, 1:2, 2:3]
|
||||
println(listOf("1", "a", "3").mapNotNull { it.toIntOrNull() }) // [1, 3]
|
||||
println(list.flatMap { listOf(it, it * 10) }) // [1, 10, 2, 20, 3, 30]
|
||||
println(listOf(listOf(1, 2), listOf(3)).flatten()) // [1, 2, 3]
|
||||
println(list.zip(listOf("a", "b"))) // [(1, a), (2, b)]
|
||||
println(list.withIndex().map { "${it.index}${it.value}" }) // [01, 12, 23]
|
||||
}
|
||||
```
|
||||
`mapNotNull` - самая полезная из них: преобразование с отбрасыванием неудач в одном шаге, вместо `map` + `filterNotNull`.
|
||||
|
||||
`flatMap` нужен, когда преобразование даёт коллекцию, а результат нужен плоским:
|
||||
```kotlin
|
||||
data class Order5(val id: Int, val items: List<String>)
|
||||
|
||||
fun main() {
|
||||
val orders = listOf(
|
||||
Order5(1, listOf("хлеб", "молоко")),
|
||||
Order5(2, listOf("сыр")),
|
||||
)
|
||||
println(orders.flatMap { it.items }) // [хлеб, молоко, сыр]
|
||||
println(orders.map { it.items }) // [[хлеб, молоко], [сыр]]
|
||||
}
|
||||
```
|
||||
|
||||
## Фильтрация
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4, 5)
|
||||
|
||||
println(list.filter { it % 2 == 0 }) // [2, 4]
|
||||
println(list.filterNot { it % 2 == 0 }) // [1, 3, 5]
|
||||
println(list.filterIndexed { i, _ -> i > 2 }) // [4, 5]
|
||||
println(listOf(1, null, 3).filterNotNull()) // [1, 3]
|
||||
println(listOf<Any>(1, "a", 2).filterIsInstance<Int>()) // [1, 2]
|
||||
println(list.take(2)) // [1, 2]
|
||||
println(list.takeWhile { it < 3 }) // [1, 2]
|
||||
println(list.dropWhile { it < 3 }) // [3, 4, 5]
|
||||
println(list.distinct()) // [1, 2, 3, 4, 5]
|
||||
println(listOf("aa", "ab", "ba").distinctBy { it.first() }) // [aa, ba]
|
||||
}
|
||||
```
|
||||
|
||||
`partition` делит на две коллекции за один проход:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val (even, odd) = listOf(1, 2, 3, 4).partition { it % 2 == 0 }
|
||||
println("$even $odd") // [2, 4] [1, 3]
|
||||
}
|
||||
```
|
||||
|
||||
## Поиск и проверки
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4)
|
||||
|
||||
println(list.first { it > 2 }) // 3, бросает если нет
|
||||
println(list.firstOrNull { it > 10 }) // null
|
||||
println(list.find { it > 2 }) // 3 — то же, что firstOrNull
|
||||
println(list.last()) // 4
|
||||
println(list.single { it == 2 }) // 2, бросает если не ровно один
|
||||
println(list.singleOrNull { it > 2 }) // null — их два
|
||||
|
||||
println(list.any { it > 3 }) // true
|
||||
println(list.all { it > 0 }) // true
|
||||
println(list.none { it > 10 }) // true
|
||||
println(list.count { it % 2 == 0 }) // 2
|
||||
println(3 in list) // true
|
||||
println(list.indexOfFirst { it > 2 }) // 2
|
||||
}
|
||||
```
|
||||
|
||||
Поведение на пустой коллекции - источник ошибок:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val empty = emptyList<Int>()
|
||||
|
||||
println(empty.any { it > 0 }) // false
|
||||
println(empty.all { it > 0 }) // true — вакуумная истина!
|
||||
println(empty.none { it > 0 }) // true
|
||||
println(empty.sum()) // 0
|
||||
println(empty.firstOrNull()) // null
|
||||
println(empty.maxOrNull()) // null
|
||||
}
|
||||
```
|
||||
`all` на пустой коллекции возвращает `true` - это математически корректно, но в проверках вида «все элементы валидны» часто неожиданно.
|
||||
|
||||
## Агрегация
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4)
|
||||
|
||||
println(list.sum()) // 10
|
||||
println(list.average()) // 2.5
|
||||
println(list.maxOrNull()) // 4
|
||||
println(list.minOrNull()) // 1
|
||||
println(list.sumOf { it * 10 }) // 100
|
||||
println(list.maxByOrNull { -it }) // 1
|
||||
println(list.maxOfOrNull { it * 2 }) // 8
|
||||
println(list.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3, 4]
|
||||
println(list.joinToString(limit = 2, truncated = "…")) // 1, 2, …
|
||||
}
|
||||
```
|
||||
|
||||
`maxBy` против `maxOf` - частая путаница:
|
||||
```kotlin
|
||||
data class User8(val name: String, val age: Int)
|
||||
|
||||
fun main() {
|
||||
val users = listOf(User8("Иван", 30), User8("Пётр", 25))
|
||||
|
||||
println(users.maxByOrNull { it.age }) // User8(name=Иван, age=30) — элемент
|
||||
println(users.maxOfOrNull { it.age }) // 30 — значение
|
||||
}
|
||||
```
|
||||
|
||||
## fold и reduce
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4)
|
||||
|
||||
println(list.fold(0) { acc, x -> acc + x }) // 10
|
||||
println(list.fold(1) { acc, x -> acc * x }) // 24
|
||||
println(list.reduce { acc, x -> acc + x }) // 10
|
||||
println(list.runningFold(0) { acc, x -> acc + x }) // [0, 1, 3, 6, 10]
|
||||
println(list.foldRight(0) { x, acc -> acc * 10 + x }) // 4321 — справа налево
|
||||
|
||||
println(emptyList<Int>().fold(0) { a, b -> a + b }) // 0
|
||||
try {
|
||||
emptyList<Int>().reduce { a, b -> a + b }
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
println("reduce падает на пустой коллекции")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| | `fold` | `reduce` |
|
||||
|---|---|---|
|
||||
| начальное значение | задаёте вы | первый элемент |
|
||||
| тип результата | любой | тип элемента |
|
||||
| пустая коллекция | вернёт начальное | исключение |
|
||||
|
||||
`fold` умеет менять тип, и это его главное применение:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val words = listOf("раз", "два", "три")
|
||||
|
||||
val lengths: Map<String, Int> = words.fold(mutableMapOf()) { acc, w ->
|
||||
acc[w] = w.length
|
||||
acc
|
||||
}
|
||||
println(lengths) // {раз=3, два=3, три=3}
|
||||
|
||||
println(words.fold(0) { acc, w -> acc + w.length }) // 9 — List<String> -> Int
|
||||
}
|
||||
```
|
||||
|
||||
## Группировка
|
||||
```kotlin
|
||||
fun main() {
|
||||
val words = listOf("яблоко", "арбуз", "банан", "боб")
|
||||
|
||||
println(words.groupBy { it.first() })
|
||||
// {я=[яблоко], а=[арбуз], б=[банан, боб]}
|
||||
|
||||
println(words.groupBy(keySelector = { it.first() }, valueTransform = { it.length }))
|
||||
// {я=[6], а=[5], б=[5, 3]}
|
||||
|
||||
println(words.groupingBy { it.first() }.eachCount())
|
||||
// {я=1, а=1, б=2}
|
||||
|
||||
println(words.groupingBy { it.first() }.fold(0) { acc, w -> acc + w.length })
|
||||
// {я=6, а=5, б=8}
|
||||
}
|
||||
```
|
||||
`groupingBy` отличается от `groupBy` тем, что не создаёт промежуточные списки - он сразу агрегирует. Для подсчёта и суммирования по группам это и быстрее, и короче.
|
||||
|
||||
## associate
|
||||
```kotlin
|
||||
fun main() {
|
||||
val users = listOf("Иван", "Пётр", "Анна")
|
||||
|
||||
println(users.associateWith { it.length }) // {Иван=4, Пётр=4, Анна=4}
|
||||
println(users.associateBy { it.first() }) // {И=Иван, П=Пётр, А=Анна}
|
||||
println(users.associate { it.first() to it.length }) // {И=4, П=4, А=4}
|
||||
}
|
||||
```
|
||||
|
||||
| Функция | Ключ | Значение |
|
||||
|---|---|---|
|
||||
| `associateWith` | элемент | результат лямбды |
|
||||
| `associateBy` | результат лямбды | элемент |
|
||||
| `associate` | из `Pair` | из `Pair` |
|
||||
|
||||
`associateBy` против `groupBy` - важная разница:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val words = listOf("банан", "боб")
|
||||
|
||||
println(words.associateBy { it.first() }) // {б=боб} — потеря!
|
||||
println(words.groupBy { it.first() }) // {б=[банан, боб]}
|
||||
}
|
||||
```
|
||||
`associateBy` при дубликате ключа **молча перезаписывает**. Если ключ не уникален, нужен `groupBy`.
|
||||
|
||||
## Сортировка
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(3, 1, 2)
|
||||
|
||||
println(list.sorted()) // [1, 2, 3]
|
||||
println(list.sortedDescending()) // [3, 2, 1]
|
||||
println(list.reversed()) // [2, 1, 3] — не сортировка!
|
||||
println(list.shuffled().size) // 3
|
||||
|
||||
val users = listOf("Пётр" to 25, "Иван" to 30, "Анна" to 25)
|
||||
println(users.sortedBy { it.second }) // [(Пётр, 25), (Анна, 25), (Иван, 30)]
|
||||
println(users.sortedWith(compareBy({ it.second }, { it.first })))
|
||||
// [(Анна, 25), (Пётр, 25), (Иван, 30)] — по возрасту, потом по имени
|
||||
println(users.sortedWith(compareByDescending<Pair<String, Int>> { it.second }
|
||||
.thenBy { it.first }))
|
||||
// [(Иван, 30), (Анна, 25), (Пётр, 25)]
|
||||
}
|
||||
```
|
||||
Сортировка Kotlin **стабильна**: элементы с равными ключами сохраняют исходный порядок.
|
||||
|
||||
На `MutableList` есть сортировка на месте - без `ed` в имени:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = mutableListOf(3, 1, 2)
|
||||
list.sort() // на месте
|
||||
println(list) // [1, 2, 3]
|
||||
list.sortByDescending { it }
|
||||
println(list) // [3, 2, 1]
|
||||
|
||||
val copy = list.sorted() // новая коллекция
|
||||
println("$list / $copy") // [3, 2, 1] / [1, 2, 3]
|
||||
}
|
||||
```
|
||||
Соглашение в stdlib: `sort()` меняет на месте, `sorted()` возвращает копию. То же с `reverse()`/`reversed()`, `shuffle()`/`shuffled()`.
|
||||
|
||||
## Разбиение и объединение
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = listOf(1, 2, 3, 4, 5)
|
||||
|
||||
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
|
||||
println(list.chunked(2) { it.sum() }) // [3, 7, 5]
|
||||
println(list.windowed(3)) // [[1,2,3], [2,3,4], [3,4,5]]
|
||||
println(list.windowed(2, step = 2, partialWindows = true)) // [[1,2], [3,4], [5]]
|
||||
println(list.zipWithNext()) // [(1,2), (2,3), (3,4), (4,5)]
|
||||
println(list.zipWithNext { a, b -> b - a }) // [1, 1, 1, 1]
|
||||
}
|
||||
```
|
||||
`windowed` - скользящее окно: удобно для расчёта разниц, средних и сглаживания.
|
||||
|
||||
## Операции на Map
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
|
||||
|
||||
println(map.filter { (_, v) -> v > 1 }) // {b=2, c=3}
|
||||
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
|
||||
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
|
||||
println(map.filterKeys { it != "a" }) // {b=2, c=3}
|
||||
println(map.maxByOrNull { it.value }?.key) // c
|
||||
println(map.entries.sumOf { it.value }) // 6
|
||||
println(map.toList().map { (k, v) -> "$k$v" }) // [a1, b2, c3]
|
||||
}
|
||||
```
|
||||
Деструктуризация в лямбде (`{ (k, v) -> }`) работает для `Map.Entry` и для `Pair`.
|
||||
|
||||
## Практический пример
|
||||
```kotlin
|
||||
data class Sale(val region: String, val product: String, val amount: Long)
|
||||
|
||||
fun main() {
|
||||
val sales = listOf(
|
||||
Sale("Север", "хлеб", 100),
|
||||
Sale("Север", "молоко", 200),
|
||||
Sale("Юг", "хлеб", 150),
|
||||
Sale("Юг", "сыр", 300),
|
||||
)
|
||||
|
||||
val byRegion = sales.groupBy { it.region }.mapValues { (_, v) -> v.sumOf { it.amount } }
|
||||
println(byRegion) // {Север=300, Юг=450}
|
||||
|
||||
val top = sales.maxByOrNull { it.amount }
|
||||
println("${top?.product}: ${top?.amount}") // сыр: 300
|
||||
|
||||
val products = sales.map { it.product }.distinct().sorted()
|
||||
println(products) // [молоко, сыр, хлеб]
|
||||
|
||||
val report = byRegion.entries
|
||||
.sortedByDescending { it.value }
|
||||
.joinToString("\n") { "${it.key}: ${it.value}" }
|
||||
println(report) // Юг: 450\nСевер: 300
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java Stream | Kotlin |
|
||||
> |---|---|
|
||||
> | `stream().map(f).collect(toList())` | `map(f)` |
|
||||
> | `stream().filter(p).collect(toList())` | `filter(p)` |
|
||||
> | `collect(groupingBy(f))` | `groupBy(f)` |
|
||||
> | `collect(toMap(k, v))` | `associate { k to v }` |
|
||||
> | `reduce(0, Integer::sum)` | `fold(0) { a, b -> a + b }` |
|
||||
> | `mapToInt(f).sum()` | `sumOf(f)` |
|
||||
> | `flatMap(x -> x.stream())` | `flatMap { it }` |
|
||||
> | `collect(partitioningBy(p))` | `partition(p)` |
|
||||
> | `anyMatch(p)` | `any(p)` |
|
||||
> | нет аналога | `mapNotNull`, `windowed`, `zipWithNext`, `groupingBy` |
|
||||
>
|
||||
> **Главное - не нужен `.stream()` и `.collect()`.** В Java любая операция требует входа в стрим и выхода из него; это ~25 символов обвязки на каждую цепочку. В Kotlin операции объявлены прямо на `Iterable`, поэтому короткие цепочки читаются как одно выражение:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println(listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 10 }) // 50
|
||||
> }
|
||||
> ```
|
||||
> ```java
|
||||
> List.of(1, 2, 3).stream().filter(x -> x > 1).mapToInt(x -> x * 10).sum();
|
||||
> ```
|
||||
>
|
||||
> **Цена - энергичность.** Java-стрим ленив: промежуточные операции не создают коллекций. Kotlin-операции на `Iterable` создают новый список на каждом шаге. Для цепочки из 3 операций по списку из 1000 элементов это 3 списка. Ленивый аналог - [Sequence](Sequence.md), и он почти дословно соответствует `Stream`.
|
||||
>
|
||||
> **`mapNotNull` и `groupingBy` аналогов в Java не имеют.** В Java фильтрация с преобразованием - это `map` + `filter(Objects::nonNull)` или `flatMap` с `Optional.stream()`. Подсчёт по группам - `groupingBy(f, counting())`, то есть вложенные коллекторы, которые плохо читаются.
|
||||
>
|
||||
> **`Optional` в цепочках не нужен.** Java-стримы возвращают `Optional` из `findFirst` и `max`; в Kotlin это nullable-тип, с которым работают обычные `?.` и `?:`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println(listOf(1, 2, 3).maxOrNull() ?: 0) // 3
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Каждая операция создаёт новую коллекцию:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val big = List(100_000) { it }
|
||||
> val result = big.map { it * 2 }.filter { it % 3 == 0 }.take(5)
|
||||
> println(result) // [0, 6, 12, 18, 24]
|
||||
> }
|
||||
> ```
|
||||
> `map` обработал все 100 000 элементов и создал список, `filter` - ещё один, и только потом `take` взял пять. С `asSequence()` работы было бы на порядки меньше.
|
||||
>
|
||||
> **`all` на пустой коллекции - `true`:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val errors = emptyList<String>()
|
||||
> println(errors.all { it.isNotBlank() }) // true
|
||||
> println(errors.isNotEmpty() && errors.all { it.isNotBlank() }) // false
|
||||
> }
|
||||
> ```
|
||||
> Проверка «все данные валидны» на пустом входе пройдёт. Если пустота недопустима, проверяйте её отдельно.
|
||||
>
|
||||
> **`associateBy` теряет дубликаты молча** - при повторе ключа остаётся последний элемент. Для неуникальных ключей нужен `groupBy`.
|
||||
>
|
||||
> **`reduce` падает на пустой коллекции**, `fold` - нет. В коде, где коллекция может быть пустой, `reduce` - потенциальное исключение.
|
||||
>
|
||||
> **`sortedBy` с nullable-ключом ставит `null` первыми:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf("b" to 2, "a" to null, "c" to 1)
|
||||
> println(list.sortedBy { it.second }) // [(a, null), (c, 1), (b, 2)]
|
||||
> println(list.sortedWith(compareBy(nullsLast()) { it.second })) // [(c, 1), (b, 2), (a, null)]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`reversed()` - не сортировка**, а переворот. `listOf(3, 1, 2).reversed()` даёт `[2, 1, 3]`.
|
||||
>
|
||||
> **`sort()` и `sorted()` легко перепутать.** `sort()` молча меняет исходную коллекцию - если она пришла извне, это неожиданная мутация:
|
||||
> ```kotlin
|
||||
> fun process(items: MutableList<Int>): List<Int> {
|
||||
> items.sort() // испортили аргумент вызывающего
|
||||
> return items
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val original = mutableListOf(3, 1, 2)
|
||||
> process(original)
|
||||
> println(original) // [1, 2, 3] — вызывающий этого не просил
|
||||
> }
|
||||
> ```
|
||||
> Принимайте `List` вместо `MutableList` - тогда `sort()` просто не скомпилируется.
|
||||
>
|
||||
> **`first()` и `single()` бросают, а не возвращают `null`.** Версии с `OrNull` безопасны; `single` вдобавок падает, если элементов больше одного.
|
||||
>
|
||||
> **`flatMap { it }` на `List<List<T>>` работает, но `flatten()` яснее.**
|
||||
>
|
||||
> **Цепочка из `filter` после `map` делает лишнюю работу.** `filter` перед `map` обрабатывает меньше элементов:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = List(10) { it }
|
||||
> println(list.map { it * 2 }.filter { it > 10 }) // [12, 14, 16, 18], 10 преобразований
|
||||
> println(list.filter { it > 5 }.map { it * 2 }) // [12, 14, 16, 18], 4 преобразования
|
||||
> }
|
||||
> ```
|
||||
> Здесь результаты совпадают, а работы во втором случае вдвое с лишним меньше. Но совпадают они не всегда: перестановка `filter` и `map` меняет смысл, если предикат проверяет уже преобразованное значение. Сначала убедитесь в эквивалентности, потом выбирайте по стоимости.
|
||||
>
|
||||
> **`joinToString` по умолчанию без ограничения длины.** Для списка на миллион элементов он построит огромную строку; в логах используйте `limit`.
|
||||
|
||||
Далее: [Sequence](Sequence.md)
|
||||
@@ -1,545 +0,0 @@
|
||||
---
|
||||
title: Channels
|
||||
description: Передача значений между корутинами: типы буферов, закрытие, fan-out и fan-in, select и когда канал лучше Flow
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- channels
|
||||
---
|
||||
|
||||
`Channel` - очередь для передачи значений между корутинами. Если `Flow` описывает, как значения производятся, то `Channel` - это объект, в который одна корутина кладёт значения, а другая забирает.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
|
||||
launch {
|
||||
for (i in 1..3) {
|
||||
channel.send(i) // приостановится, пока никто не принял
|
||||
}
|
||||
channel.close()
|
||||
}
|
||||
|
||||
for (value in channel) { // завершится при close()
|
||||
println("получено: $value")
|
||||
}
|
||||
// получено: 1
|
||||
// получено: 2
|
||||
// получено: 3
|
||||
}
|
||||
```
|
||||
`send` и `receive` - `suspend`-функции: отправитель ждёт получателя, получатель ждёт отправителя. Это rendezvous-канал, вариант по умолчанию.
|
||||
|
||||
## Типы каналов
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// RENDEZVOUS (по умолчанию): буфера нет
|
||||
val rendezvous = Channel<Int>()
|
||||
println("rendezvous принял без получателя: ${rendezvous.trySend(1).isSuccess}")
|
||||
|
||||
// BUFFERED: буфер на n элементов
|
||||
val buffered = Channel<Int>(capacity = 2)
|
||||
println("в буфер на 2: ${buffered.trySend(1).isSuccess} ${buffered.trySend(2).isSuccess}")
|
||||
println("третий не влез: ${buffered.trySend(3).isSuccess}")
|
||||
|
||||
// CONFLATED: хранит только последнее
|
||||
val conflated = Channel<Int>(Channel.CONFLATED)
|
||||
conflated.trySend(1); conflated.trySend(2); conflated.trySend(3)
|
||||
println("conflated хранит последнее: ${conflated.receive()}")
|
||||
|
||||
// UNLIMITED: растёт без ограничений
|
||||
val unlimited = Channel<Int>(Channel.UNLIMITED)
|
||||
repeat(1000) { unlimited.trySend(it) }
|
||||
println("unlimited принял всё: ${unlimited.receive()}")
|
||||
// rendezvous принял без получателя: false
|
||||
// в буфер на 2: true true
|
||||
// третий не влез: false
|
||||
// conflated хранит последнее: 3
|
||||
// unlimited принял всё: 0
|
||||
}
|
||||
```
|
||||
| Тип | Поведение `send` при заполнении |
|
||||
|---|---|
|
||||
| `RENDEZVOUS` (0) | ждёт, пока кто-то вызовет `receive` |
|
||||
| `BUFFERED` (по умолчанию 64) | ждёт освобождения места |
|
||||
| `CONFLATED` | не ждёт, заменяет непрочитанное значение |
|
||||
| `UNLIMITED` | не ждёт никогда, растёт в памяти |
|
||||
| `capacity = n` + `onBufferOverflow` | `DROP_OLDEST` / `DROP_LATEST` вместо ожидания |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val dropping = Channel<Int>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
repeat(5) { dropping.trySend(it) }
|
||||
println("осталось последние два: ${dropping.receive()} ${dropping.receive()}")
|
||||
// осталось последние два: 3 4
|
||||
}
|
||||
```
|
||||
|
||||
## Закрытие канала
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>(capacity = 3)
|
||||
channel.send(1)
|
||||
channel.send(2)
|
||||
channel.close()
|
||||
|
||||
println("после close ещё можно читать: ${channel.receive()} ${channel.receive()}")
|
||||
println("отправка запрещена: ${channel.trySend(3).isSuccess}")
|
||||
println("закрыт для отправки: ${channel.isClosedForSend}")
|
||||
|
||||
val error = runCatching { channel.receive() }.exceptionOrNull()
|
||||
println("чтение из пустого закрытого: ${error is ClosedReceiveChannelException}")
|
||||
// после close ещё можно читать: 1 2
|
||||
// отправка запрещена: false
|
||||
// закрыт для отправки: true
|
||||
// чтение из пустого закрытого: true
|
||||
}
|
||||
```
|
||||
`close()` - это «больше значений не будет»: уже отправленные элементы дочитываются, новые не принимаются. `for`-цикл по каналу завершается нормально, а `receive()` на исчерпанном канале бросает исключение. Безопасная альтернатива - `receiveCatching`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
channel.close()
|
||||
|
||||
val result = channel.receiveCatching()
|
||||
println("успех: ${result.isSuccess}, закрыт: ${result.isClosed}")
|
||||
println("значение или null: ${result.getOrNull()}")
|
||||
// успех: false, закрыт: true
|
||||
// значение или null: null
|
||||
}
|
||||
```
|
||||
|
||||
Закрытие с ошибкой передаёт её получателю:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
launch {
|
||||
channel.send(1)
|
||||
channel.close(IllegalStateException("источник сломался"))
|
||||
}
|
||||
|
||||
val error = runCatching {
|
||||
for (v in channel) println("получено: $v")
|
||||
}.exceptionOrNull()
|
||||
println("ошибка дошла: ${error?.message}")
|
||||
// получено: 1
|
||||
// ошибка дошла: источник сломался
|
||||
}
|
||||
```
|
||||
|
||||
## produce: канал как результат корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.produceSquares(n: Int): ReceiveChannel<Int> = produce {
|
||||
for (i in 1..n) {
|
||||
delay(10)
|
||||
send(i * i)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val squares = produceSquares(4)
|
||||
println(squares.toList()) // [1, 4, 9, 16]
|
||||
}
|
||||
```
|
||||
`produce` закрывает канал автоматически при завершении тела - это его главное преимущество перед ручным `Channel()` + `launch`. Он помечен `@ObsoleteCoroutinesApi`, но остаётся самым аккуратным способом создать канал-источник.
|
||||
|
||||
## Fan-out: несколько получателей
|
||||
Канал распределяет элементы между получателями - каждый элемент достаётся **ровно одному**:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
fun main() = runBlocking {
|
||||
val tasks = Channel<Int>()
|
||||
val handled = ConcurrentHashMap<Int, Int>()
|
||||
|
||||
repeat(3) { worker ->
|
||||
launch {
|
||||
for (task in tasks) {
|
||||
delay(10)
|
||||
handled[task] = worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(9) { tasks.send(it) }
|
||||
tasks.close()
|
||||
delay(100)
|
||||
|
||||
println("обработано задач: ${handled.size}")
|
||||
println("каждая ровно раз: ${handled.keys.sorted() == (0..8).toList()}")
|
||||
println("работали несколько воркеров: ${handled.values.toSet().size > 1}")
|
||||
// обработано задач: 9
|
||||
// каждая ровно раз: true
|
||||
// работали несколько воркеров: true
|
||||
}
|
||||
```
|
||||
Это главное отличие канала от `SharedFlow`: канал **распределяет**, `SharedFlow` **транслирует** всем. Пул воркеров без канала не сделать.
|
||||
|
||||
## Fan-in: несколько отправителей
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val results = Channel<String>()
|
||||
|
||||
launch { delay(10); results.send("от первого") }
|
||||
launch { delay(20); results.send("от второго") }
|
||||
launch { delay(30); results.send("от третьего") }
|
||||
|
||||
repeat(3) { println(results.receive()) }
|
||||
results.close()
|
||||
// от первого
|
||||
// от второго
|
||||
// от третьего
|
||||
}
|
||||
```
|
||||
|
||||
## Конвейер
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
|
||||
var i = 1
|
||||
while (i <= 5) send(i++)
|
||||
}
|
||||
|
||||
fun CoroutineScope.squares(input: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
|
||||
for (n in input) send(n * n)
|
||||
}
|
||||
|
||||
fun CoroutineScope.evens(input: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
|
||||
for (n in input) if (n % 2 == 0) send(n)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val pipeline = evens(squares(numbers()))
|
||||
println(pipeline.toList()) // [4, 16]
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
```
|
||||
Каждая стадия - отдельная корутина, работающая параллельно с остальными. Для такой задачи обычно достаточно `Flow` с операторами, но каналы дают контроль над буферами каждой стадии.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["produce: 1..5"] -->|"Channel"| B["squares"]
|
||||
B -->|"Channel"| C["evens"]
|
||||
C -->|"Channel"| D["collect"]
|
||||
E["Каждая стадия —<br/>своя корутина"] -.-> B
|
||||
```
|
||||
|
||||
## Тикер и таймеры
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val ticks = mutableListOf<Int>()
|
||||
val job = launch {
|
||||
var i = 0
|
||||
while (true) {
|
||||
delay(20)
|
||||
ticks += i++
|
||||
}
|
||||
}
|
||||
delay(70)
|
||||
job.cancel()
|
||||
println("тиков примерно 3: ${ticks.size in 2..4}") // тиков примерно 3: true
|
||||
}
|
||||
```
|
||||
Для периодических задач проще цикл с `delay`, чем `ticker` (который помечен устаревшим). Если нужен именно `Flow`, его даёт `flow { while (true) { delay(x); emit(Unit) } }`.
|
||||
|
||||
## select: ждать первое из нескольких
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
fun main() = runBlocking {
|
||||
val fast = Channel<String>()
|
||||
val slow = Channel<String>()
|
||||
|
||||
launch { delay(10); fast.send("быстрый") }
|
||||
launch { delay(50); slow.send("медленный") }
|
||||
|
||||
val first = select<String> {
|
||||
fast.onReceive { "пришло из fast: $it" }
|
||||
slow.onReceive { "пришло из slow: $it" }
|
||||
}
|
||||
println(first)
|
||||
coroutineContext.cancelChildren()
|
||||
// пришло из fast: быстрый
|
||||
}
|
||||
```
|
||||
`select` работает и с `Deferred` - это способ устроить гонку запросов:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mirror1 = async { delay(50); "зеркало 1" }
|
||||
val mirror2 = async { delay(10); "зеркало 2" }
|
||||
|
||||
val winner = select<String> {
|
||||
mirror1.onAwait { it }
|
||||
mirror2.onAwait { it }
|
||||
}
|
||||
println("ответил первым: $winner")
|
||||
coroutineContext.cancelChildren()
|
||||
// ответил первым: зеркало 2
|
||||
}
|
||||
```
|
||||
Важно: проигравший `Deferred` продолжает работу - его нужно отменить явно (здесь это делает `cancelChildren`).
|
||||
|
||||
## Channel как мьютекс для состояния
|
||||
Канал-«актор» - способ изолировать изменяемое состояние в одной корутине:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
sealed interface Command {
|
||||
data class Increment(val by: Int) : Command
|
||||
data class Read(val reply: CompletableDeferred<Int>) : Command
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val commands = Channel<Command>()
|
||||
|
||||
launch {
|
||||
var state = 0 // доступно только этой корутине
|
||||
for (cmd in commands) {
|
||||
when (cmd) {
|
||||
is Command.Increment -> state += cmd.by
|
||||
is Command.Read -> cmd.reply.complete(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(100) { commands.send(Command.Increment(1)) }
|
||||
val reply = CompletableDeferred<Int>()
|
||||
commands.send(Command.Read(reply))
|
||||
println("состояние без блокировок: ${reply.await()}")
|
||||
commands.close()
|
||||
// состояние без блокировок: 100
|
||||
}
|
||||
```
|
||||
Синхронизации нет вообще: состояние изменяет одна корутина, остальные присылают сообщения. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## Channel против Flow
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// Channel: горячий, значения расходятся по получателям
|
||||
val channel = Channel<Int>(capacity = 3)
|
||||
channel.send(1); channel.send(2)
|
||||
channel.close()
|
||||
println("канал раздал: ${channel.toList()}")
|
||||
|
||||
// Flow: холодный, каждый подписчик получает всё
|
||||
val flow = flowOf(1, 2)
|
||||
println("поток повторил: ${flow.toList()} ${flow.toList()}")
|
||||
// канал раздал: [1, 2]
|
||||
// поток повторил: [1, 2] [1, 2]
|
||||
}
|
||||
```
|
||||
| | `Channel` | `Flow` |
|
||||
|---|---|---|
|
||||
| природа | горячий, объект-очередь | холодный, описание |
|
||||
| значение получает | один получатель | каждый подписчик |
|
||||
| существует без получателей | да, накапливает | нет, не запущен |
|
||||
| нужно закрывать | да | нет |
|
||||
| операторы | почти нет | десятки |
|
||||
| типичное применение | пул воркеров, развязка корутин | данные, события, UI |
|
||||
|
||||
Практическое правило: **начинайте с `Flow`**. Канал нужен, когда значения должны распределяться между получателями (пул воркеров), когда производитель и потребитель должны работать независимо, или когда нужна точная политика буферизации. Для «потока данных из репозитория в UI» канал - неправильный выбор: он одноразовый и легко теряет данные.
|
||||
|
||||
Если канал уже есть, его можно превратить во `Flow`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>(capacity = 2)
|
||||
channel.send(1); channel.send(2)
|
||||
channel.close()
|
||||
|
||||
println(channel.consumeAsFlow().toList()) // [1, 2]
|
||||
}
|
||||
```
|
||||
`consumeAsFlow()` можно собрать только один раз - канал одноразовый. Повторный `collect` бросит `IllegalStateException`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `BlockingQueue<T>` | `Channel<T>` |
|
||||
> | `ArrayBlockingQueue(n)` | `Channel(capacity = n)` |
|
||||
> | `LinkedBlockingQueue()` | `Channel(UNLIMITED)` |
|
||||
> | `SynchronousQueue` | `Channel(RENDEZVOUS)` |
|
||||
> | `queue.put(x)` (блокирует поток) | `channel.send(x)` (приостанавливает) |
|
||||
> | `queue.take()` (блокирует поток) | `channel.receive()` (приостанавливает) |
|
||||
> | «отравленная пилюля» как признак конца | `close()` |
|
||||
> | `java.util.concurrent.Flow` | `Flow` |
|
||||
>
|
||||
> **`BlockingQueue` блокирует поток, канал - нет.** Это главное различие. Пул воркеров на `BlockingQueue` требует по потоку на воркера, и 1000 воркеров - это 1000 потоков. Канал держит их как корутины:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
> import java.util.concurrent.atomic.AtomicInteger
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val tasks = Channel<Int>(capacity = 100)
|
||||
> val done = AtomicInteger(0)
|
||||
> repeat(1000) { // 1000 воркеров
|
||||
> launch { for (t in tasks) { delay(1); done.incrementAndGet() } }
|
||||
> }
|
||||
> repeat(500) { tasks.send(it) }
|
||||
> tasks.close()
|
||||
> delay(200)
|
||||
> println("обработано: ${done.get()}") // обработано: 500
|
||||
> }
|
||||
> ```
|
||||
> В Java это 1000 потоков и около гигабайта стеков.
|
||||
>
|
||||
> **Признак конца данных.** `BlockingQueue` не умеет закрываться: чтобы сообщить воркерам «задач больше нет», в очередь кладут специальное значение - «отравленную пилюлю» (`POISON_PILL`), причём по одной на каждого воркера, иначе часть из них зависнет навсегда:
|
||||
> ```java
|
||||
> for (int i = 0; i < workers; i++) queue.put(POISON_PILL);
|
||||
> ```
|
||||
> Это хрупкий паттерн: нужно знать точное число получателей, а сама пилюля ломает типизацию (обычно это `null`-объект или специальный экземпляр). `channel.close()` корректно завершает всех получателей, сколько бы их ни было, а `for (x in channel)` просто выходит из цикла.
|
||||
>
|
||||
> **Передача ошибки.** В Java исключение из производителя в очередь не положить - его оборачивают в элемент-обёртку (`Either`-подобный класс) или передают через отдельный механизм. `channel.close(exception)` доставляет исключение получателям, и оно выбрасывается в точке `receive`.
|
||||
>
|
||||
> **Таймауты и выбор из нескольких очередей.** `BlockingQueue` даёт `poll(timeout, unit)`, но ждать «первое значение из двух очередей» штатно невозможно - приходится опрашивать по очереди с короткими таймаутами. `select` в Kotlin решает это напрямую, причём без активного ожидания.
|
||||
>
|
||||
> **`java.util.concurrent.Flow` - это Reactive Streams, а не очередь.** Оно появилось в Java 9 как SPI для реактивных библиотек, с ручным управлением давлением через `Subscription.request(n)`. Ближайший аналог в Kotlin - `Flow`, а не `Channel`. Подробнее: [Flow](Flow.md)
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Незакрытый канал подвешивает получателя навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> launch {
|
||||
> channel.send(1)
|
||||
> // close() забыли
|
||||
> }
|
||||
> val job = launch {
|
||||
> for (v in channel) println("получено: $v")
|
||||
> println("этого не будет")
|
||||
> }
|
||||
> delay(50)
|
||||
> println("получатель всё ещё ждёт: ${job.isActive}")
|
||||
> coroutineContext.cancelChildren()
|
||||
> // получено: 1
|
||||
> // получатель всё ещё ждёт: true
|
||||
> }
|
||||
> ```
|
||||
> `for`-цикл по каналу ждёт `close()`. Используйте `produce`, который закрывает канал сам.
|
||||
>
|
||||
> **`receive()` на закрытом канале бросает исключение:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> channel.close()
|
||||
> val error = runCatching { channel.receive() }.exceptionOrNull()
|
||||
> println(error is ClosedReceiveChannelException) // true
|
||||
> }
|
||||
> ```
|
||||
> Для безопасного чтения - `receiveCatching()` или `for`-цикл.
|
||||
>
|
||||
> **`send` на закрытом канале тоже бросает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> channel.close()
|
||||
> val error = runCatching { channel.send(1) }.exceptionOrNull()
|
||||
> println(error is ClosedSendChannelException) // true
|
||||
> }
|
||||
> ```
|
||||
> `trySend` вместо исключения вернёт неуспешный результат.
|
||||
>
|
||||
> **`Channel.UNLIMITED` - это `OutOfMemoryError` под нагрузкой.** Он снимает обратное давление: производитель быстрее потребителя будет накапливать элементы в памяти, пока процесс не упадёт. Безлимитный канал оправдан только при известном конечном числе элементов.
|
||||
>
|
||||
> **`trySend` молча теряет данные:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>(capacity = 2)
|
||||
> val sent = (1..5).count { channel.trySend(it).isSuccess }
|
||||
> println("отправлено из 5: $sent") // отправлено из 5: 2
|
||||
> }
|
||||
> ```
|
||||
> Результат `trySend` обычно игнорируют - и три элемента исчезают без следа. Проверяйте `isSuccess` или используйте `send`.
|
||||
>
|
||||
> **Канал - одноразовый.** Повторный `consumeAsFlow().collect` или второй `toList()` после исчерпания бросит исключение либо вернёт пустой результат. Для многократного чтения нужен `Flow` или `SharedFlow`.
|
||||
>
|
||||
> **Канал раздаёт, а не транслирует.** Если нужно, чтобы **все** подписчики получили каждое событие, канал не подойдёт - элемент достаётся одному получателю. Нужен `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **`CONFLATED` теряет значения по определению.** Он хранит только последнее непрочитанное - для счётчиков и журналов это ошибка, для «актуального состояния» - норма.
|
||||
>
|
||||
> **Отмена корутины с `send` оставляет элемент непереданным:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> val sender = launch { channel.send(1) } // ждёт получателя
|
||||
> delay(20)
|
||||
> sender.cancel()
|
||||
> println("отправитель отменён: ${sender.isCancelled}")
|
||||
> println("канал пуст: ${channel.tryReceive().isFailure}")
|
||||
> // отправитель отменён: true
|
||||
> // канал пуст: true
|
||||
> }
|
||||
> ```
|
||||
> Элемент не доставлен и не сохранён. Для надёжной доставки нужен буфер или подтверждение.
|
||||
>
|
||||
> **Проигравший в `select` продолжает работать.** `select` возвращает первый результат, но остальные `Deferred` и корутины живут дальше, расходуя ресурсы. Отменяйте их явно.
|
||||
>
|
||||
> **`select` с `onSend` и несколькими каналами не атомарен между итерациями.** В цикле состояние каналов между проверками меняется - на `select` нельзя строить транзакционную логику.
|
||||
>
|
||||
> **`produce` и `actor` помечены устаревшими** (`@ObsoleteCoroutinesApi`): их API может измениться. Для новых источников данных предпочтительнее `Flow`, для состояния - `Mutex` или `StateFlow`.
|
||||
>
|
||||
> **`isClosedForReceive`/`isEmpty` - гонка по определению.** Между проверкой и действием состояние изменится. Единственный корректный способ - `receiveCatching()` или `for`-цикл, обрабатывающие закрытие атомарно.
|
||||
|
||||
Далее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
@@ -1,783 +0,0 @@
|
||||
---
|
||||
title: Flow
|
||||
description: Холодный асинхронный поток значений: построение, операторы, контекст, обработка ошибок и backpressure
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- flow
|
||||
---
|
||||
|
||||
`Flow<T>` - асинхронная последовательность значений. Если `suspend`-функция возвращает одно значение, то `Flow` - много, причём каждое может приходить с задержкой.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun temperatures(): Flow<Int> = flow {
|
||||
for (t in listOf(20, 22, 25)) {
|
||||
delay(20) // внутри flow можно приостанавливаться
|
||||
emit(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
temperatures().collect { println("температура: $it") }
|
||||
// температура: 20
|
||||
// температура: 22
|
||||
// температура: 25
|
||||
}
|
||||
```
|
||||
Сравните с `List`: список нужно вычислить целиком, прежде чем вернуть. `Flow` отдаёт значения по мере готовности.
|
||||
|
||||
## Холодный поток
|
||||
`Flow` ничего не делает, пока его не начнут собирать. Каждый вызов `collect` запускает тело заново.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var starts = 0
|
||||
val f = flow {
|
||||
starts++
|
||||
emit(1)
|
||||
emit(2)
|
||||
}
|
||||
|
||||
println("создан, запусков: $starts") // создан, запусков: 0
|
||||
f.collect { }
|
||||
f.collect { }
|
||||
println("после двух collect: $starts") // после двух collect: 2
|
||||
}
|
||||
```
|
||||
Это главное свойство: `Flow` - **описание** вычисления, а не запущенный процесс. Горячие потоки, которые работают независимо от подписчиков, - это `StateFlow` и `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
|
||||
## Способы создать Flow
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3).collect { print("$it ") }
|
||||
println() // 1 2 3
|
||||
|
||||
listOf("a", "b").asFlow().collect { print("$it ") }
|
||||
println() // a b
|
||||
|
||||
(1..3).asFlow().collect { print("$it ") }
|
||||
println() // 1 2 3
|
||||
|
||||
flow {
|
||||
emit("из билдера")
|
||||
emitAll(flowOf("и", "вложенного"))
|
||||
}.collect { print("$it ") }
|
||||
println() // из билдера и вложенного
|
||||
}
|
||||
```
|
||||
Для колбэчных источников есть `callbackFlow` - он же мост от слушателей к `Flow`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class Sensor {
|
||||
private var listener: ((Int) -> Unit)? = null
|
||||
fun subscribe(l: (Int) -> Unit) { listener = l }
|
||||
fun unsubscribe() { listener = null; println("слушатель снят") }
|
||||
fun fire(value: Int) = listener?.invoke(value)
|
||||
}
|
||||
|
||||
fun Sensor.readings(): Flow<Int> = callbackFlow {
|
||||
subscribe { value -> trySend(value) }
|
||||
awaitClose { unsubscribe() } // вызовется при отмене collect
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sensor = Sensor()
|
||||
val job = launch {
|
||||
sensor.readings().collect { println("показание: $it") }
|
||||
}
|
||||
delay(20)
|
||||
sensor.fire(42)
|
||||
sensor.fire(43)
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
// показание: 42
|
||||
// показание: 43
|
||||
// слушатель снят
|
||||
}
|
||||
```
|
||||
`awaitClose` обязателен: без него `callbackFlow` бросит `IllegalStateException`, потому что иначе слушатель остался бы висеть.
|
||||
|
||||
## Промежуточные операторы
|
||||
Операторы ленивые: они только оборачивают поток, не запуская его.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = (1..10).asFlow()
|
||||
.filter { it % 2 == 0 }
|
||||
.map { it * it }
|
||||
.take(3)
|
||||
.toList()
|
||||
println(result) // [4, 16, 36]
|
||||
}
|
||||
```
|
||||
Главное отличие от операторов коллекций - внутри можно приостанавливаться:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
suspend fun enrich(id: Int): String { delay(10); return "запись $id" }
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3)
|
||||
.map { enrich(it) } // suspend внутри map
|
||||
.collect { println(it) }
|
||||
// запись 1
|
||||
// запись 2
|
||||
// запись 3
|
||||
}
|
||||
```
|
||||
|
||||
| Оператор | Что делает |
|
||||
|---|---|
|
||||
| `map`, `filter` | как у коллекций, но с `suspend` внутри |
|
||||
| `transform` | произвольное число `emit` на элемент |
|
||||
| `take`, `drop`, `takeWhile` | ограничение по количеству |
|
||||
| `onEach` | побочный эффект без изменения значений |
|
||||
| `withIndex`, `runningFold`, `scan` | накопление |
|
||||
| `distinctUntilChanged` | пропуск подряд идущих дублей |
|
||||
| `debounce`, `sample` | фильтрация по времени |
|
||||
| `flatMapConcat`, `flatMapMerge`, `flatMapLatest` | поток из потоков |
|
||||
| `zip`, `combine` | объединение двух потоков |
|
||||
| `onStart`, `onCompletion`, `catch` | обработка жизненного цикла |
|
||||
|
||||
`transform` - самый общий:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3)
|
||||
.transform { value ->
|
||||
emit("начало $value")
|
||||
emit("конец $value")
|
||||
}
|
||||
.collect { print("$it; ") }
|
||||
println()
|
||||
// начало 1; конец 1; начало 2; конец 2; начало 3; конец 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Терминальные операторы
|
||||
Только они запускают поток, и все они `suspend`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val f = flowOf(1, 2, 3, 4)
|
||||
|
||||
println(f.toList()) // [1, 2, 3, 4]
|
||||
println(f.first()) // 1
|
||||
println(f.firstOrNull { it > 10 }) // null
|
||||
println(f.lastOrNull()) // 4
|
||||
println(f.count()) // 4
|
||||
println(f.reduce { a, b -> a + b }) // 10
|
||||
println(f.fold(100) { a, b -> a + b }) // 110
|
||||
println(f.toSet()) // [1, 2, 3, 4]
|
||||
println(f.filter { it == 2 }.single()) // 2
|
||||
}
|
||||
```
|
||||
`first()` отменяет поток сразу после первого значения - этого достаточно, чтобы остановить бесконечный источник:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var emitted = 0
|
||||
val infinite = flow {
|
||||
var i = 0
|
||||
while (true) {
|
||||
emitted++
|
||||
emit(i++)
|
||||
}
|
||||
}
|
||||
println(infinite.first()) // 0
|
||||
println("излучено значений: $emitted") // излучено значений: 1
|
||||
}
|
||||
```
|
||||
|
||||
## Контекст и flowOn
|
||||
По умолчанию `Flow` выполняется в контексте того, кто его собирает - это называется context preservation:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
println("flow на: ${Thread.currentThread().name}")
|
||||
emit(1)
|
||||
}.collect {
|
||||
println("collect на: ${Thread.currentThread().name}")
|
||||
}
|
||||
// flow на: main
|
||||
// collect на: main
|
||||
}
|
||||
```
|
||||
Сменить контекст **производителя** можно только через `flowOn`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
val onPool = Thread.currentThread().name.contains("DefaultDispatcher")
|
||||
println("flow в пуле: $onPool")
|
||||
emit(1)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect {
|
||||
println("collect на: ${Thread.currentThread().name}")
|
||||
}
|
||||
// flow в пуле: true
|
||||
// collect на: main
|
||||
}
|
||||
```
|
||||
`flowOn` действует на всё, что **выше** него по цепочке:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.map { it * 10 } // в IO
|
||||
.flowOn(Dispatchers.IO)
|
||||
.map { it + 1 } // в main
|
||||
.collect { print("$it ") }
|
||||
println() // 11 21
|
||||
}
|
||||
```
|
||||
Менять контекст внутри `flow { }` через `withContext` запрещено:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val broken = flow {
|
||||
withContext(Dispatchers.IO) { emit(1) } // нарушение контракта
|
||||
}
|
||||
val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
println(error is IllegalStateException) // true
|
||||
println(error?.message?.contains("Flow invariant is violated")) // true
|
||||
}
|
||||
```
|
||||
Причина запрета - предсказуемость: собирающий код должен быть уверен, что `collect` вызывается в его контексте (например, в UI-потоке). Для смены контекста есть только `flowOn`.
|
||||
|
||||
## Обработка ошибок
|
||||
`try/catch` работает, но правильнее оператор `catch`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
emit(1)
|
||||
throw IllegalStateException("источник упал")
|
||||
}
|
||||
.catch { e -> println("перехвачено: ${e.message}") }
|
||||
.collect { println("значение: $it") }
|
||||
// значение: 1
|
||||
// перехвачено: источник упал
|
||||
}
|
||||
```
|
||||
`catch` ловит ошибки только **выше** себя - это называется прозрачность исключений:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val error = runCatching {
|
||||
flowOf(1)
|
||||
.catch { println("не сработает") }
|
||||
.collect { throw IllegalStateException("ошибка в collect") }
|
||||
}.exceptionOrNull()
|
||||
println("ошибка collect прошла мимо catch: ${error?.message}")
|
||||
// ошибка collect прошла мимо catch: ошибка в collect
|
||||
}
|
||||
```
|
||||
Чтобы ловить и ошибки потребителя, нужен `onEach` + `catch` в таком порядке:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.onEach { if (it == 2) error("обработка упала на $it") }
|
||||
.catch { e -> println("перехвачено: ${e.message}") }
|
||||
.collect { println("обработано: $it") }
|
||||
// обработано: 1
|
||||
// перехвачено: обработка упала на 2
|
||||
}
|
||||
```
|
||||
`catch` может подменить поток значениями по умолчанию:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow<String> { error("сеть недоступна") }
|
||||
.catch { emit("данные из кеша") }
|
||||
.collect { println(it) }
|
||||
// данные из кеша
|
||||
}
|
||||
```
|
||||
|
||||
## retry и повторные попытки
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var attempt = 0
|
||||
flow {
|
||||
attempt++
|
||||
if (attempt < 3) error("попытка $attempt не удалась")
|
||||
emit("успех на попытке $attempt")
|
||||
}
|
||||
.retry(3) { e ->
|
||||
println("ретрай после: ${e.message}")
|
||||
delay(10)
|
||||
true
|
||||
}
|
||||
.collect { println(it) }
|
||||
// ретрай после: попытка 1 не удалась
|
||||
// ретрай после: попытка 2 не удалась
|
||||
// успех на попытке 3
|
||||
}
|
||||
```
|
||||
`retryWhen` даёт доступ к номеру попытки - это нужно для экспоненциальной задержки:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var calls = 0
|
||||
flow {
|
||||
calls++
|
||||
if (calls < 3) error("сбой")
|
||||
emit("готово")
|
||||
}
|
||||
.retryWhen { _, attempt ->
|
||||
if (attempt >= 3) false
|
||||
else {
|
||||
delay(10L * (attempt + 1)) // 10, 20, 30 мс
|
||||
println("попытка ${attempt + 1}")
|
||||
true
|
||||
}
|
||||
}
|
||||
.collect { println(it) }
|
||||
// попытка 1
|
||||
// попытка 2
|
||||
// готово
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл: onStart, onCompletion
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.onStart { println("начали") }
|
||||
.onEach { println("значение $it") }
|
||||
.onCompletion { cause -> println("завершили, ошибка: $cause") }
|
||||
.collect { }
|
||||
// начали
|
||||
// значение 1
|
||||
// значение 2
|
||||
// завершили, ошибка: null
|
||||
}
|
||||
```
|
||||
`onCompletion` вызывается всегда - при успехе, ошибке и отмене, причём получает причину:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
emit(1)
|
||||
error("упало")
|
||||
}
|
||||
.onCompletion { cause -> println("причина: ${cause?.message}") }
|
||||
.catch { }
|
||||
.collect { }
|
||||
// причина: упало
|
||||
}
|
||||
```
|
||||
`onStart` может излучать значения - это способ показать «загрузка...» до первого реального элемента:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow { delay(20); emit("данные") }
|
||||
.onStart { emit("загрузка...") }
|
||||
.collect { println(it) }
|
||||
// загрузка...
|
||||
// данные
|
||||
}
|
||||
```
|
||||
|
||||
## buffer: производитель и потребитель параллельно
|
||||
По умолчанию производитель ждёт потребителя:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun source() = flow {
|
||||
repeat(3) {
|
||||
delay(20) // производство
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
source().collect { delay(20) } // потребление
|
||||
}
|
||||
val buffered = measureTimeMillis {
|
||||
source().buffer().collect { delay(20) }
|
||||
}
|
||||
println("без буфера около 120 мс: ${sequential >= 110}")
|
||||
println("с буфером быстрее: ${buffered < sequential}")
|
||||
// без буфера около 120 мс: true
|
||||
// с буфером быстрее: true
|
||||
}
|
||||
```
|
||||
Без буфера шаги складываются: 3 × (20 + 20) = 120 мс. С буфером производство идёт параллельно с потреблением, и общее время приближается к максимуму из двух конвейеров.
|
||||
|
||||
## conflate и collectLatest
|
||||
Когда важны только свежие значения, промежуточные можно отбрасывать:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun fast() = flow {
|
||||
repeat(5) {
|
||||
delay(10)
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val all = mutableListOf<Int>()
|
||||
fast().collect { delay(40); all += it }
|
||||
|
||||
val conflated = mutableListOf<Int>()
|
||||
fast().conflate().collect { delay(40); conflated += it }
|
||||
|
||||
println("без conflate получено всё: ${all.size == 5}")
|
||||
println("с conflate пропущены промежуточные: ${conflated.size < 5}")
|
||||
println("последнее значение всегда доходит: ${conflated.last() == 4}")
|
||||
// без conflate получено всё: true
|
||||
// с conflate пропущены промежуточные: true
|
||||
// последнее значение всегда доходит: true
|
||||
}
|
||||
```
|
||||
`collectLatest` работает иначе: он не пропускает значения, а **отменяет** обработку предыдущего:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val completed = mutableListOf<Int>()
|
||||
flow {
|
||||
repeat(3) { delay(10); emit(it) }
|
||||
}.collectLatest { value ->
|
||||
delay(25) // не успеет для первых двух
|
||||
completed += value
|
||||
}
|
||||
println("доработал только последний: $completed") // доработал только последний: [2]
|
||||
}
|
||||
```
|
||||
|
||||
| Стратегия | Поведение при быстром производителе |
|
||||
|---|---|
|
||||
| по умолчанию | производитель ждёт потребителя |
|
||||
| `buffer(n)` | очередь на `n` элементов, затем ожидание |
|
||||
| `buffer(onBufferOverflow = DROP_OLDEST)` | выбрасывает старые |
|
||||
| `conflate()` | хранит только последнее непрочитанное |
|
||||
| `collectLatest` | отменяет обработку устаревшего значения |
|
||||
| `debounce(ms)` | пропускает только после паузы |
|
||||
| `sample(ms)` | берёт последнее раз в интервал |
|
||||
|
||||
## Объединение потоков
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val letters = flowOf("a", "b", "c")
|
||||
val numbers = flowOf(1, 2, 3)
|
||||
|
||||
println(letters.zip(numbers) { l, n -> "$l$n" }.toList()) // [a1, b2, c3]
|
||||
|
||||
val slow = flow { emit(1); delay(30); emit(2) }
|
||||
val fast2 = flow { emit("x"); delay(10); emit("y") }
|
||||
println(slow.combine(fast2) { a, b -> "$a$b" }.toList())
|
||||
// [1x, 1y, 2y]
|
||||
}
|
||||
```
|
||||
`zip` ждёт пару - элементов будет столько, сколько в коротком потоке. `combine` излучает при каждом изменении любого из потоков, используя последние известные значения другого. Для UI (например, «форма валидна, если валидны все поля») нужен `combine`.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val concatenated = flowOf(1, 2).flatMapConcat { n ->
|
||||
flow { emit("$n-a"); delay(10); emit("$n-b") }
|
||||
}.toList()
|
||||
println(concatenated) // [1-a, 1-b, 2-a, 2-b]
|
||||
|
||||
val latest = flowOf(1, 2, 3).flatMapLatest { n ->
|
||||
flow { delay(20); emit("результат $n") }
|
||||
}.toList()
|
||||
println(latest) // [результат 3]
|
||||
}
|
||||
```
|
||||
`flatMapLatest` - основной оператор для поиска по вводу: новый запрос отменяет предыдущий.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
suspend fun search(query: String): List<String> {
|
||||
delay(20)
|
||||
return listOf("результат для «$query»")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val input = flow {
|
||||
emit("к"); delay(5)
|
||||
emit("ко"); delay(5)
|
||||
emit("кот"); delay(100) // пользователь остановился
|
||||
emit("кота")
|
||||
}
|
||||
|
||||
input
|
||||
.debounce(30) // ждём паузу в наборе
|
||||
.filter { it.length >= 2 }
|
||||
.flatMapLatest { search(it).asFlow() }
|
||||
.collect { println(it) }
|
||||
// результат для «кот»
|
||||
// результат для «кота»
|
||||
}
|
||||
```
|
||||
Это каноничный конвейер: `debounce` отсекает промежуточные нажатия, `flatMapLatest` отменяет устаревший запрос.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["ввод: к, ко, кот"] --> B["debounce(30)"]
|
||||
B --> C["только 'кот'<br/>после паузы"]
|
||||
C --> D["flatMapLatest"]
|
||||
D --> E["запрос к API"]
|
||||
E --> F["collect: результаты"]
|
||||
D -.->|"пришёл новый ввод"| G["отмена<br/>предыдущего запроса"]
|
||||
```
|
||||
|
||||
## Flow как возвращаемый тип
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class OrderRepository {
|
||||
// не suspend: Flow холодный, вызов ничего не запускает
|
||||
fun observeOrders(): Flow<String> = flow {
|
||||
repeat(3) { i ->
|
||||
delay(10)
|
||||
emit("заказ $i")
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // производитель всегда в IO
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
OrderRepository().observeOrders().collect { println(it) }
|
||||
// заказ 0
|
||||
// заказ 1
|
||||
// заказ 2
|
||||
}
|
||||
```
|
||||
Правила проектирования: функция, возвращающая `Flow`, не должна быть `suspend` (создание потока мгновенно), а `flowOn` ставится внутри - вызывающий не обязан знать, где работает источник.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Stream<T>` (синхронный, одноразовый) | `Sequence<T>` |
|
||||
> | `Flow` из Reactive Streams | `Flow<T>` |
|
||||
> | RxJava `Observable`/`Flowable` | `Flow<T>` |
|
||||
> | Project Reactor `Flux<T>` | `Flow<T>` |
|
||||
> | `Subscriber.request(n)` | приостановка `emit` |
|
||||
> | `subscribeOn`/`observeOn` | `flowOn` |
|
||||
> | `onErrorResume` | `catch` |
|
||||
> | `BlockingQueue` + поток | `Channel` |
|
||||
>
|
||||
> **`java.util.stream.Stream` - это не аналог.** `Stream` синхронный: `stream.map(f)` не может ждать ответа сети, а терминальная операция блокирует поток до конца обработки. Прямой аналог `Stream` в Kotlin - `Sequence`. `Flow` отличается тем, что внутри каждого оператора разрешена приостановка. Подробнее: [Sequence](../Коллекции/Sequence.md)
|
||||
>
|
||||
> **Reactive Streams решают ту же задачу, но сложнее.** RxJava и Reactor построены на коллбэках (`onNext`, `onError`, `onComplete`) и требуют явного управления давлением через `request(n)`. В `Flow` backpressure получается бесплатно: `emit` - это `suspend`-функция, поэтому если потребитель не готов, производитель просто приостанавливается:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var produced = 0
|
||||
> flow {
|
||||
> repeat(100) { produced++; emit(it) }
|
||||
> }.take(3).collect { }
|
||||
> println("произведено ровно столько, сколько взяли: $produced")
|
||||
> // произведено ровно столько, сколько взяли: 3
|
||||
> }
|
||||
> ```
|
||||
> Никакого `request(n)` - producer сам останавливается на `emit`.
|
||||
>
|
||||
> **Стектрейсы и отладка.** В RxJava стектрейс ошибки состоит из кадров библиотеки, а место в бизнес-логике приходится искать по `onAssembly`-хукам. `Flow` - обычные `suspend`-функции, поэтому исключение приходит с кадрами вашего кода, а `try/catch`, `for` и `return` работают внутри операторов естественно.
|
||||
>
|
||||
> **Количество операторов.** В RxJava их несколько сотен (`flatMapIterable`, `concatMapDelayError`, `switchIfEmpty`...), потому что каждый сценарий требует отдельного оператора - внутри цепочки нельзя писать обычный код. В `Flow` операторов несколько десятков: остальное выражается через `transform` и обычные конструкции языка.
|
||||
>
|
||||
> **Отмена подписки.** В Rx нужно хранить `Disposable` и не забыть вызвать `dispose()` - классический источник утечек, для борьбы с которым придумали `CompositeDisposable`. `Flow` собирается внутри корутины, поэтому отмена области автоматически прекращает сбор:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> flow {
|
||||
> try {
|
||||
> repeat(100) { delay(10); emit(it) }
|
||||
> } finally { println("источник закрыт") }
|
||||
> }.collect { }
|
||||
> }
|
||||
> delay(30)
|
||||
> job.cancelAndJoin()
|
||||
> // источник закрыт
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Context preservation - то, чего нет в Rx.** В RxJava `observeOn` можно поставить в любом месте, и понять, на каком потоке выполнится `subscribe`, можно только прочитав всю цепочку. `Flow` гарантирует, что `collect` всегда выполняется в контексте собирающего, а `withContext` внутри `flow { }` запрещён на уровне рантайма.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Flow` не запустится без терминального оператора:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var side = 0
|
||||
> flowOf(1, 2, 3).onEach { side++ } // collect не вызван
|
||||
> delay(20)
|
||||
> println("побочных эффектов: $side") // побочных эффектов: 0
|
||||
> }
|
||||
> ```
|
||||
> Компилятор предупреждает о неиспользованном результате, но не всегда - например, если значение присвоено переменной.
|
||||
>
|
||||
> **`withContext` внутри `flow { }` бросает исключение в рантайме:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val error = runCatching {
|
||||
> flow { withContext(Dispatchers.IO) { emit(1) } }.collect { }
|
||||
> }.exceptionOrNull()
|
||||
> println(error?.message?.contains("Flow invariant is violated")) // true
|
||||
> }
|
||||
> ```
|
||||
> Это ошибка времени выполнения, а не компиляции - её легко пропустить в редкой ветке кода. Используйте `flowOn`.
|
||||
>
|
||||
> **`flowOn` действует только выше по цепочке.** `flow.map { a }.flowOn(IO).map { b }` выполнит `a` в IO, а `b` - в контексте сборщика. Порядок операторов меняет смысл.
|
||||
>
|
||||
> **`catch` не ловит ошибки `collect`:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val error = runCatching {
|
||||
> flowOf(1).catch { println("не вызовется") }.collect { error("из collect") }
|
||||
> }.exceptionOrNull()
|
||||
> println("ошибка прошла мимо: ${error?.message}") // ошибка прошла мимо: из collect
|
||||
> }
|
||||
> ```
|
||||
> Перенесите логику в `onEach` перед `catch`.
|
||||
>
|
||||
> **`emit` из другой корутины запрещён:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val broken = flow {
|
||||
> coroutineScope {
|
||||
> launch { emit(1) } // emit в другой корутине
|
||||
> }
|
||||
> }
|
||||
> val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
> println(error != null) // true
|
||||
> }
|
||||
> ```
|
||||
> Сообщение - про нарушение инварианта. Для излучения из нескольких источников нужен `channelFlow` или `callbackFlow`.
|
||||
>
|
||||
> **`callbackFlow` без `awaitClose` падает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val broken = callbackFlow<Int> {
|
||||
> trySend(1)
|
||||
> // нет awaitClose
|
||||
> }
|
||||
> val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
> println(error is IllegalStateException) // true
|
||||
> }
|
||||
> ```
|
||||
> Это защита от утечки слушателя: без `awaitClose` библиотека не знает, когда отписываться.
|
||||
>
|
||||
> **`trySend` молча теряет значения при полном буфере.** У `callbackFlow` буфер по умолчанию 64 элемента; `trySend` возвращает `ChannelResult`, который обычно игнорируют. Если источник быстрее потребителя, часть событий исчезнет - проверяйте результат или задавайте `buffer(Channel.UNLIMITED)` осознанно.
|
||||
>
|
||||
> **Повторный `collect` запускает поток заново.** Если `Flow` делает сетевой запрос, два `collect` - это два запроса. Для разделения результата между подписчиками нужен `shareIn`/`stateIn`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **`Flow` без буфера последователен, а не параллелен:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> flowOf(1, 2, 3).map { delay(30); it }.collect { }
|
||||
> }
|
||||
> println("последовательно, около 90 мс: ${t >= 85}") // последовательно, около 90 мс: true
|
||||
> }
|
||||
> ```
|
||||
> `map` с `suspend` внутри обрабатывает элементы по одному. Для параллельной обработки нужен `flatMapMerge` с `concurrency` или `map { async { } }` поверх списка.
|
||||
>
|
||||
> **`conflate` и `collectLatest` теряют данные - это их цель.** Для UI-состояния это правильно, для обработки платежей или записи в журнал - нет. Убедитесь, что потеря промежуточных значений допустима.
|
||||
>
|
||||
> **`debounce` задерживает даже последнее значение.** Пользователь перестал печатать - результат появится только через указанный интервал. Слишком большое значение ощущается как лаг интерфейса.
|
||||
>
|
||||
> **Бесконечный `Flow` в `toList()` повиснет.** Терминальные операторы, собирающие всё (`toList`, `count`, `fold`), на бесконечном источнике не завершатся никогда. Нужен `take(n)` или `first()`.
|
||||
>
|
||||
> **`SharingStarted.Eagerly` в `stateIn` держит источник активным всегда** - типичная утечка при переносе холодного `Flow` в горячий. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **Исключение в `onCompletion` не перехватывается предыдущим `catch`.** `onCompletion` - не `finally`: он не может подавить ошибку, а своя ошибка внутри него уйдёт потребителю.
|
||||
>
|
||||
> **`first()` отменяет источник.** Для `callbackFlow` это значит, что слушатель будет снят сразу после первого значения - если вы ожидали продолжения подписки, её нет.
|
||||
|
||||
Далее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
@@ -1,618 +0,0 @@
|
||||
---
|
||||
title: StateFlow и SharedFlow
|
||||
description: Горячие потоки: хранение состояния, события, replay, stateIn и shareIn, конфляция и выбор между ними
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- flow
|
||||
---
|
||||
|
||||
`StateFlow` и `SharedFlow` - **горячие** потоки: они существуют и работают независимо от подписчиков. Обычный `Flow` холодный - он запускается на каждый `collect` и умирает вместе с ним.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
|
||||
val job = launch {
|
||||
state.collect { println("подписчик: $it") }
|
||||
}
|
||||
delay(20)
|
||||
state.value = 1
|
||||
state.value = 2
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println("текущее значение без подписчиков: ${state.value}")
|
||||
// подписчик: 0
|
||||
// подписчик: 1
|
||||
// подписчик: 2
|
||||
// текущее значение без подписчиков: 2
|
||||
}
|
||||
```
|
||||
Обратите внимание: подписчик сразу получил текущее значение `0`, а после отмены сбора состояние осталось доступным через `.value`.
|
||||
|
||||
## StateFlow: состояние
|
||||
`StateFlow` - это наблюдаемое значение. У него всегда есть текущее состояние, и оно всегда одно.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class Counter {
|
||||
private val _count = MutableStateFlow(0)
|
||||
val count: StateFlow<Int> = _count.asStateFlow() // только чтение снаружи
|
||||
|
||||
fun increment() { _count.value++ }
|
||||
fun add(n: Int) = _count.update { it + n } // атомарно
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val counter = Counter()
|
||||
val job = launch { counter.count.collect { println("счёт: $it") } }
|
||||
|
||||
delay(20)
|
||||
counter.increment()
|
||||
delay(20)
|
||||
counter.add(10)
|
||||
delay(20)
|
||||
job.cancel()
|
||||
// счёт: 0
|
||||
// счёт: 1
|
||||
// счёт: 11
|
||||
}
|
||||
```
|
||||
Пара «приватный `MutableStateFlow` + публичный `StateFlow`» - основной шаблон: изменять состояние может только владелец, читать - все.
|
||||
|
||||
## StateFlow конфлюирует и дедуплицирует
|
||||
Два свойства, которые нужно знать наизусть.
|
||||
|
||||
**1. Одинаковые подряд значения не доходят** - `StateFlow` сравнивает через `equals`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow("a")
|
||||
val received = mutableListOf<String>()
|
||||
val job = launch { state.collect { received += it } }
|
||||
|
||||
delay(20)
|
||||
state.value = "a"; delay(20) // то же значение — не придёт
|
||||
state.value = "b"; delay(20)
|
||||
state.value = "b"; delay(20) // снова дубль
|
||||
state.value = "a"; delay(20)
|
||||
job.cancel()
|
||||
println(received) // [a, b, a]
|
||||
}
|
||||
```
|
||||
Задержки между присваиваниями здесь не для красоты: без них подписчик не успел бы обработать значения и сработало бы второе свойство - конфляция (см. ниже), из-за которой он увидел бы только `a` и финальное значение.
|
||||
|
||||
Это бесплатный `distinctUntilChanged`, но он же источник ошибок с изменяемыми объектами:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class MutableBox(val items: MutableList<Int>)
|
||||
|
||||
fun main() = runBlocking {
|
||||
val box = MutableBox(mutableListOf(1))
|
||||
val state = MutableStateFlow(box)
|
||||
var updates = 0
|
||||
val job = launch { state.collect { updates++ } }
|
||||
|
||||
delay(20)
|
||||
box.items.add(2) // изменили содержимое
|
||||
state.value = box // тот же объект — equals true
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println("обновлений: $updates") // обновлений: 1
|
||||
}
|
||||
```
|
||||
Подписчик не узнал об изменении. Поэтому состояние в `StateFlow` должно быть неизменяемым - `data class` с `val` и неизменяемыми коллекциями. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
**2. Медленный подписчик пропускает промежуточные значения:**
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch {
|
||||
state.collect { delay(50); received += it }
|
||||
}
|
||||
delay(10)
|
||||
repeat(10) { state.value = it + 1 }
|
||||
delay(150)
|
||||
job.cancel()
|
||||
println("получено не всё: ${received.size < 10}")
|
||||
println("последнее значение дошло: ${received.last() == 10}")
|
||||
// получено не всё: true
|
||||
// последнее значение дошло: true
|
||||
}
|
||||
```
|
||||
`StateFlow` - про актуальное состояние, а не про историю. Последнее значение дойдёт всегда, промежуточные - нет.
|
||||
|
||||
## MutableStateFlow.update
|
||||
Присваивание `value` не атомарно при конкуренции:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val unsafe = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(100) { unsafe.value = unsafe.value + 1 } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("потеряны обновления: ${unsafe.value < 10_000}")
|
||||
|
||||
val safe = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(100) { safe.update { v -> v + 1 } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("update атомарен: ${safe.value == 10_000}")
|
||||
// потеряны обновления: true
|
||||
// update атомарен: true
|
||||
}
|
||||
```
|
||||
`update` выполняет CAS-цикл, поэтому при конкурентных изменениях нужен именно он. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## SharedFlow: события
|
||||
`SharedFlow` не хранит «текущее значение» - он транслирует события всем подписчикам.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<String>()
|
||||
|
||||
val a = launch { events.collect { println("A: $it") } }
|
||||
val b = launch { events.collect { println("B: $it") } }
|
||||
delay(20) // дать подписчикам стартовать
|
||||
|
||||
events.emit("событие 1")
|
||||
delay(20)
|
||||
a.cancel(); b.cancel()
|
||||
// A: событие 1
|
||||
// B: событие 1
|
||||
}
|
||||
```
|
||||
По умолчанию `replay = 0`: подписчик получает только то, что произошло **после** подписки. Событие, отправленное без подписчиков, исчезает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<String>()
|
||||
events.emit("в пустоту") // подписчиков нет
|
||||
|
||||
val received = mutableListOf<String>()
|
||||
val job = launch { events.collect { received += it } }
|
||||
delay(20)
|
||||
events.emit("услышано")
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println(received) // [услышано]
|
||||
}
|
||||
```
|
||||
|
||||
## replay
|
||||
`replay` задаёт, сколько последних событий получит новый подписчик:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<Int>(replay = 2)
|
||||
events.emit(1)
|
||||
events.emit(2)
|
||||
events.emit(3)
|
||||
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch { events.collect { received += it } }
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println(received) // [2, 3]
|
||||
}
|
||||
```
|
||||
`StateFlow` эквивалентен `SharedFlow(replay = 1)` с дедупликацией и обязательным начальным значением.
|
||||
|
||||
## Буфер и стратегия переполнения
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val dropping = MutableSharedFlow<Int>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 2,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch { dropping.collect { delay(50); received += it } }
|
||||
delay(20)
|
||||
|
||||
repeat(10) { dropping.emit(it) } // emit не приостанавливается
|
||||
delay(200)
|
||||
job.cancel()
|
||||
println("часть событий отброшена: ${received.size < 10}") // часть событий отброшена: true
|
||||
}
|
||||
```
|
||||
| Параметр | Смысл |
|
||||
|---|---|
|
||||
| `replay` | сколько событий повторить новому подписчику |
|
||||
| `extraBufferCapacity` | буфер сверх `replay` |
|
||||
| `onBufferOverflow` | `SUSPEND` (по умолчанию), `DROP_OLDEST`, `DROP_LATEST` |
|
||||
|
||||
Важно: `DROP_OLDEST`/`DROP_LATEST` работают только при ненулевом буфере (`replay + extraBufferCapacity > 0`).
|
||||
|
||||
`tryEmit` - неприостанавливающаяся отправка:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val noBuffer = MutableSharedFlow<Int>()
|
||||
println("без буфера и подписчиков: ${noBuffer.tryEmit(1)}") // без буфера и подписчиков: true
|
||||
|
||||
val withSubscriber = MutableSharedFlow<Int>()
|
||||
val job = launch { withSubscriber.collect { delay(1000) } }
|
||||
delay(20)
|
||||
println("подписчик занят, буфера нет: ${withSubscriber.tryEmit(2)}") // подписчик занят, буфера нет: false
|
||||
job.cancel()
|
||||
}
|
||||
```
|
||||
Тонкость, которая удивляет: без подписчиков `tryEmit` возвращает `true`, хотя событие никто не получил - отправка «удалась», получателей просто не было.
|
||||
|
||||
## stateIn и shareIn
|
||||
Превращают холодный `Flow` в горячий:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var starts = 0
|
||||
val cold = flow {
|
||||
starts++
|
||||
repeat(3) { delay(20); emit(it) }
|
||||
}
|
||||
|
||||
val hot = cold.stateIn(
|
||||
scope = this,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = -1,
|
||||
)
|
||||
delay(30)
|
||||
|
||||
val a = launch { hot.take(1).collect { println("A видит: $it") } }
|
||||
val b = launch { hot.take(1).collect { println("B видит: $it") } }
|
||||
a.join(); b.join()
|
||||
println("источник запущен раз: $starts")
|
||||
// A видит: 0
|
||||
// B видит: 0
|
||||
// источник запущен раз: 1
|
||||
}
|
||||
```
|
||||
Без `stateIn` два подписчика запустили бы источник дважды - два сетевых запроса вместо одного.
|
||||
|
||||
`SharingStarted` определяет жизненный цикл источника:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var active = 0
|
||||
val source = flow {
|
||||
active++
|
||||
try {
|
||||
repeat(100) { delay(10); emit(it) }
|
||||
} finally { active-- }
|
||||
}
|
||||
|
||||
val shared = source.shareIn(
|
||||
scope = this,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
replay = 1,
|
||||
)
|
||||
|
||||
val job = launch { shared.collect { } }
|
||||
delay(30)
|
||||
println("источник активен при подписчике: ${active == 1}")
|
||||
job.cancelAndJoin()
|
||||
delay(30)
|
||||
println("источник остановлен без подписчиков: ${active == 0}")
|
||||
coroutineContext.cancelChildren() // иначе runBlocking ждёт корутину shareIn
|
||||
// источник активен при подписчике: true
|
||||
// источник остановлен без подписчиков: true
|
||||
}
|
||||
```
|
||||
| `SharingStarted` | Когда источник активен |
|
||||
|---|---|
|
||||
| `Eagerly` | сразу и до отмены scope |
|
||||
| `Lazily` | с первого подписчика, затем навсегда |
|
||||
| `WhileSubscribed()` | только пока есть подписчики |
|
||||
| `WhileSubscribed(stopTimeoutMillis = 5000)` | плюс пауза перед остановкой |
|
||||
|
||||
`WhileSubscribed(5000)` - стандартный выбор для Android: при повороте экрана подписчик пересоздаётся за миллисекунды, и таймаут не даёт зря перезапускать запрос.
|
||||
|
||||
## Как выбрать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужен поток значений"] --> B{"Один подписчик,<br/>работа по запросу?"}
|
||||
B -->|"да"| C["обычный Flow<br/>холодный"]
|
||||
B -->|"нет, нужно делить"| D{"Есть текущее<br/>значение?"}
|
||||
D -->|"да, это состояние"| E["StateFlow<br/>initialValue обязателен"]
|
||||
D -->|"нет, это события"| F{"Нужна история<br/>для новых подписчиков?"}
|
||||
F -->|"нет"| G["SharedFlow<br/>replay = 0"]
|
||||
F -->|"да, n последних"| H["SharedFlow<br/>replay = n"]
|
||||
```
|
||||
|
||||
| | `StateFlow` | `SharedFlow` |
|
||||
|---|---|---|
|
||||
| начальное значение | обязательно | нет |
|
||||
| `.value` | есть | нет |
|
||||
| дедупликация | да, по `equals` | нет |
|
||||
| `replay` | всегда 1 | настраивается |
|
||||
| потеря промежуточных | да, конфляция | зависит от буфера |
|
||||
| типичное применение | состояние экрана, настройки | навигация, снекбары, ошибки |
|
||||
|
||||
Практическое правило: состояние (что сейчас на экране) - `StateFlow`; события (что произошло один раз) - `SharedFlow`. Показ сообщения об ошибке через `StateFlow` приводит к повторному показу при пересоздании подписчика.
|
||||
|
||||
## Состояние экрана целиком
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
data class ScreenState(
|
||||
val loading: Boolean = false,
|
||||
val items: List<String> = emptyList(),
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class ViewModel(private val scope: CoroutineScope) {
|
||||
private val _state = MutableStateFlow(ScreenState())
|
||||
val state: StateFlow<ScreenState> = _state.asStateFlow()
|
||||
|
||||
fun load() {
|
||||
scope.launch {
|
||||
_state.update { it.copy(loading = true, error = null) }
|
||||
delay(20)
|
||||
_state.update { it.copy(loading = false, items = listOf("первый", "второй")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val vm = ViewModel(this)
|
||||
val job = launch { vm.state.collect { println(it) } }
|
||||
delay(10)
|
||||
vm.load()
|
||||
delay(60)
|
||||
job.cancel()
|
||||
// ScreenState(loading=false, items=[], error=null)
|
||||
// ScreenState(loading=true, items=[], error=null)
|
||||
// ScreenState(loading=false, items=[первый, второй], error=null)
|
||||
}
|
||||
```
|
||||
Один `StateFlow` на всё состояние лучше трёх отдельных: подписчик видит согласованный снимок, а не промежуточные комбинации полей. `copy` + `update` делают изменение атомарным. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
## Производные состояния
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val query = MutableStateFlow("")
|
||||
val items = MutableStateFlow(listOf("кот", "кофе", "книга"))
|
||||
|
||||
val filtered = combine(query, items) { q, list ->
|
||||
list.filter { it.startsWith(q) }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
delay(20) // Eagerly запускает корутину, но не мгновенно
|
||||
println(filtered.value) // [кот, кофе, книга]
|
||||
query.value = "ко"
|
||||
delay(20)
|
||||
println(filtered.value) // [кот, кофе]
|
||||
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
```
|
||||
`combine` + `stateIn` - способ получить вычисляемое состояние из нескольких источников. Важно вызвать `cancelChildren()` или ограничить scope: `stateIn` с `Eagerly` запускает корутину, которая живёт до отмены области.
|
||||
|
||||
## subscriptionCount
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<Int>()
|
||||
println("подписчиков: ${events.subscriptionCount.value}") // подписчиков: 0
|
||||
|
||||
val job = launch { events.collect { } }
|
||||
delay(20)
|
||||
println("после подписки: ${events.subscriptionCount.value}") // после подписки: 1
|
||||
|
||||
job.cancelAndJoin()
|
||||
println("после отмены: ${events.subscriptionCount.value}") // после отмены: 0
|
||||
}
|
||||
```
|
||||
Это `StateFlow<Int>`, поэтому за числом подписчиков можно наблюдать - именно так `WhileSubscribed` понимает, когда останавливать источник.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Observable` + `Observer` (паттерн) | `StateFlow` |
|
||||
> | `PropertyChangeListener` | `StateFlow` |
|
||||
> | RxJava `BehaviorSubject` | `StateFlow` |
|
||||
> | RxJava `PublishSubject` | `SharedFlow(replay = 0)` |
|
||||
> | RxJava `ReplaySubject(n)` | `SharedFlow(replay = n)` |
|
||||
> | `AtomicReference` + слушатели | `MutableStateFlow` |
|
||||
> | `CopyOnWriteArrayList<Listener>` | встроенная многоадресность |
|
||||
>
|
||||
> **Паттерн «наблюдатель» вручную.** В Java его пишут заново в каждом проекте: список слушателей, синхронизация при добавлении и удалении, защита от `ConcurrentModificationException` при оповещении, снятие слушателей при закрытии компонента. Классическая реализация:
|
||||
> ```java
|
||||
> private final List<Consumer<Integer>> listeners = new CopyOnWriteArrayList<>();
|
||||
> private volatile int value;
|
||||
>
|
||||
> public void setValue(int v) {
|
||||
> this.value = v;
|
||||
> for (var l : listeners) l.accept(v);
|
||||
> }
|
||||
> public void addListener(Consumer<Integer> l) { listeners.add(l); }
|
||||
> public void removeListener(Consumer<Integer> l) { listeners.remove(l); }
|
||||
> ```
|
||||
> Здесь уже есть проблемы: оповещение идёт в потоке вызывающего, медленный слушатель задерживает остальных, забытый `removeListener` - утечка. `MutableStateFlow` решает всё это за одну строку, а отписка происходит автоматически при отмене корутины-подписчика.
|
||||
>
|
||||
> **`BehaviorSubject` против `StateFlow`.** Прямой аналог, но с двумя отличиями. `BehaviorSubject` не дедуплицирует одинаковые значения - в Rx для этого добавляют `distinctUntilChanged()` руками. И `Subject` можно «завершить» через `onComplete`/`onError`, после чего он мёртв; `StateFlow` не завершается никогда - ошибки передают внутри состояния (поле `error`), что для UI удобнее: экран всегда в каком-то состоянии.
|
||||
>
|
||||
> **Утечки подписок.** В Rx подписка возвращает `Disposable`, который нужно хранить и вызывать `dispose()`; для борьбы с забывчивостью придумали `CompositeDisposable` и `AutoDispose`. В Kotlin подписчик - корутина, поэтому отмена scope отписывает всех:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val state = MutableStateFlow(0)
|
||||
> val scope = CoroutineScope(Job())
|
||||
> scope.launch { state.collect { } }
|
||||
> delay(20)
|
||||
> println("подписчиков: ${state.subscriptionCount.value}")
|
||||
> scope.cancel()
|
||||
> delay(20)
|
||||
> println("после cancel: ${state.subscriptionCount.value}")
|
||||
> // подписчиков: 1
|
||||
> // после cancel: 0
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`volatile` поле против `StateFlow`.** `volatile int value` даёт видимость между потоками, но не даёт ни оповещения, ни атомарного обновления (`value++` всё равно гонка - нужен `AtomicInteger`). `MutableStateFlow` совмещает три роли: потокобезопасное хранилище, атомарный `update` и многоадресная рассылка.
|
||||
>
|
||||
> **Нет аналога `WhileSubscribed`.** В Java управление жизненным циклом источника при появлении и исчезновении слушателей пишут вручную: счётчик подписчиков, запуск потока при первом, остановка при последнем, таймаут на повторное подключение. `shareIn(started = WhileSubscribed(5000))` - это одна строка.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`StateFlow` не доставит одинаковое значение дважды:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val showError = MutableStateFlow("ошибка сети")
|
||||
> var shown = 0
|
||||
> val job = launch { showError.collect { shown++ } }
|
||||
> delay(20)
|
||||
> showError.value = "ошибка сети" // то же самое — не придёт
|
||||
> delay(20)
|
||||
> job.cancel()
|
||||
> println("показов: $shown") // показов: 1
|
||||
> }
|
||||
> ```
|
||||
> Для повторяющихся событий (две одинаковые ошибки подряд) нужен `SharedFlow`.
|
||||
>
|
||||
> **Изменяемое состояние внутри `StateFlow` ломает оповещение.** Если вы положили `MutableList` и меняете его содержимое, `equals` вернёт `true` и подписчики ничего не узнают. Храните неизменяемые `data class` и обновляйте через `copy`.
|
||||
>
|
||||
> **`state.value = state.value + 1` - гонка:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val state = MutableStateFlow(0)
|
||||
> withContext(Dispatchers.Default) {
|
||||
> List(50) { launch { repeat(200) { state.value = state.value + 1 } } }
|
||||
> .forEach { it.join() }
|
||||
> }
|
||||
> println("обновления потеряны: ${state.value < 10_000}") // обновления потеряны: true
|
||||
> }
|
||||
> ```
|
||||
> Используйте `update { }`.
|
||||
>
|
||||
> **`SharedFlow` без подписчиков теряет события.** `emit` при `replay = 0` и нулевом буфере просто завершится, не доставив ничего. Для «событий, которые нельзя потерять» нужен `replay ≥ 1` или `Channel`. Подробнее: [Channels](Channels.md)
|
||||
>
|
||||
> **`emit` в `SharedFlow` с `SUSPEND` приостанавливается на медленном подписчике:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val events = MutableSharedFlow<Int>()
|
||||
> val job = launch { events.collect { delay(100) } }
|
||||
> delay(20)
|
||||
> val t = measureTimeMillis { events.emit(1); events.emit(2) }
|
||||
> println("emit ждал подписчика: ${t >= 90}") // emit ждал подписчика: true
|
||||
> job.cancel()
|
||||
> }
|
||||
> ```
|
||||
> Один зависший подписчик блокирует отправку всем. Задавайте `extraBufferCapacity` и стратегию переполнения.
|
||||
>
|
||||
> **`tryEmit` возвращает `true` без подписчиков**, хотя событие никто не получил. Проверка `if (tryEmit(x))` не означает «доставлено».
|
||||
>
|
||||
> **`DROP_OLDEST` без буфера не работает.** При `replay = 0` и `extraBufferCapacity = 0` стратегия переполнения игнорируется, потому что буфера нет - `emit` будет приостанавливаться.
|
||||
>
|
||||
> **`SharingStarted.Eagerly` держит источник навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var active = 0
|
||||
> val source = flow {
|
||||
> active++
|
||||
> try { repeat(1000) { delay(10); emit(it) } } finally { active-- }
|
||||
> }
|
||||
> val hot = source.stateIn(this, SharingStarted.Eagerly, -1)
|
||||
> delay(30)
|
||||
> println("работает без единого подписчика: ${active == 1}")
|
||||
> coroutineContext.cancelChildren()
|
||||
> // работает без единого подписчика: true
|
||||
> }
|
||||
> ```
|
||||
> Если источник - опрос сервера, он будет работать и жечь батарею, пока жив scope. Для подписок UI нужен `WhileSubscribed()`.
|
||||
>
|
||||
> **`stateIn`/`shareIn` требуют scope, который кто-то отменит.** Передача `GlobalScope` или scope, живущего весь процесс, превращает их в утечку.
|
||||
>
|
||||
> **`StateFlow.value` - снимок, а не транзакция:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val a = MutableStateFlow(1)
|
||||
> val b = MutableStateFlow(10)
|
||||
> // между чтениями значения могли измениться
|
||||
> val sum = a.value + b.value
|
||||
> println("несогласованный снимок возможен: $sum") // несогласованный снимок возможен: 11
|
||||
> }
|
||||
> ```
|
||||
> Два связанных значения нужно держать в одном `StateFlow` как единый объект состояния.
|
||||
>
|
||||
> **`collect` на `StateFlow` никогда не завершается.** Это бесконечный поток: `toList()` на нём повиснет, `onCompletion` не вызовется сам. Нужен `take(n)`, `first()` или отмена корутины.
|
||||
>
|
||||
> **Подписчик, созданный до `emit`, может его пропустить:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val events = MutableSharedFlow<Int>()
|
||||
> launch { events.collect { println("не увидит: $it") } }
|
||||
> events.emit(1) // launch ещё не успел подписаться
|
||||
> delay(20)
|
||||
> coroutineContext.cancelChildren()
|
||||
> // (ничего не напечатано)
|
||||
> }
|
||||
> ```
|
||||
> `launch` возвращается до начала тела, поэтому подписка ещё не оформлена. В тестах для этого используют `events.subscriptionCount.first { it > 0 }`, в продакшене - `replay ≥ 1`.
|
||||
>
|
||||
> **`asStateFlow()` не копирует состояние** - это обёртка только для чтения над тем же объектом. Если внешний код получит доступ к исходному `MutableStateFlow`, он сможет его менять.
|
||||
>
|
||||
> **Ошибка в подписчике отменяет его подписку, а не поток.** Исключение в теле `collect` уходит в корутину-подписчик и может уронить её scope, при этом `StateFlow` продолжит жить. Оборачивайте тело `collect` в `try/catch`, если ошибки ожидаемы.
|
||||
|
||||
Далее: [Channels](Channels.md)
|
||||
@@ -1,395 +0,0 @@
|
||||
---
|
||||
title: Suspend-функции
|
||||
description: Модификатор suspend, Continuation, машина состояний и мост к колбэчным API
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
`suspend` означает: эта функция может приостановиться, поэтому вызывать её разрешено только оттуда, где приостановка возможна.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun fetchData(): String {
|
||||
delay(100) // точка приостановки
|
||||
return "данные"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(fetchData()) // данные
|
||||
}
|
||||
```
|
||||
Вне корутины вызов не скомпилируется: `fun main() { fetchData() }` даёт «Suspend function can only be called within coroutine body».
|
||||
|
||||
## Что добавляет компилятор
|
||||
`suspend fun fetchData(): String` превращается в метод с дополнительным параметром:
|
||||
```kotlin
|
||||
// примерно так это выглядит с точки зрения Java:
|
||||
// Object fetchData(Continuation<? super String> continuation)
|
||||
```
|
||||
`Continuation` - это «что делать дальше»: объект с методом `resumeWith`, хранящий сохранённое состояние функции.
|
||||
```kotlin
|
||||
import kotlin.coroutines.*
|
||||
|
||||
fun main() {
|
||||
val continuation = object : Continuation<String> {
|
||||
override val context = EmptyCoroutineContext
|
||||
override fun resumeWith(result: Result<String>) {
|
||||
println("получено: ${result.getOrNull()}")
|
||||
}
|
||||
}
|
||||
continuation.resumeWith(Result.success("значение")) // получено: значение
|
||||
}
|
||||
```
|
||||
Отсюда и название CPS - continuation-passing style. Компилятор делает это преобразование автоматически, поэтому в исходнике остаётся обычный последовательный код.
|
||||
|
||||
## Машина состояний
|
||||
Каждая точка приостановки превращается в метку состояния:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun threeSteps(): Int {
|
||||
println("шаг 1") // состояние 0
|
||||
delay(10)
|
||||
println("шаг 2") // состояние 1
|
||||
delay(10)
|
||||
println("шаг 3") // состояние 2
|
||||
return 42
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(threeSteps())
|
||||
// шаг 1
|
||||
// шаг 2
|
||||
// шаг 3
|
||||
// 42
|
||||
}
|
||||
```
|
||||
В байт-коде это один метод с `switch` по полю `label`: при каждом возобновлении он входит в себя заново и прыгает к нужной метке. Локальные переменные, живущие между приостановками, становятся полями объекта-продолжения.
|
||||
|
||||
Практические следствия:
|
||||
- приостановка стоит дешевле переключения потока, но дороже обычного вызова
|
||||
- глубоко вложенные `suspend`-вызовы создают по объекту продолжения на кадр
|
||||
- стектрейс восстанавливается библиотекой, а не JVM - поэтому он может быть короче ожидаемого
|
||||
|
||||
## Функция, которая не приостанавливается
|
||||
`suspend` не обязывает приостанавливаться:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun maybeSlow(cached: String?): String {
|
||||
cached?.let { return it } // быстрый путь: без приостановки
|
||||
delay(100)
|
||||
return "загружено"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(maybeSlow("из кеша")) // из кеша
|
||||
println(maybeSlow(null)) // загружено
|
||||
}
|
||||
```
|
||||
Если приостановки не произошло, функция возвращает значение напрямую - без накладных расходов. Это важно для «горячих» путей: `suspend` не означает «всегда медленно».
|
||||
|
||||
Компилятор предупреждает о `suspend` без точек приостановки:
|
||||
```kotlin
|
||||
suspend fun noSuspension(): Int = 42 // warning: redundant 'suspend' modifier
|
||||
|
||||
fun main() {
|
||||
println("нужен вызов из корутины или runBlocking")
|
||||
}
|
||||
```
|
||||
Такой модификатор стоит убрать - он заставляет вызывающих быть `suspend` без причины.
|
||||
|
||||
## suspend-лямбды и типы
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun retry(times: Int, block: suspend () -> String): String {
|
||||
repeat(times - 1) { attempt ->
|
||||
try {
|
||||
return block()
|
||||
} catch (e: Exception) {
|
||||
println("попытка ${attempt + 1}: ${e.message}")
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
val result = retry(3) {
|
||||
counter++
|
||||
delay(5)
|
||||
if (counter < 3) error("не готово") else "успех"
|
||||
}
|
||||
println(result)
|
||||
// попытка 1: не готово
|
||||
// попытка 2: не готово
|
||||
// успех
|
||||
}
|
||||
```
|
||||
Тип `suspend () -> String` - функциональный тип, внутри которого можно приостанавливаться. Без `suspend` в типе параметра `delay` внутри лямбды не скомпилируется.
|
||||
|
||||
Для лямбды с получателем аналогично: `suspend CoroutineScope.() -> Unit` - именно так объявлены `launch` и `runBlocking`.
|
||||
|
||||
## Приостановка без блокировки: suspendCancellableCoroutine
|
||||
Это мост от колбэчного API к `suspend`-функции - основной способ интегрировать существующие библиотеки.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
// колбэчный API, который нужно обернуть
|
||||
fun legacyLoad(onSuccess: (String) -> Unit, onError: (Throwable) -> Unit) {
|
||||
Thread {
|
||||
Thread.sleep(50)
|
||||
onSuccess("результат из колбэка")
|
||||
}.start()
|
||||
}
|
||||
|
||||
suspend fun load(): String = suspendCancellableCoroutine { cont ->
|
||||
legacyLoad(
|
||||
onSuccess = { cont.resume(it) },
|
||||
onError = { cont.resumeWithException(it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(load()) // результат из колбэка
|
||||
}
|
||||
```
|
||||
Правила: `resume` или `resumeWithException` нужно вызвать **ровно один раз**. Повторный вызов бросит `IllegalStateException`.
|
||||
|
||||
Отмену нужно поддержать явно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun awaitFuture(future: CompletableFuture<String>): String =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
future.whenComplete { value, error ->
|
||||
if (error != null) cont.cancel(error) else cont.resume(value)
|
||||
}
|
||||
cont.invokeOnCancellation { future.cancel(true) } // освобождаем ресурс
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val future = CompletableFuture.supplyAsync {
|
||||
Thread.sleep(30)
|
||||
"из future"
|
||||
}
|
||||
println(awaitFuture(future)) // из future
|
||||
}
|
||||
```
|
||||
`invokeOnCancellation` - то, что отличает корректную обёртку от утечки: при отмене корутины нижележащий запрос тоже нужно отменить. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
|
||||
## Последовательность и параллельность
|
||||
`suspend`-функции по умолчанию выполняются последовательно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun step(name: String): String { delay(100); return name }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
val a = step("a")
|
||||
val b = step("b")
|
||||
println("$a$b") // ab
|
||||
}
|
||||
println("последовательно около 200 мс: ${sequential >= 200}") // последовательно около 200 мс: true
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
coroutineScope {
|
||||
val a = async { step("a") }
|
||||
val b = async { step("b") }
|
||||
println("${a.await()}${b.await()}") // ab
|
||||
}
|
||||
}
|
||||
println("параллельно около 100 мс: ${parallel < 180}") // параллельно около 100 мс: true
|
||||
}
|
||||
```
|
||||
Это сознательное решение: последовательность - разумное поведение по умолчанию, параллельность требуется явно запросить через `async`. Подробнее: [Билдеры корутин](Билдеры%20корутин.md)
|
||||
|
||||
## Правила проектирования suspend-API
|
||||
| Правило | Почему |
|
||||
|---|---|
|
||||
| `suspend fun` не должна блокировать поток | иначе теряется весь смысл |
|
||||
| смена диспетчера - внутри функции, а не у вызывающего | вызывающий не должен знать, I/O это или CPU |
|
||||
| функция должна быть безопасна для отмены | иначе отмена не работает или течёт |
|
||||
| возвращать значение, а не `Deferred` | параллельность - дело вызывающего |
|
||||
| `suspend` + возврат `Flow` обычно излишни | `Flow` сам по себе холодный |
|
||||
|
||||
Правило про диспетчер - самое практичное:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// хорошо: вызывающему не нужно думать о потоках
|
||||
suspend fun readConfig(): String = withContext(Dispatchers.IO) {
|
||||
"содержимое файла" // здесь был бы блокирующий File.readText
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(readConfig()) // содержимое файла
|
||||
}
|
||||
```
|
||||
Такая функция называется main-safe: её можно вызвать с любого потока, включая UI.
|
||||
|
||||
## suspend и наследование
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
interface Repository {
|
||||
suspend fun find(id: Int): String?
|
||||
}
|
||||
|
||||
class RemoteRepository : Repository {
|
||||
override suspend fun find(id: Int): String? {
|
||||
delay(10)
|
||||
return if (id == 1) "найдено" else null
|
||||
}
|
||||
}
|
||||
|
||||
class CachedRepository : Repository {
|
||||
private val cache = mapOf(1 to "из кеша")
|
||||
override suspend fun find(id: Int): String? = cache[id] // без приостановки
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(RemoteRepository().find(1)) // найдено
|
||||
println(CachedRepository().find(1)) // из кеша
|
||||
println(CachedRepository().find(2)) // null
|
||||
}
|
||||
```
|
||||
`suspend` в интерфейсе - правильный способ объявить операцию, которая **может** быть асинхронной. Реализация решает, приостанавливаться ли. Обратное невозможно: не-suspend метод интерфейса нельзя переопределить как `suspend`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `CompletableFuture<T> f()` | `suspend fun f(): T` |
|
||||
> | `void f(Callback<T> cb)` | `suspend fun f(): T` |
|
||||
> | `f().thenApply(x -> ...)` | `val x = f()` |
|
||||
> | `CompletionStage` в сигнатуре | `suspend` в модификаторе |
|
||||
> | блокирующий `f()` + пул потоков | `suspend` + `withContext(IO)` |
|
||||
> | нет аналога | `Continuation` |
|
||||
>
|
||||
> **Асинхронность в типе возврата против модификатора.** В Java асинхронная функция возвращает обёртку: `CompletableFuture<User>`. Это заражает тип: `List<CompletableFuture<User>>` приходится разворачивать через `allOf`, а вложенные вызовы дают `CompletableFuture<CompletableFuture<T>>` и `thenCompose` вместо `thenApply`. В Kotlin тип возврата остаётся обычным (`User`), а асинхронность выражается модификатором:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadOne(id: Int): String { delay(10); return "u$id" }
|
||||
>
|
||||
> suspend fun loadMany(ids: List<Int>): List<String> = coroutineScope {
|
||||
> ids.map { async { loadOne(it) } }.awaitAll()
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(loadMany(listOf(1, 2, 3))) } // [u1, u2, u3]
|
||||
> ```
|
||||
> В Java это `CompletableFuture.allOf(...)` с ручной сборкой результатов и приведением типов.
|
||||
>
|
||||
> **`Continuation` - это обобщённый коллбэк.** В сущности `suspend fun f(): T` компилируется в `f(Continuation<T>)`, то есть в тот же колбэчный стиль, который в Java пишут руками. Разница в том, что преобразование делает компилятор, поэтому исходник остаётся линейным, а в Java программист пишет лямбды сам и получает «callback hell» при вложенности.
|
||||
>
|
||||
> **Из Java `suspend`-функцию вызвать неудобно.** Она видна как метод с лишним параметром `Continuation`, возвращающий `Object`. Для интеграции есть готовые мосты: `future { }` из `kotlinx-coroutines-jdk8` возвращает `CompletableFuture`, а `runBlocking` даёт синхронный вызов. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> **Виртуальные потоки убирают необходимость в модификаторе.** В Java 21 блокирующий `loadOne(id)` на виртуальном потоке масштабируется без изменения сигнатуры - это главный плюс Loom: существующий код работает как есть. Цена - компилятор не знает, где функция блокируется, поэтому нет ни структурированной отмены по дереву, ни проверки «не блокируй UI-поток» на этапе компиляции.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокировка внутри `suspend` сводит корутины на нет:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> // плохо: функция объявлена suspend, но блокирует поток
|
||||
> suspend fun badRead(): String {
|
||||
> Thread.sleep(50)
|
||||
> return "заблокировал поток"
|
||||
> }
|
||||
>
|
||||
> // хорошо: блокирующий код в подходящем диспетчере
|
||||
> suspend fun goodRead(): String = withContext(Dispatchers.IO) {
|
||||
> Thread.sleep(50)
|
||||
> "поток из IO-пула"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(badRead())
|
||||
> println(goodRead())
|
||||
> // заблокировал поток
|
||||
> // поток из IO-пула
|
||||
> }
|
||||
> ```
|
||||
> Компилятор это не поймает: `suspend` не запрещает блокировку. Ошибка проявится как «приложение зависает под нагрузкой».
|
||||
>
|
||||
> **Повторный `resume` бросает исключение:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.coroutines.resume
|
||||
>
|
||||
> suspend fun broken(): String = suspendCancellableCoroutine { cont ->
|
||||
> cont.resume("первый")
|
||||
> // cont.resume("второй") // IllegalStateException: Already resumed
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(broken()) } // первый
|
||||
> ```
|
||||
> Колбэчные API, которые могут вызвать коллбэк дважды (например, при ретраях внутри), требуют защиты флагом или `isActive`-проверки.
|
||||
>
|
||||
> **Обёртка без `invokeOnCancellation` течёт.** При отмене корутины нижележащий запрос продолжит работу: соединение останется открытым, таймер - запущенным, файл - читаемым. Всегда освобождайте ресурс в `invokeOnCancellation`.
|
||||
>
|
||||
> **`suspendCoroutine` без `Cancellable` игнорирует отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> withTimeout(30) { delay(1000) }
|
||||
> } catch (e: TimeoutCancellationException) {
|
||||
> println("таймаут сработал: ${e::class.simpleName}")
|
||||
> }
|
||||
> }
|
||||
> job.join()
|
||||
> }
|
||||
> ```
|
||||
> `delay` отменяем, поэтому таймаут работает. А вот `suspendCoroutine { }` (без `Cancellable`) приостановится навсегда и таймаут его не прервёт - используйте `suspendCancellableCoroutine`.
|
||||
>
|
||||
> **`suspend` поднимается по всему стеку вызовов.** Добавить приостановку в глубоко вложенную функцию означает помечать `suspend` всю цепочку вверх. Если наверху - не-suspend интерфейс (например, `Comparator` или `equals`), сделать это невозможно, и придётся либо менять архитектуру, либо блокировать через `runBlocking`.
|
||||
>
|
||||
> **`suspend` нельзя использовать в конструкторе, геттере свойства и `init`:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class Service {
|
||||
> // suspend init — невозможно
|
||||
> // val value: String get() = loadValue() // геттер не может быть suspend
|
||||
>
|
||||
> suspend fun loadValue(): String { delay(10); return "значение" }
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(Service().loadValue()) } // значение
|
||||
> ```
|
||||
> Для асинхронной инициализации используют отдельную `suspend fun init()` или `lazy`-обёртку через `Deferred`.
|
||||
>
|
||||
> **Нельзя переопределить обычный метод как `suspend`.** Если интерфейс объявлен в Java (`Runnable`, `Callable`, `Comparator`), асинхронность туда не поместится - только `runBlocking` внутри, что блокирует поток.
|
||||
>
|
||||
> **Стектрейсы обрезаны.** Исключение из корутины содержит кадры только текущей точки возобновления, а не всю логическую цепочку. Для отладки включают `-Dkotlinx.coroutines.debug` или добавляют `kotlinx-coroutines-debug`, которые восстанавливают историю приостановок.
|
||||
>
|
||||
> **Рекурсивная `suspend`-функция создаёт объект продолжения на каждый кадр:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun countDown(n: Int): Int = if (n <= 0) 0 else countDown(n - 1) + 1
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(countDown(1000)) // 1000
|
||||
> }
|
||||
> ```
|
||||
> Глубина ограничена не стеком JVM, а памятью, но на десятках тысяч кадров это заметная аллокация. Хвостовая рекурсия (`tailrec`) с `suspend` не сочетается.
|
||||
>
|
||||
> **`suspend` без приостановок - лишний.** Он заставляет всех вызывающих быть `suspend`, не давая ничего взамен. Компилятор предупреждает; предупреждение стоит слушать.
|
||||
|
||||
Далее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
@@ -1,417 +0,0 @@
|
||||
---
|
||||
title: Билдеры корутин
|
||||
description: launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще.
|
||||
|
||||
| Билдер | Возвращает | Блокирует поток | Нужен результат |
|
||||
|---|---|---|---|
|
||||
| `launch` | `Job` | нет | нет |
|
||||
| `async` | `Deferred<T>` | нет | да |
|
||||
| `runBlocking` | `T` | да | да |
|
||||
| `withContext` | `T` | нет | да, смена контекста |
|
||||
| `produce` | `ReceiveChannel<T>` | нет | поток значений |
|
||||
| `actor` | `SendChannel<T>` | нет | обработка сообщений |
|
||||
|
||||
## launch: запустить и не ждать результата
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
delay(50)
|
||||
println("работа выполнена")
|
||||
}
|
||||
println("launch вернулся сразу: ${job.isActive}")
|
||||
job.join()
|
||||
println("дождались")
|
||||
// launch вернулся сразу: true
|
||||
// работа выполнена
|
||||
// дождались
|
||||
}
|
||||
```
|
||||
`launch` возвращает `Job` - дескриптор для `join()`, `cancel()` и проверки состояния. Результат тела отбрасывается: тип всегда `Job`, даже если блок что-то вычислил.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch { 42 } // значение 42 теряется
|
||||
job.join()
|
||||
println(job::class.simpleName != null) // true
|
||||
}
|
||||
```
|
||||
|
||||
## async: запустить и получить результат
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun loadPrice(id: Int): Int { delay(100); return id * 10 }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val time = measureTimeMillis {
|
||||
val a = async { loadPrice(1) }
|
||||
val b = async { loadPrice(2) }
|
||||
println("сумма: ${a.await() + b.await()}") // сумма: 30
|
||||
}
|
||||
println("параллельно: ${time < 180}") // параллельно: true
|
||||
}
|
||||
```
|
||||
`Deferred<T>` - это `Job` с результатом: у него есть и `join()`, и `await()`. Вызов `await()` приостанавливает корутину до готовности значения.
|
||||
|
||||
`await` можно вызывать повторно - результат кешируется:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var calls = 0
|
||||
val d = async { calls++; delay(20); "значение" }
|
||||
println(d.await()) // значение
|
||||
println(d.await()) // значение
|
||||
println("тело выполнилось раз: $calls") // тело выполнилось раз: 1
|
||||
}
|
||||
```
|
||||
|
||||
Ленивый запуск - тело не начнётся до `await()` или `start()`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val lazy = async(start = CoroutineStart.LAZY) {
|
||||
println("тело стартовало")
|
||||
"результат"
|
||||
}
|
||||
println("создан, но не запущен")
|
||||
delay(20)
|
||||
println(lazy.await())
|
||||
// создан, но не запущен
|
||||
// тело стартовало
|
||||
// результат
|
||||
}
|
||||
```
|
||||
Внимание: ленивый `async`, у которого так и не вызвали `await()`, никогда не выполнится и заблокирует завершение области до её отмены.
|
||||
|
||||
## runBlocking: мост из синхронного кода
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun compute(): Int { delay(20); return 42 }
|
||||
|
||||
fun main() {
|
||||
val result = runBlocking { compute() } // обычная функция вызывает suspend
|
||||
println(result) // 42
|
||||
}
|
||||
```
|
||||
`runBlocking` блокирует текущий поток до завершения корутины и всех её детей. Это единственный билдер, который **не** требует scope - он его создаёт.
|
||||
|
||||
Применимость:
|
||||
| Место | Уместен ли `runBlocking` |
|
||||
|---|---|
|
||||
| функция `main` | да |
|
||||
| тесты (если нет `runTest`) | да |
|
||||
| CLI-утилита | да |
|
||||
| обработчик HTTP-запроса | нет, фреймворк даёт scope |
|
||||
| Android UI-поток | категорически нет |
|
||||
| внутри другой корутины | нет, это блокировка потока |
|
||||
|
||||
## withContext: сменить контекст без новой корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun readFile(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // здесь блокирующий вызов допустим
|
||||
"содержимое"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(readFile()) // содержимое
|
||||
}
|
||||
```
|
||||
`withContext` не создаёт новую корутину - он переключает контекст текущей и возвращает значение блока. Это главный инструмент для «main-safe» функций.
|
||||
|
||||
Разница с `async { }.await()` существенна:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun viaWithContext(): Int = withContext(Dispatchers.Default) { 1 }
|
||||
|
||||
suspend fun viaAsync(): Int = coroutineScope {
|
||||
async(Dispatchers.Default) { 1 }.await() // лишняя корутина
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("${viaWithContext()} ${viaAsync()}") // 1 1
|
||||
}
|
||||
```
|
||||
Результат одинаков, но `async().await()` создаёт объект `Deferred` и корутину зря. Правило: если результат нужен **сразу**, это `withContext`; `async` оправдан только когда между запуском и `await` есть другая работа.
|
||||
|
||||
## Параллельная декомпозиция
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
data class Dashboard(val user: String, val orders: List<String>, val balance: Int)
|
||||
|
||||
suspend fun fetchUser(): String { delay(30); return "Иван" }
|
||||
suspend fun fetchOrders(): List<String> { delay(40); return listOf("заказ 1") }
|
||||
suspend fun fetchBalance(): Int { delay(20); return 1000 }
|
||||
|
||||
suspend fun loadDashboard(): Dashboard = coroutineScope {
|
||||
val user = async { fetchUser() }
|
||||
val orders = async { fetchOrders() }
|
||||
val balance = async { fetchBalance() }
|
||||
Dashboard(user.await(), orders.await(), balance.await())
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadDashboard())
|
||||
// Dashboard(user=Иван, orders=[заказ 1], balance=1000)
|
||||
}
|
||||
```
|
||||
Это каноничный шаблон: `coroutineScope` + несколько `async` + сборка результата. Время работы - максимум из трёх, а не сумма; при ошибке любого запроса остальные отменяются.
|
||||
|
||||
## CoroutineStart
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
launch(start = CoroutineStart.DEFAULT) { println("3: обычный, после текущего кода") }
|
||||
launch(start = CoroutineStart.UNDISPATCHED) { println("1: сразу, до первой приостановки") }
|
||||
val lazy = launch(start = CoroutineStart.LAZY) { println("4: только после start()") }
|
||||
println("2: тело main")
|
||||
lazy.start()
|
||||
delay(20)
|
||||
// 1: сразу, до первой приостановки
|
||||
// 2: тело main
|
||||
// 3: обычный, после текущего кода
|
||||
// 4: только после start()
|
||||
}
|
||||
```
|
||||
| Значение | Поведение |
|
||||
|---|---|
|
||||
| `DEFAULT` | запланировать запуск согласно диспетчеру |
|
||||
| `LAZY` | не запускать до `start()`, `join()` или `await()` |
|
||||
| `ATOMIC` | запустить, игнорируя отмену до первой точки приостановки |
|
||||
| `UNDISPATCHED` | выполнять в текущем потоке до первой приостановки |
|
||||
|
||||
`UNDISPATCHED` используют, когда важно выполнить начало тела немедленно (например, зарегистрировать подписку до возврата из функции). Остальные значения нужны редко.
|
||||
|
||||
## produce и actor
|
||||
`produce` - билдер, возвращающий канал с результатами:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
|
||||
for (i in 1..3) {
|
||||
delay(10)
|
||||
send(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = numbers()
|
||||
for (value in channel) print("$value ")
|
||||
println() // 1 2 3
|
||||
}
|
||||
```
|
||||
`actor` - обратное: корутина, принимающая сообщения:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
sealed interface Msg {
|
||||
data class Add(val value: Int) : Msg
|
||||
data class GetSum(val response: CompletableDeferred<Int>) : Msg
|
||||
}
|
||||
|
||||
fun CoroutineScope.counter() = actor<Msg> {
|
||||
var sum = 0
|
||||
for (msg in channel) {
|
||||
when (msg) {
|
||||
is Msg.Add -> sum += msg.value
|
||||
is Msg.GetSum -> msg.response.complete(sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val c = counter()
|
||||
c.send(Msg.Add(10))
|
||||
c.send(Msg.Add(5))
|
||||
val response = CompletableDeferred<Int>()
|
||||
c.send(Msg.GetSum(response))
|
||||
println(response.await()) // 15
|
||||
c.close()
|
||||
}
|
||||
```
|
||||
Оба помечены `@ObsoleteCoroutinesApi` - их планируют заменить. Для потоков значений используйте [Flow](Flow.md), для разделяемого состояния - [Mutex или StateFlow](Разделяемое%20состояние.md).
|
||||
|
||||
## Как выбирать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужна корутина"] --> B{"Вы в suspend-функции<br/>или в scope?"}
|
||||
B -->|"нет, обычный код"| C["runBlocking"]
|
||||
B -->|"да"| D{"Нужен результат?"}
|
||||
D -->|"нет, fire-and-forget"| E["launch"]
|
||||
D -->|"да"| F{"Нужен прямо сейчас?"}
|
||||
F -->|"да, просто другой поток"| G["withContext"]
|
||||
F -->|"нет, сначала другая работа"| H["async + await"]
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `executor.execute(task)` | `launch` |
|
||||
> | `executor.submit(task)` -> `Future<T>` | `async` -> `Deferred<T>` |
|
||||
> | `future.get()` (блокирует) | `deferred.await()` (приостанавливает) |
|
||||
> | `CompletableFuture.supplyAsync` | `async` |
|
||||
> | `future.get()` в `main` | `runBlocking` |
|
||||
> | `CompletableFuture.allOf` | `awaitAll` |
|
||||
> | явная передача пула в каждый вызов | контекст наследуется |
|
||||
>
|
||||
> **`await()` приостанавливает, `get()` блокирует.** Это ключевая разница. `Future.get()` занимает поток до готовности результата, поэтому в пуле из 8 потоков 8 ожидающих задач полностью его парализуют - классический deadlock при вложенных `get()`. `Deferred.await()` освобождает поток, поэтому миллион ожидающих корутин живут в нескольких потоках.
|
||||
>
|
||||
> **Контекст наследуется, а не передаётся.** В Java пул приходится протаскивать в каждый вызов: `supplyAsync(task, executor)`, и забытый второй аргумент молча отправит задачу в `ForkJoinPool.commonPool`. В Kotlin дочерняя корутина наследует контекст родителя:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> withContext(Dispatchers.IO) {
|
||||
> launch {
|
||||
> val name = Thread.currentThread().name
|
||||
> val onIo = name.contains("DefaultDispatcher")
|
||||
> println("ребёнок унаследовал IO: $onIo")
|
||||
> }
|
||||
> }
|
||||
> // ребёнок унаследовал IO: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`CompletableFuture` комбинируется методами, корутины - кодом.** Java предлагает `thenCombine`, `allOf`, `anyOf`, `thenCompose` - по методу на каждый случай. В Kotlin сбор результатов пишется обычным кодом (`listOf(a.await(), b.await())`), поэтому `if`, циклы и `try/catch` работают там естественно.
|
||||
>
|
||||
> **Нет аналога `runBlocking` в Java, потому что он не нужен:** `future.get()` и так блокирует. Обратная ситуация - в Kotlin `runBlocking` нужен именно как явная граница между блокирующим и приостанавливаемым миром, и эта явность полезна: видно, где поток встаёт.
|
||||
>
|
||||
> **`launch` против `submit` в обработке ошибок.** Исключение в `submit` попадает в `Future` и теряется, если не вызвать `get()`. Исключение в `launch` уходит родителю и обязательно будет обработано - либо родителем, либо `CoroutineExceptionHandler`, либо уронит приложение. Терять ошибки молча корутины не позволяют. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`runBlocking` внутри корутины блокирует поток диспетчера:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
> launch {
|
||||
> runBlocking { delay(50) } // держит единственный поток
|
||||
> println("первая закончила")
|
||||
> }
|
||||
> launch { println("вторая ждала освобождения потока") }
|
||||
> }
|
||||
> // первая закончила
|
||||
> // вторая ждала освобождения потока
|
||||
> }
|
||||
> ```
|
||||
> На ограниченном диспетчере это прямой путь к deadlock. Внутри корутины нужен `withContext`, а не `runBlocking`.
|
||||
>
|
||||
> **`async` без `await` проглатывает исключение до конца области:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = runCatching {
|
||||
> coroutineScope {
|
||||
> async { throw IllegalStateException("ошибка в async") }
|
||||
> delay(20)
|
||||
> "тело завершилось"
|
||||
> }
|
||||
> }
|
||||
> println(result.exceptionOrNull()?.message) // ошибка в async
|
||||
> }
|
||||
> ```
|
||||
> Ошибка не исчезла - она отменила область при завершении. Но заметить её в месте возникновения невозможно, а если `async` висит в `SupervisorJob`, она потеряется совсем.
|
||||
>
|
||||
> **Ленивый `async` без `await` подвешивает область навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> async(start = CoroutineStart.LAZY) { "никогда не выполнится" }
|
||||
> println("тело launch закончилось")
|
||||
> }
|
||||
> val finished = withTimeoutOrNull(200) { job.join(); true } ?: false
|
||||
> println("launch завершился: $finished")
|
||||
> job.cancel()
|
||||
> // тело launch закончилось
|
||||
> // launch завершился: false
|
||||
> }
|
||||
> ```
|
||||
> Ленивый `Deferred` остаётся незавершённым ребёнком, а родитель обязан дождаться всех детей - поэтому `launch` не завершится никогда, даже после выхода из тела. Без `job.cancel()` программа повиснет. Это тихая утечка: ни ошибки, ни предупреждения.
|
||||
>
|
||||
> **`async` последовательно - это не параллельно:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> suspend fun slow(): Int { delay(100); return 1 }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val wrong = measureTimeMillis {
|
||||
> val a = async { slow() }.await() // await сразу — ждём здесь же
|
||||
> val b = async { slow() }.await()
|
||||
> println(a + b) // 2
|
||||
> }
|
||||
> println("последовательно: ${wrong >= 200}") // последовательно: true
|
||||
> }
|
||||
> ```
|
||||
> `await()` сразу после `async` убивает параллельность. Запускайте все `async`, потом ждите.
|
||||
>
|
||||
> **`launch` теряет результат молча.** Тип `Job` не содержит значения, поэтому `launch { computeSomething() }` компилируется и ничего не делает с результатом. Компилятор не предупредит.
|
||||
>
|
||||
> **`withContext` на каждый мелкий вызов стоит дорого:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun badLoop(): Int = withContext(Dispatchers.Default) {
|
||||
> var sum = 0
|
||||
> repeat(1000) { sum += it } // одно переключение — правильно
|
||||
> sum
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(badLoop()) // 499500
|
||||
> }
|
||||
> ```
|
||||
> Переключение контекста - это передача задачи в другой пул с синхронизацией. Выносите `withContext` наружу цикла, а не внутрь.
|
||||
>
|
||||
> **`runBlocking` в Android UI-потоке вызывает ANR.** Это самая частая серьёзная ошибка: UI замирает до конца корутины. В Android используйте `lifecycleScope`/`viewModelScope`.
|
||||
>
|
||||
> **`produce` и `actor` устарели.** Они помечены `@ObsoleteCoroutinesApi`, их API может измениться. Для новых проектов - `Flow` и `Channel` напрямую, либо `Mutex` для состояния.
|
||||
>
|
||||
> **`CoroutineStart.ATOMIC` игнорирует отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> @OptIn(ExperimentalCoroutinesApi::class)
|
||||
> val job = launch(start = CoroutineStart.ATOMIC) {
|
||||
> println("тело всё равно начнётся")
|
||||
> delay(100)
|
||||
> println("а это уже нет")
|
||||
> }
|
||||
> job.cancel()
|
||||
> job.join()
|
||||
> // тело всё равно начнётся
|
||||
> }
|
||||
> ```
|
||||
> Начало тела выполнится даже после `cancel()`. Это нужно для гарантированного освобождения ресурсов, но легко превращается в сюрприз.
|
||||
>
|
||||
> **Возврат `Deferred` из публичной функции ломает структурированность** - вызывающий может не вызвать `await()`, и ошибка потеряется. Возвращайте значение из `suspend`-функции.
|
||||
|
||||
Далее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
@@ -1,404 +0,0 @@
|
||||
---
|
||||
title: Диспетчеры и контекст
|
||||
description: CoroutineContext как набор элементов, выбор между Default, IO и Main, limitedParallelism и наследование контекста
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
`CoroutineContext` - неизменяемый набор элементов, описывающий, как и где выполняется корутина. Диспетчер - один из этих элементов, отвечающий за поток.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(coroutineContext[ContinuationInterceptor] != null) // true
|
||||
println(coroutineContext[Job] != null) // true
|
||||
println(coroutineContext[CoroutineName]) // null — имени не задали
|
||||
|
||||
withContext(CoroutineName("загрузка")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // загрузка
|
||||
}
|
||||
}
|
||||
```
|
||||
Элементы складываются оператором `+`, а извлекаются по ключу как из `Map`. Ключ диспетчера называется `ContinuationInterceptor` - это более общее понятие «перехватчик продолжений», частным случаем которого является диспетчер. Ключ `CoroutineDispatcher` тоже существует, но помечен `@ExperimentalStdlibApi`.
|
||||
|
||||
## Из чего состоит контекст
|
||||
| Элемент | Зачем |
|
||||
|---|---|
|
||||
| `Job` | жизненный цикл, иерархия, отмена |
|
||||
| `CoroutineDispatcher` | на каком потоке выполнять |
|
||||
| `CoroutineName` | имя для отладки и логов |
|
||||
| `CoroutineExceptionHandler` | что делать с необработанной ошибкой |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
val context = Dispatchers.IO + CoroutineName("задача") + SupervisorJob()
|
||||
|
||||
withContext(context) {
|
||||
println(coroutineContext[CoroutineName]?.name) // задача
|
||||
println(coroutineContext[ContinuationInterceptor] == Dispatchers.IO) // true
|
||||
}
|
||||
}
|
||||
```
|
||||
Сложение не коммутативно по смыслу: при конфликте элементов одного типа побеждает **правый**.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun main() {
|
||||
val base: CoroutineContext = Dispatchers.IO + CoroutineName("первое")
|
||||
val combined = base + CoroutineName("второе")
|
||||
println(combined[CoroutineName]?.name) // второе
|
||||
}
|
||||
```
|
||||
Для двух диспетчеров подряд (`Dispatchers.IO + Dispatchers.Default`) компилятор выдаёт предупреждение о бессмысленности выражения - правило то же, побеждает правый, но писать так незачем.
|
||||
|
||||
## Стандартные диспетчеры
|
||||
| Диспетчер | Потоков | Для чего |
|
||||
|---|---|---|
|
||||
| `Dispatchers.Default` | по числу ядер (минимум 2) | вычисления, обработка данных |
|
||||
| `Dispatchers.IO` | до 64 (растёт по требованию) | блокирующий I/O: файлы, JDBC, legacy-HTTP |
|
||||
| `Dispatchers.Main` | один, платформенный | UI (Android, Swing, JavaFX) |
|
||||
| `Dispatchers.Unconfined` | любой, текущий | редкие случаи, не для продакшена |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("runBlocking: ${Thread.currentThread().name}")
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
println("Default: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
println("IO: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
|
||||
}
|
||||
// runBlocking: main
|
||||
// Default: true
|
||||
// IO: true
|
||||
}
|
||||
```
|
||||
Обратите внимание: и `Default`, и `IO` показывают `DefaultDispatcher-worker-N` - они **делят один пул потоков**. `IO` - это тот же пул с другим лимитом параллелизма, поэтому переключение между `Default` и `IO` дешёвое и не требует смены потока.
|
||||
|
||||
## Default против IO
|
||||
Выбор определяется одним вопросом: код считает или ждёт?
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// CPU-bound: больше потоков, чем ядер, только вредит
|
||||
suspend fun compute(): Long = withContext(Dispatchers.Default) {
|
||||
(1..1_000_000).sumOf { it.toLong() }
|
||||
}
|
||||
|
||||
// блокирующий I/O: поток простаивает, поэтому их нужно много
|
||||
suspend fun readBlocking(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // имитация JDBC или File.readText
|
||||
"прочитано"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(compute()) // 500000500000
|
||||
println(readBlocking()) // прочитано
|
||||
}
|
||||
```
|
||||
Логика лимитов: на `Default` потоков столько же, сколько ядер, потому что больше вычислять всё равно некому - лишние потоки только добавят переключений контекста. На `IO` потоки в основном спят в ожидании ответа, поэтому их держат до 64.
|
||||
|
||||
Если API уже `suspend` и не блокирует (Ktor-клиент, R2DBC, `kotlinx-io`), диспетчер менять не нужно вообще - это лишнее переключение.
|
||||
|
||||
## limitedParallelism
|
||||
Ограничивает число одновременных корутин, не создавая новых потоков:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val limited = Dispatchers.IO.limitedParallelism(2)
|
||||
val running = AtomicInteger(0)
|
||||
val maxRunning = AtomicInteger(0)
|
||||
|
||||
withContext(limited) {
|
||||
repeat(10) {
|
||||
launch {
|
||||
val now = running.incrementAndGet()
|
||||
maxRunning.updateAndGet { m -> maxOf(m, now) }
|
||||
Thread.sleep(20) // занимает поток
|
||||
running.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
println("одновременно выполнялось не больше 2: ${maxRunning.get() <= 2}")
|
||||
// одновременно выполнялось не больше 2: true
|
||||
}
|
||||
```
|
||||
Это замена старому `newFixedThreadPoolContext`: вместо отдельного пула используется общий, а ограничивается только параллелизм. Типичное применение - не перегрузить базу данных или внешний API.
|
||||
|
||||
Важная тонкость: лимит считает корутины, **занимающие поток**, а не существующие. Приостановленная корутина слот освобождает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val limited = Dispatchers.IO.limitedParallelism(2)
|
||||
val inFlight = AtomicInteger(0)
|
||||
val maxInFlight = AtomicInteger(0)
|
||||
|
||||
withContext(limited) {
|
||||
repeat(10) {
|
||||
launch {
|
||||
val now = inFlight.incrementAndGet()
|
||||
maxInFlight.updateAndGet { m -> maxOf(m, now) }
|
||||
delay(20) // слот освобождён
|
||||
inFlight.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
println("одновременно в полёте больше 2: ${maxInFlight.get() > 2}")
|
||||
// одновременно в полёте больше 2: true
|
||||
}
|
||||
```
|
||||
Поэтому `limitedParallelism` - не семафор на число запросов. Если нужно ограничить именно количество одновременных обращений к внешнему API (включая время ожидания ответа), нужен `Semaphore` из `kotlinx.coroutines.sync`. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## Dispatchers.Main
|
||||
```kotlin
|
||||
// Android или Swing — требует соответствующей зависимости
|
||||
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
|
||||
//
|
||||
// viewModelScope.launch { // уже на Main
|
||||
// val data = withContext(Dispatchers.IO) { repository.load() }
|
||||
// textView.text = data // снова на Main
|
||||
// }
|
||||
```
|
||||
Главный шаблон UI-программирования: корутина стартует на `Main`, тяжёлую работу уводит в `withContext`, результат обрабатывает снова на `Main` - без коллбэков и `runOnUiThread`. Без платформенной зависимости `Dispatchers.Main` бросит `IllegalStateException` при первом обращении.
|
||||
|
||||
## Unconfined
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(Dispatchers.Unconfined) {
|
||||
println("до delay: ${Thread.currentThread().name}")
|
||||
delay(10)
|
||||
println("после delay на потоке таймера: ${Thread.currentThread().name.contains("DefaultExecutor")}")
|
||||
}
|
||||
// до delay: main
|
||||
// после delay на потоке таймера: true
|
||||
}
|
||||
```
|
||||
`Unconfined` не привязывает корутину к потоку: до первой приостановки она идёт в текущем, а после возобновляется в том потоке, который её разбудил. Это ломает любые предположения о потоках, поэтому в продакшене не используется - только в тестах и специальных случаях.
|
||||
|
||||
## Наследование контекста
|
||||
Дочерняя корутина наследует контекст родителя, заменяя только `Job`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(Dispatchers.IO + CoroutineName("родитель")) {
|
||||
launch {
|
||||
println("имя: ${coroutineContext[CoroutineName]?.name}") // имя: родитель
|
||||
val inherited = coroutineContext[ContinuationInterceptor] == Dispatchers.IO
|
||||
println("диспетчер унаследован: $inherited") // диспетчер унаследован: true
|
||||
}
|
||||
launch(CoroutineName("ребёнок")) {
|
||||
println("переопределено: ${coroutineContext[CoroutineName]?.name}") // переопределено: ребёнок
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`Job` ребёнка всегда новый и становится дочерним по отношению к `Job` родителя - именно так строится дерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Контекст родителя:<br/>Job#1 + Dispatchers.IO + Name"] --> B["launch { }"]
|
||||
B --> C["Контекст ребёнка:<br/>Job#2 (parent = Job#1)<br/>+ Dispatchers.IO + Name"]
|
||||
A --> D["launch(Dispatchers.Default) { }"]
|
||||
D --> E["Контекст ребёнка:<br/>Job#3 (parent = Job#1)<br/>+ Dispatchers.Default + Name"]
|
||||
```
|
||||
|
||||
## Свои элементы контекста
|
||||
Элемент контекста - способ передать данные через всё дерево корутин:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class RequestId(val value: String) : AbstractCoroutineContextElement(RequestId) {
|
||||
companion object Key : CoroutineContext.Key<RequestId>
|
||||
}
|
||||
|
||||
suspend fun logWithId(message: String) {
|
||||
val id = coroutineContext[RequestId]?.value ?: "нет id"
|
||||
println("[$id] $message")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(RequestId("req-42")) {
|
||||
logWithId("начало обработки")
|
||||
launch { logWithId("из дочерней корутины") }
|
||||
}
|
||||
logWithId("вне контекста")
|
||||
// [req-42] начало обработки
|
||||
// [req-42] из дочерней корутины
|
||||
// [нет id] вне контекста
|
||||
}
|
||||
```
|
||||
Это корутинный аналог `ThreadLocal`, но работающий правильно при переключении потоков. Применяется для трассировки запросов, идентификаторов пользователя, локали.
|
||||
|
||||
## ThreadLocal и корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
val threadLocal = ThreadLocal<String?>()
|
||||
|
||||
fun main() = runBlocking {
|
||||
threadLocal.set("главный")
|
||||
println("до: ${threadLocal.get()}")
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
println("на другом потоке: ${threadLocal.get()}") // null — другой поток
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default + threadLocal.asContextElement("перенесённый")) {
|
||||
println("через asContextElement: ${threadLocal.get()}") // перенесённый
|
||||
}
|
||||
// до: главный
|
||||
// на другом потоке: null
|
||||
// через asContextElement: перенесённый
|
||||
}
|
||||
```
|
||||
`ThreadLocal` привязан к потоку, а корутина потоки меняет - поэтому напрямую он не работает. `asContextElement()` переносит значение при каждом переключении. Это важно для MDC в логировании и Spring Security.
|
||||
|
||||
## Проверка потока в отладке
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking(CoroutineName("корень")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // корень
|
||||
launch(CoroutineName("дочерняя")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // дочерняя
|
||||
}.join()
|
||||
}
|
||||
```
|
||||
Запуск с `-Dkotlinx.coroutines.debug` добавляет к имени потока идентификатор корутины (`main @корень#1`), что сильно упрощает чтение логов.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `ExecutorService` передаётся аргументом | диспетчер наследуется из контекста |
|
||||
> | `Executors.newFixedThreadPool(n)` | `Dispatchers.Default` / `limitedParallelism(n)` |
|
||||
> | `Executors.newCachedThreadPool()` | `Dispatchers.IO` |
|
||||
> | `SwingUtilities.invokeLater`, `runOnUiThread` | `Dispatchers.Main` |
|
||||
> | `ThreadLocal` | элемент контекста |
|
||||
> | `MDC` в логировании | `MDCContext` или свой элемент |
|
||||
> | пул = потоки | диспетчер = политика, потоки общие |
|
||||
>
|
||||
> **Пул потоков против диспетчера.** В Java каждый `ExecutorService` владеет своими потоками: четыре пула по 10 потоков - это 40 реальных потоков, даже если 35 из них простаивают. `Dispatchers.Default` и `Dispatchers.IO` делят один пул, а `limitedParallelism` ограничивает только число одновременных задач. Поэтому в Kotlin «ограничить параллелизм до 2 для работы с БД» не стоит двух простаивающих потоков.
|
||||
>
|
||||
> **Контекст наследуется, пул - нет.** В Java пул нужно передавать в каждый асинхронный вызов, и забытый аргумент молча уводит задачу в `ForkJoinPool.commonPool`:
|
||||
> ```java
|
||||
> CompletableFuture.supplyAsync(this::load); // commonPool!
|
||||
> CompletableFuture.supplyAsync(this::load, myExecutor); // правильно
|
||||
> ```
|
||||
> Эти две строки отличаются одним аргументом и дают совершенно разное поведение под нагрузкой. В Kotlin дочерняя корутина получает диспетчер родителя автоматически, а переопределение - явное и видимое.
|
||||
>
|
||||
> **`ThreadLocal` в асинхронном коде в Java не работает так же** - и там это обходят сложнее. При передаче задачи в пул `ThreadLocal` теряется; Spring решает это через `TaskDecorator` и копирование `SecurityContext`/MDC в каждом месте. В Kotlin элемент контекста путешествует вместе с корутиной по всему дереву, а `asContextElement()` переносит и настоящий `ThreadLocal`, когда он нужен библиотеке.
|
||||
>
|
||||
> **Переключение потока явное и дешёвое.** В Java чтобы вернуться в UI-поток, нужен `invokeLater` или `Handler.post` с коллбэком, который разрывает линейность кода. `withContext(Dispatchers.Main)` - это обычный блок, после которого выполнение продолжается на нужном потоке:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadAndRender(): String {
|
||||
> val data = withContext(Dispatchers.IO) { Thread.sleep(10); "данные" }
|
||||
> return "отрисовано: $data" // снова на исходном диспетчере
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(loadAndRender()) } // отрисовано: данные
|
||||
> ```
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокирующий вызов на `Dispatchers.Default` исчерпывает пул:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(2)) {
|
||||
> List(6) { launch { Thread.sleep(50) } }.forEach { it.join() }
|
||||
> }
|
||||
> }
|
||||
> println("сериализовалось вместо параллели: ${t >= 150}") // сериализовалось вместо параллели: true
|
||||
> }
|
||||
> ```
|
||||
> Шесть задач по 50 мс на двух потоках заняли 150 мс вместо 50. На реальном `Default` (число ядер) блокирующий код точно так же съедает все потоки, и вся программа встаёт - включая несвязанные корутины.
|
||||
>
|
||||
> **`Dispatchers.IO` не делает блокирующий код неблокирующим.** Он лишь даёт больше потоков. 64 одновременных JDBC-запроса всё равно упрутся в размер пула соединений, а 1000 корутин на `IO` встанут в очередь.
|
||||
>
|
||||
> **`Dispatchers.Main` падает без платформенной зависимости:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() {
|
||||
> val error = runCatching { runBlocking { withContext(Dispatchers.Main) { } } }
|
||||
> .exceptionOrNull()
|
||||
> println(error != null) // true
|
||||
> }
|
||||
> ```
|
||||
> Сообщение - «Module with the Main dispatcher is missing». Нужен `kotlinx-coroutines-android`, `-javafx` или `-swing`.
|
||||
>
|
||||
> **`newSingleThreadContext` создаёт настоящий поток, который нужно закрывать:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> @OptIn(DelicateCoroutinesApi::class)
|
||||
> val dispatcher = newSingleThreadContext("мой-поток")
|
||||
> try {
|
||||
> withContext(dispatcher) {
|
||||
> println(Thread.currentThread().name) // мой-поток
|
||||
> }
|
||||
> } finally {
|
||||
> dispatcher.close() // без этого поток живёт до конца процесса
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> API помечено `@DelicateCoroutinesApi` именно из-за этой утечки. Почти всегда вместо него нужен `limitedParallelism(1)`.
|
||||
>
|
||||
> **`Unconfined` возобновляется в неизвестном потоке.** Код после `delay` окажется на потоке таймера, после `Channel.receive` - на потоке отправителя. Любая работа с UI или `ThreadLocal` при этом ломается непредсказуемо.
|
||||
>
|
||||
> **`withContext` внутри цикла - лишние переключения:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val inside = measureTimeMillis {
|
||||
> repeat(10_000) { withContext(Dispatchers.Default) { it } }
|
||||
> }
|
||||
> val outside = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default) { repeat(10_000) { } }
|
||||
> }
|
||||
> println("снаружи быстрее: ${outside < inside}") // снаружи быстрее: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Передача `Job` в `withContext` рвёт структурированность.** `withContext(Dispatchers.IO + Job())` создаёт корутину без родителя - отмена сверху до неё не дойдёт. Передавайте только диспетчер и имя.
|
||||
>
|
||||
> **Контекст неизменяем, `+` создаёт новый.** `context + CoroutineName("x")` не меняет исходный объект; забытое присваивание результата - тихая ошибка.
|
||||
>
|
||||
> **`ThreadLocal` без `asContextElement` теряется при любом переключении**, в том числе при возобновлении после `delay` на другом потоке. Для MDC в логах это значит, что идентификатор запроса исчезнет из части записей - используйте `MDCContext` из `kotlinx-coroutines-slf4j`.
|
||||
>
|
||||
> **Число потоков `IO` настраивается только через системное свойство** (`kotlinx.coroutines.io.parallelism`) и влияет на весь процесс. Для ограничения конкретного места используйте `limitedParallelism`.
|
||||
>
|
||||
> **Переключение диспетчера для неблокирующего API - чистая потеря.** Если библиотека уже `suspend` и не блокирует, `withContext(Dispatchers.IO)` вокруг неё добавляет переключение контекста и ничего не даёт.
|
||||
|
||||
Далее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
@@ -1,529 +0,0 @@
|
||||
---
|
||||
title: Исключения в корутинах
|
||||
description: Как ошибки идут по дереву, разница launch и async, SupervisorJob, CoroutineExceptionHandler и CancellationException
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Обработка ошибок в корутинах отличается от обычной именно потому, что корутина - не вызов функции: исключение возникает не там, где его ждёт `try`, а внутри другой корутины.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
launch { throw IllegalStateException("ошибка внутри") }
|
||||
}
|
||||
}
|
||||
println(result.exceptionOrNull()?.message) // ошибка внутри
|
||||
}
|
||||
```
|
||||
Ошибка из `launch` не выбрасывается в месте вызова `launch` - она идёт **вверх по дереву** и проявляется на границе области.
|
||||
|
||||
## Два механизма: launch и async
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// launch: ошибка идёт родителю сразу
|
||||
val fromLaunch = runCatching {
|
||||
coroutineScope { launch { error("из launch") } }
|
||||
}.exceptionOrNull()?.message
|
||||
|
||||
// async: ошибка хранится в Deferred и выбрасывается при await
|
||||
val fromAsync = runCatching {
|
||||
coroutineScope {
|
||||
val d = async { error("из async") }
|
||||
d.await()
|
||||
}
|
||||
}.exceptionOrNull()?.message
|
||||
|
||||
println("$fromLaunch | $fromAsync") // из launch | из async
|
||||
}
|
||||
```
|
||||
Разница в том, **когда** вы увидите ошибку:
|
||||
|
||||
| | `launch` | `async` |
|
||||
|---|---|---|
|
||||
| куда идёт ошибка | сразу родителю | сохраняется в `Deferred` |
|
||||
| где ловить | `try` вокруг области, обработчик | `try` вокруг `await()` |
|
||||
| если не обработать | падает область | при `coroutineScope` - тоже падает область |
|
||||
| если `async` висит в supervisor | - | ошибка потеряется совсем |
|
||||
|
||||
Ключевой момент: `async` в обычной области **всё равно** передаёт ошибку родителю, даже без `await()`. Исключение в этом случае попадает в родителя при завершении области.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
async { error("await не вызывали") }
|
||||
delay(20)
|
||||
"тело дошло до конца"
|
||||
}
|
||||
}
|
||||
println(result.exceptionOrNull()?.message) // await не вызывали
|
||||
}
|
||||
```
|
||||
А вот в supervisor-области непрочитанная ошибка `async` исчезает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
supervisorScope {
|
||||
async { error("никто не узнает") } // await не вызван
|
||||
delay(20)
|
||||
println("ни падения, ни лога")
|
||||
}
|
||||
// ни падения, ни лога
|
||||
}
|
||||
```
|
||||
Это самый тихий способ потерять ошибку в корутинах.
|
||||
|
||||
## Как ошибка идёт по дереву
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
launch { // уровень 1
|
||||
launch { // уровень 2
|
||||
launch { // уровень 3
|
||||
throw IllegalStateException("с третьего уровня")
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
try { delay(1000) } finally { println("соседнюю ветку отменили") }
|
||||
}
|
||||
}
|
||||
}
|
||||
println("дошло до корня: ${result.exceptionOrNull()?.message}")
|
||||
// соседнюю ветку отменили
|
||||
// дошло до корня: с третьего уровня
|
||||
}
|
||||
```
|
||||
Механика: корутина, получившая ошибку, отменяет себя, передаёт исключение родителю, тот отменяет **всех остальных детей** и повторяет процедуру выше. Промежуточные уровни не могут «не пропустить» ошибку - её нужно ловить `try/catch` внутри самой корутины.
|
||||
|
||||
```mermaid
|
||||
graph BT
|
||||
D["Ребёнок уровня 3<br/>throw"] -->|"ошибка наверх"| C["Ребёнок уровня 2"]
|
||||
C -->|"ошибка наверх"| B["Ребёнок уровня 1"]
|
||||
B -->|"ошибка наверх"| A["Корень области"]
|
||||
A -->|"отменить"| E["Соседняя ветка"]
|
||||
A --> F{"есть обработчик?"}
|
||||
F -->|"да"| G["CoroutineExceptionHandler"]
|
||||
F -->|"нет"| H["Thread.uncaughtExceptionHandler<br/>падение"]
|
||||
```
|
||||
|
||||
## Где try/catch работает
|
||||
Внутри самой корутины - как обычно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
launch {
|
||||
try {
|
||||
error("локальная ошибка")
|
||||
} catch (e: IllegalStateException) {
|
||||
println("обработали на месте: ${e.message}")
|
||||
}
|
||||
}.join()
|
||||
println("родитель не затронут")
|
||||
// обработали на месте: локальная ошибка
|
||||
// родитель не затронут
|
||||
}
|
||||
```
|
||||
А вокруг `launch` - нет:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch(SupervisorJob()) { // отдельный Job, чтобы не уронить main
|
||||
try {
|
||||
launch { error("из вложенного launch") }
|
||||
} catch (e: Exception) {
|
||||
println("этого не будет")
|
||||
}
|
||||
}
|
||||
job.join()
|
||||
println("родитель выжил: ${job.isCancelled}")
|
||||
// родитель выжил: true
|
||||
}
|
||||
```
|
||||
`launch` возвращается мгновенно, до того как тело начало выполняться - ловить там нечего.
|
||||
|
||||
## SupervisorJob
|
||||
Супервизор меняет направление: ошибка ребёнка не идёт вверх.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e ->
|
||||
println("обработчик: ${e.message}")
|
||||
}
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
scope.launch { error("первая задача упала") }
|
||||
delay(20)
|
||||
scope.launch { println("вторая работает как ни в чём не бывало") }
|
||||
delay(20)
|
||||
println("scope жив: ${scope.isActive}")
|
||||
scope.cancel()
|
||||
// обработчик: первая задача упала
|
||||
// вторая работает как ни в чём не бывало
|
||||
// scope жив: true
|
||||
}
|
||||
```
|
||||
Сравните с обычным `Job`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("обработчик: ${e.message}") }
|
||||
val scope = CoroutineScope(Job() + handler)
|
||||
|
||||
scope.launch { error("первая задача упала") }
|
||||
delay(20)
|
||||
val second = scope.launch { println("не запустится") }
|
||||
delay(20)
|
||||
println("scope мёртв: ${!scope.isActive}, вторая отменена: ${second.isCancelled}")
|
||||
// обработчик: первая задача упала
|
||||
// scope мёртв: true, вторая отменена: true
|
||||
}
|
||||
```
|
||||
Правило: долгоживущий scope компонента - `SupervisorJob`; группа задач внутри одной операции - обычный `Job` через `coroutineScope`.
|
||||
|
||||
## supervisorScope против coroutineScope
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadAllOrFail(): String = coroutineScope {
|
||||
val a = async { delay(20); "A" }
|
||||
val b = async { error("B упал"); "B" }
|
||||
"${a.await()}${b.await()}"
|
||||
}
|
||||
|
||||
suspend fun loadWhatCan(): List<String> = supervisorScope {
|
||||
val a = async { delay(20); "A" }
|
||||
val b = async<String> { error("B упал") }
|
||||
listOf(
|
||||
runCatching { a.await() }.getOrElse { "?" },
|
||||
runCatching { b.await() }.getOrElse { "?" },
|
||||
)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(runCatching { loadAllOrFail() }.exceptionOrNull()?.message) // B упал
|
||||
println(loadWhatCan()) // [A, ?]
|
||||
}
|
||||
```
|
||||
Первый вариант - «нужны все данные», второй - «покажем что есть». В supervisor-области каждый `await()` нужно обрабатывать отдельно, иначе ошибка улетит в обработчик по умолчанию.
|
||||
|
||||
## CoroutineExceptionHandler
|
||||
Это «последняя линия» - обработчик необработанных ошибок, аналог `Thread.UncaughtExceptionHandler`.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { context, e ->
|
||||
println("необработанная ошибка в ${context[CoroutineName]?.name}: ${e.message}")
|
||||
}
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
scope.launch(CoroutineName("загрузчик")) { error("сеть недоступна") }
|
||||
delay(20)
|
||||
scope.cancel()
|
||||
// необработанная ошибка в загрузчик: сеть недоступна
|
||||
}
|
||||
```
|
||||
Три правила, которые не очевидны:
|
||||
|
||||
**1. Обработчик работает только в корневой корутине.** В дочерней он игнорируется:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("вызван: ${e.message}") }
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
scope.launch {
|
||||
launch(handler) { error("ошибка в ребёнке") } // handler проигнорирован
|
||||
}
|
||||
delay(50)
|
||||
scope.cancel()
|
||||
// Exception in thread ... IllegalStateException: ошибка в ребёнке
|
||||
}
|
||||
```
|
||||
Ошибка ушла наверх, а наверху обработчика нет. Ставить `handler` нужно в контекст scope или корневого билдера.
|
||||
|
||||
**2. `async` обработчик не использует** - его ошибка принадлежит `Deferred`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("не будет вызван") }
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
val d = scope.async<Int> { error("ошибка в async") }
|
||||
delay(20)
|
||||
println("ошибка ждёт в Deferred: ${runCatching { d.await() }.isFailure}")
|
||||
scope.cancel()
|
||||
// ошибка ждёт в Deferred: true
|
||||
}
|
||||
```
|
||||
|
||||
**3. `runBlocking` обработчик не использует** - он выбрасывает исключение наружу, в вызывающий поток.
|
||||
|
||||
## CancellationException - не ошибка
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
launch {
|
||||
delay(1000)
|
||||
}.cancel() // отмена ребёнка
|
||||
delay(20)
|
||||
println("родитель продолжает работу")
|
||||
}
|
||||
job.join()
|
||||
println("родитель завершился нормально: ${!job.isCancelled}")
|
||||
// родитель продолжает работу
|
||||
// родитель завершился нормально: true
|
||||
}
|
||||
```
|
||||
`CancellationException`, выброшенное корутиной, трактуется как «я завершилась по запросу» и наверх не передаётся. Именно поэтому `withTimeout` внутри одной корутины не роняет соседей. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
|
||||
Обратная сторона: `CancellationException` нельзя использовать как обычное исключение - оно будет молча проигнорировано:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch(SupervisorJob()) {
|
||||
throw CancellationException("это не ошибка, а отмена")
|
||||
}
|
||||
job.join()
|
||||
println("отменён, а не упал: ${job.isCancelled}") // отменён, а не упал: true
|
||||
}
|
||||
```
|
||||
|
||||
## Агрегация ошибок
|
||||
Родитель получает **первое** исключение. Ошибки, возникшие уже во время отмены - например, в блоке `finally` соседней корутины - не теряются, а попадают в `suppressed`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val error = runCatching {
|
||||
coroutineScope {
|
||||
launch { delay(10); throw IllegalStateException("первая") }
|
||||
launch {
|
||||
try { delay(1000) } finally { throw IllegalArgumentException("из finally") }
|
||||
}
|
||||
}
|
||||
}.exceptionOrNull()
|
||||
|
||||
println("основная: ${error?.message}")
|
||||
println("подавленных: ${error?.suppressed?.size}")
|
||||
println("подавленная: ${error?.suppressed?.firstOrNull()?.message}")
|
||||
// основная: первая
|
||||
// подавленных: 1
|
||||
// подавленная: из finally
|
||||
}
|
||||
```
|
||||
Важно, чего здесь **нет**: если бы вторая корутина просто бросила исключение через 30 мс, подавленной ошибки не появилось бы - её отменили бы на 10-й миллисекунде, и она никогда не дошла бы до `throw`. Fail-fast означает именно это: вторая ошибка обычно не возникает, потому что работу остановили. В `suppressed` попадают только сбои очистки.
|
||||
|
||||
Практический вывод для логирования: печатать нужно полный стектрейс (`printStackTrace()` или `logger.error(msg, e)`), а не `e.message` - иначе сбои в `finally` останутся невидимыми.
|
||||
|
||||
## Повторные попытки
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun <T> retry(times: Int, delayMs: Long, block: suspend () -> T): T {
|
||||
var last: Throwable? = null
|
||||
repeat(times) { attempt ->
|
||||
try {
|
||||
return block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e // отмену не ретраим!
|
||||
} catch (e: Exception) {
|
||||
last = e
|
||||
println("попытка ${attempt + 1} не удалась: ${e.message}")
|
||||
delay(delayMs)
|
||||
}
|
||||
}
|
||||
throw last ?: IllegalStateException("нет попыток")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var attempts = 0
|
||||
val result = retry(3, 10) {
|
||||
attempts++
|
||||
if (attempts < 3) error("сеть недоступна") else "данные получены"
|
||||
}
|
||||
println(result)
|
||||
// попытка 1 не удалась: сеть недоступна
|
||||
// попытка 2 не удалась: сеть недоступна
|
||||
// данные получены
|
||||
}
|
||||
```
|
||||
Строка `catch (e: CancellationException) { throw e }` обязательна в любом обобщённом обработчике ошибок в корутинах - без неё функция станет неотменяемой.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | исключение в `submit` → в `Future` | ошибка `launch` → родителю |
|
||||
> | `Future.get()` → `ExecutionException` | `await()` → исходное исключение |
|
||||
> | `CompletableFuture.exceptionally` | `try/catch` вокруг `await` |
|
||||
> | `Thread.UncaughtExceptionHandler` | `CoroutineExceptionHandler` |
|
||||
> | `InterruptedException` (проверяемое) | `CancellationException` (непроверяемое) |
|
||||
> | ошибка задачи не влияет на пул | ошибка отменяет область |
|
||||
> | нет аналога | `SupervisorJob` |
|
||||
>
|
||||
> **Ошибку в Java легко потерять молча.** `executor.submit(task)` складывает исключение в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто - ни логов, ни падения:
|
||||
> ```java
|
||||
> executor.submit(() -> { throw new RuntimeException("исчезнет"); });
|
||||
> // тишина
|
||||
> ```
|
||||
> Корутины этого не допускают: ошибка `launch` обязательно дойдёт до родителя, а если никто не обработал - до `CoroutineExceptionHandler` или `Thread.uncaughtExceptionHandler`. Исключение одно - непрочитанный `async` в supervisor-области, и это считается ошибкой программиста.
|
||||
>
|
||||
> **`get()` оборачивает исключение, `await()` - нет.** В Java настоящая причина лежит внутри `ExecutionException`, поэтому обработка выглядит так:
|
||||
> ```java
|
||||
> try {
|
||||
> future.get();
|
||||
> } catch (ExecutionException e) {
|
||||
> if (e.getCause() instanceof IOException io) { /* ... */ }
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `await()` бросает то же исключение, что было выброшено внутри, поэтому `catch (e: IOException)` работает напрямую:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import java.io.IOException
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val caught = try {
|
||||
> coroutineScope { async { throw IOException("диск") }.await() }
|
||||
> } catch (e: IOException) {
|
||||
> "поймали ${e::class.simpleName}: ${e.message}"
|
||||
> }
|
||||
> println(caught) // поймали IOException: диск
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`exceptionally`/`handle` против `try/catch`.** В Java обработка ошибок в асинхронной цепочке - отдельный метод с лямбдой, и правила распространения ошибки по цепочке приходится помнить (`exceptionally` ловит, `thenApply` пропускает, `whenComplete` не меняет результат). В Kotlin работает обычный `try/catch`, с теми же правилами, что в синхронном коде.
|
||||
>
|
||||
> **Отмена пула против отмены области.** В Java ошибка в одной задаче никак не влияет на остальные - пул продолжает работу. Это и плюс (изоляция), и минус (бессмысленное продолжение работы, часть которой уже провалилась). Kotlin даёт выбор явно: `coroutineScope` - fail-fast, `supervisorScope` - изоляция как в Java.
|
||||
>
|
||||
> **`CancellationException` непроверяемое, `InterruptedException` - нет.** В Java прерывание приходится обрабатывать в каждом блокирующем вызове, и типичная ошибка - проглотить его, не восстановив флаг. В Kotlin отмену не нужно объявлять, но проглотить её так же легко через `catch (e: Exception)`.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`try/catch` вокруг `launch` ничего не ловит:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) {
|
||||
> try {
|
||||
> launch { error("внутри") }
|
||||
> } catch (e: Exception) {
|
||||
> println("не напечатается")
|
||||
> }
|
||||
> delay(50)
|
||||
> }
|
||||
> job.join()
|
||||
> println("ошибка ушла наверх, а не в catch")
|
||||
> // ошибка ушла наверх, а не в catch
|
||||
> }
|
||||
> ```
|
||||
> `launch` возвращается до выполнения тела. Ловить нужно **внутри** лямбды или вокруг `coroutineScope`.
|
||||
>
|
||||
> **`catch (e: Exception)` перехватывает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } catch (e: Exception) {
|
||||
> println("отмена попала в общий catch: ${e is CancellationException}")
|
||||
> }
|
||||
> println("и работа продолжилась")
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> // отмена попала в общий catch: true
|
||||
> // и работа продолжилась
|
||||
> }
|
||||
> ```
|
||||
> Всегда ставьте `catch (e: CancellationException) { throw e }` первым.
|
||||
>
|
||||
> **`runCatching` в корутинах опасен** по той же причине - он ловит `Throwable`, включая `CancellationException`. В `suspend`-коде это делает блок неотменяемым, и `cancel()` перестаёт работать. Используйте явный `try/catch` с пробросом отмены.
|
||||
>
|
||||
> **`CoroutineExceptionHandler` в дочерней корутине игнорируется.** Он учитывается только у корневой корутины области. Передавать его нужно при создании scope или в корневой `launch`.
|
||||
>
|
||||
> **`CoroutineExceptionHandler` не работает с `async`** - его ошибка принадлежит `Deferred` и ждёт `await()`. Если `await()` не вызван в supervisor-области, ошибка исчезает навсегда.
|
||||
>
|
||||
> **`SupervisorJob`, переданный в `launch`, не делает детей независимыми:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) { // supervisor — для ЭТОЙ корутины
|
||||
> launch { delay(10); error("первый ребёнок") }
|
||||
> launch {
|
||||
> try { delay(1000) } finally { println("второго тоже отменили") }
|
||||
> }
|
||||
> }
|
||||
> job.join()
|
||||
> // второго тоже отменили
|
||||
> }
|
||||
> ```
|
||||
> `SupervisorJob` в контексте билдера становится родителем самой корутины, но внутри её тела работает обычная область. Нужен `supervisorScope { }`.
|
||||
>
|
||||
> **`supervisorScope` не защищает код своего тела:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val caught = runCatching {
|
||||
> supervisorScope {
|
||||
> launch { delay(1000) }
|
||||
> error("ошибка в теле блока")
|
||||
> }
|
||||
> }.exceptionOrNull()?.message
|
||||
> println(caught) // ошибка в теле блока
|
||||
> }
|
||||
> ```
|
||||
> Supervisor меняет поведение только для детей.
|
||||
>
|
||||
> **Ошибка внутри `CoroutineExceptionHandler` роняет процесс.** Обработчик - последняя линия, выше его некуда. Логирование внутри него должно быть защищено от собственных сбоев.
|
||||
>
|
||||
> **`finally` может не выполниться при падении JVM.** Если необработанная ошибка дошла до `Thread.uncaughtExceptionHandler` и процесс завершается, часть блоков очистки в других корутинах может не успеть.
|
||||
>
|
||||
> **Подавленные исключения легко не заметить.** При нескольких одновременных ошибках `e.message` покажет только первую. В логах нужен полный `printStackTrace()` или `e.suppressed`.
|
||||
>
|
||||
> **Проброс через несколько уровней нельзя перехватить в середине.** Промежуточный `launch` не видит ошибку ребёнка как исключение - он просто отменяется. Чтобы обработать её на этом уровне, нужен `try/catch` вокруг `coroutineScope` внутри промежуточной корутины.
|
||||
>
|
||||
> **`CancellationException` как бизнес-ошибка не работает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class ValidationError(message: String) : CancellationException(message)
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) { throw ValidationError("неверные данные") }
|
||||
> job.join()
|
||||
> println("ошибка проигнорирована: отменён=${job.isCancelled}")
|
||||
> // ошибка проигнорирована: отменён=true
|
||||
> }
|
||||
> ```
|
||||
> Наследование от `CancellationException` превращает исключение в «тихую отмену»: ни обработчика, ни падения. Для бизнес-ошибок наследуйтесь от `Exception`.
|
||||
|
||||
Далее: [Flow](Flow.md)
|
||||
@@ -1,344 +0,0 @@
|
||||
---
|
||||
title: Корутины против потоков
|
||||
description: Реальная стоимость потока и корутины, сравнение с виртуальными потоками Java и случаи, когда нужны именно потоки
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Поток - ресурс ОС: стек в памяти, запись в планировщике ядра, переключение через системный вызов. Корутина - объект в куче, переключение которого - обычный вызов метода.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val coroutines = measureTimeMillis {
|
||||
List(100_000) { launch { delay(100) } }.forEach { it.join() }
|
||||
}
|
||||
println("100 000 корутин: уложились в секунду: ${coroutines < 1000}")
|
||||
// 100 000 корутин: уложились в секунду: true
|
||||
}
|
||||
```
|
||||
Сто тысяч потоков создать нельзя - JVM упадёт с `OutOfMemoryError` ещё на нескольких десятках тысяч.
|
||||
|
||||
## Стоимость в цифрах
|
||||
| Характеристика | Поток ОС | Корутина |
|
||||
|---|---|---|
|
||||
| память | около 1 МБ стека (`-Xss`) | десятки-сотни байт |
|
||||
| создание | системный вызов, ~50-100 мкс | аллокация объекта, ~100 нс |
|
||||
| переключение | через ядро, 1-10 мкс | вызов метода, ~10 нс |
|
||||
| разумный максимум | тысячи | миллионы |
|
||||
| планировщик | ядро ОС | библиотека, в пользовательском коде |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val threadTime = measureTimeMillis {
|
||||
List(1000) { Thread { Thread.sleep(10) } }.onEach { it.start() }.forEach { it.join() }
|
||||
}
|
||||
val coroutineTime = measureTimeMillis {
|
||||
List(1000) { launch { delay(10) } }.forEach { it.join() }
|
||||
}
|
||||
println("1000 потоков: ${threadTime} мс, 1000 корутин: ${coroutineTime} мс")
|
||||
println("корутины быстрее: ${coroutineTime < threadTime}")
|
||||
// корутины быстрее: true
|
||||
}
|
||||
```
|
||||
Разница здесь не в самом ожидании (оба спят по 10 мс), а в накладных расходах на создание тысячи объектов ядра.
|
||||
|
||||
## Где именно экономия
|
||||
Корутины не делают работу быстрее - они устраняют простой потоков.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
// блокировка: 4 задачи на 1 потоке = 4 × 100 мс
|
||||
val blocking = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { launch { Thread.sleep(100) } }.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
// приостановка: 4 задачи на 1 потоке = 100 мс
|
||||
val suspending = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { launch { delay(100) } }.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
println("блокировка около 400 мс: ${blocking >= 380}")
|
||||
println("приостановка около 100 мс: ${suspending < 200}")
|
||||
// блокировка около 400 мс: true
|
||||
// приостановка около 100 мс: true
|
||||
}
|
||||
```
|
||||
Тот же один поток, та же работа - разница в четыре раза. Именно это окупает корутины на сервере, обслуживающем много одновременных запросов: поток не стоит в ожидании ответа базы, а берёт следующий запрос.
|
||||
|
||||
## Корутины не помогают при счёте
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun heavy(): Long = (1..20_000_000L).sum()
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis { repeat(4) { heavy() } }
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
withContext(Dispatchers.Default) {
|
||||
List(4) { async { heavy() } }.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
val oneThread = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { async { heavy() } }.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
println("параллельно на ядрах быстрее: ${parallel < sequential}")
|
||||
println("на одном потоке выигрыша нет: ${oneThread >= parallel}")
|
||||
// параллельно на ядрах быстрее: true
|
||||
// на одном потоке выигрыша нет: true
|
||||
}
|
||||
```
|
||||
Вывод: для вычислений выигрыш даёт только параллелизм по ядрам, а не количество корутин. Тысяча корутин на четырёх ядрах считает ровно столько же, сколько четыре. Корутины здесь удобны как способ организации кода, а не как ускорение.
|
||||
|
||||
## Что происходит при блокировке
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val cores = Runtime.getRuntime().availableProcessors()
|
||||
println("ядер в системе: ${cores > 0}")
|
||||
|
||||
val finished = withTimeoutOrNull(300) {
|
||||
withContext(Dispatchers.Default) {
|
||||
// занимаем все потоки Default блокирующими вызовами
|
||||
List(cores) { launch { Thread.sleep(500) } }
|
||||
launch { println("эта корутина не получит поток вовремя") }
|
||||
}
|
||||
true
|
||||
}
|
||||
println("диспетчер оказался парализован: ${finished == null}")
|
||||
// ядер в системе: true
|
||||
// диспетчер оказался парализован: true
|
||||
}
|
||||
```
|
||||
Это главный практический риск корутин: один блокирующий вызов в неправильном диспетчере останавливает несвязанные задачи. Поток, заблокированный на `Thread.sleep` или JDBC, не может выполнять другие корутины. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
|
||||
## Виртуальные потоки Java
|
||||
Project Loom решает ту же задачу иначе: делает блокировку дешёвой.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("JVM: ${Runtime.version().feature()}")
|
||||
// Java 21+: Thread.ofVirtual().start { ... } — виртуальный поток,
|
||||
// который при блокирующем вызове освобождает несущий поток ОС.
|
||||
// Thread.sleep(1000) на нём стоит почти столько же, сколько delay(1000) в корутине.
|
||||
}
|
||||
```
|
||||
Сравнение подходов:
|
||||
|
||||
| | Корутины | Виртуальные потоки |
|
||||
|---|---|---|
|
||||
| где видно асинхронность | в типе: `suspend` | нигде, код обычный |
|
||||
| существующий блокирующий код | нужно переписать | работает как есть |
|
||||
| отмена | по дереву, встроена | `interrupt`, вручную |
|
||||
| структурированность | обязательна | `StructuredTaskScope`, опционально |
|
||||
| потоки данных | `Flow` | нет аналога, нужен Reactive Streams |
|
||||
| платформы | JVM, Android, Native, JS, Wasm | только JVM 21+ |
|
||||
| `ThreadLocal` | нужен `asContextElement` | работает (но дорог при миллионах) |
|
||||
| готовность экосистемы | библиотеки с `suspend` API | любая блокирующая библиотека |
|
||||
|
||||
**Главный плюс Loom** - обратная совместимость: миллионы строк блокирующего кода и библиотек (JDBC, Apache HttpClient, старые ORM) внезапно масштабируются без изменений.
|
||||
|
||||
**Главный плюс корутин** - асинхронность видна компилятору. Из `suspend` в сигнатуре вырастают структурированная конкурентность, отмена по дереву, `Flow` с backpressure и проверка «это нельзя вызвать из UI-потока» на этапе компиляции. С виртуальными потоками компилятор не знает, где функция заблокируется, поэтому такие гарантии недоступны.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// в сигнатуре видно, что функция может приостановиться
|
||||
suspend fun loadUser(id: Int): String {
|
||||
delay(10)
|
||||
return "пользователь $id"
|
||||
}
|
||||
|
||||
// а здесь не видно ничего: может считать 1 нс, может ждать сеть 10 с
|
||||
fun loadUserBlocking(id: Int): String {
|
||||
Thread.sleep(10)
|
||||
return "пользователь $id"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadUser(1))
|
||||
println(loadUserBlocking(2))
|
||||
// пользователь 1
|
||||
// пользователь 2
|
||||
}
|
||||
```
|
||||
Второй вариант нельзя безопасно вызвать из UI-потока, и ни компилятор, ни тип этого не подскажут.
|
||||
|
||||
Для Android вопрос не стоит: виртуальных потоков там нет, корутины - стандарт. Для серверного Kotlin на JVM 21+ оба подхода рабочие, и выбор определяется экосистемой: Ktor и Exposed дают `suspend` API, Spring MVC и JDBC - блокирующий.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужна конкурентность"] --> B{"Платформа?"}
|
||||
B -->|"Android, Native, JS"| C["корутины"]
|
||||
B -->|"JVM 21+"| D{"Код и библиотеки?"}
|
||||
D -->|"блокирующие: JDBC,<br/>Spring MVC"| E["виртуальные потоки<br/>или корутины + IO"]
|
||||
D -->|"suspend API: Ktor,<br/>R2DBC"| C
|
||||
A --> F{"Нужны потоки данных,<br/>отмена по дереву?"}
|
||||
F -->|"да"| C
|
||||
```
|
||||
|
||||
## Когда нужны именно потоки
|
||||
Корутины не универсальны.
|
||||
|
||||
**1. Блокирующий код, который нельзя переписать.** JDBC, `java.io`, нативные библиотеки через JNI. Их место - `Dispatchers.IO` или отдельный пул:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun queryDatabase(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // здесь мог быть JDBC
|
||||
"строка из БД"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(queryDatabase()) // строка из БД
|
||||
}
|
||||
```
|
||||
|
||||
**2. Код, требующий привязки к потоку.** Некоторые библиотеки используют `ThreadLocal` или требуют вызовов из одного и того же потока (OpenGL, часть UI-тулкитов, нативные SDK):
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
val dedicated = newSingleThreadContext("нативный-поток")
|
||||
try {
|
||||
withContext(dedicated) {
|
||||
println("всегда один и тот же поток: ${Thread.currentThread().name}")
|
||||
}
|
||||
} finally {
|
||||
dedicated.close()
|
||||
}
|
||||
// всегда один и тот же поток: нативный-поток
|
||||
}
|
||||
```
|
||||
|
||||
**3. Приоритеты и реальное время.** `Thread.setPriority`, потоки реального времени, привязка к ядру - у корутин этого нет вообще.
|
||||
|
||||
**4. Бесконечный цикл без точек приостановки.** Корутина, которая никогда не приостанавливается, занимает поток диспетчера полностью, и её нельзя отменить. Для такой задачи нужен выделенный поток.
|
||||
|
||||
## Измерение: сколько живёт корутина
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val before = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() }
|
||||
|
||||
val jobs = List(200_000) { launch { delay(5000) } }
|
||||
|
||||
val after = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() }
|
||||
val perCoroutine = (after - before) / 200_000
|
||||
|
||||
println("200 000 корутин создано: ${jobs.size}")
|
||||
println("байт на корутину (порядок величины): ${perCoroutine in 1..5000}")
|
||||
jobs.forEach { it.cancel() }
|
||||
// 200 000 корутин создано: 200000
|
||||
// байт на корутину (порядок величины): true
|
||||
}
|
||||
```
|
||||
Измерение грубое (GC работает параллельно), но порядок виден: сотни байт против мегабайта у потока. Именно поэтому «корутина на каждый запрос» или «корутина на каждое соединение» - нормальная архитектура, а «поток на каждый запрос» перестаёт работать на нескольких тысячах.
|
||||
|
||||
## Выбор на практике
|
||||
| Задача | Решение |
|
||||
|---|---|
|
||||
| много одновременных сетевых запросов | корутины |
|
||||
| обработка HTTP-запросов на сервере | корутины или виртуальные потоки |
|
||||
| асинхронность в Android | корутины, других вариантов нет |
|
||||
| параллельные вычисления | `Dispatchers.Default`, корутин по числу ядер |
|
||||
| JDBC и блокирующие драйверы | корутины + `Dispatchers.IO`, либо виртуальные потоки |
|
||||
| нативная библиотека с привязкой к потоку | выделенный поток |
|
||||
| задача с приоритетом ОС | поток |
|
||||
| поток событий с backpressure | `Flow` |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `new Thread()` | `launch` |
|
||||
> | `ExecutorService` | `CoroutineScope` + диспетчер |
|
||||
> | `Thread.sleep` (блокирует) | `delay` (приостанавливает) |
|
||||
> | `Thread.ofVirtual()` (Java 21+) | корутины |
|
||||
> | `StructuredTaskScope` (Java 21+) | `coroutineScope` |
|
||||
> | `ThreadLocal` | элемент контекста корутины |
|
||||
> | «поток на запрос» не масштабируется | «корутина на запрос» - норма |
|
||||
>
|
||||
> **Модель «поток на запрос» и её предел.** Классический Java-сервер выделяет поток на каждое соединение. При 10 000 одновременных соединений это 10 000 потоков, около 10 ГБ стеков и планировщик ядра, занятый переключениями. Именно отсюда выросли NIO, Netty и реактивное программирование - с их колбэками и нечитаемыми стектрейсами. Корутины дают масштабируемость NIO, сохраняя линейный код:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun handleRequest(id: Int): String {
|
||||
> delay(10) // здесь мог быть запрос к БД
|
||||
> return "ответ $id"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val responses = List(10_000) { async { handleRequest(it) } }.awaitAll()
|
||||
> println("обработано запросов: ${responses.size}") // обработано запросов: 10000
|
||||
> }
|
||||
> ```
|
||||
> Десять тысяч «обработчиков запросов» на несколько потоков - в Java до Loom это требовало полностью асинхронного стека.
|
||||
>
|
||||
> **Виртуальные потоки - ответ Java на ту же проблему.** `Thread.ofVirtual().start { }` создаёт поток, который при блокирующем вызове освобождает несущий поток ОС. По стоимости это сравнимо с корутиной, а главное преимущество - существующий блокирующий код работает без изменений. Цена - отсутствие информации об асинхронности в типах, из-за чего нет ни отмены по дереву, ни `Flow`, ни проверок на этапе компиляции. Loom добирает часть этого отдельными API (`StructuredTaskScope`), но они опциональны и появились на 8 лет позже корутин.
|
||||
>
|
||||
> **`CompletableFuture` - промежуточное решение, которое проиграло оба раза.** Он даёт неблокирующий код, но ценой разрыва линейности: лямбды, протаскивание переменных через замыкания, неудобные `if` и циклы. Корутины и виртуальные потоки решают задачу так, что код остаётся последовательным - просто разными способами.
|
||||
>
|
||||
> **`ThreadLocal` стоит дорого при миллионе потоков.** Это ограничение Loom, о котором говорят меньше: `ThreadLocal` на миллион виртуальных потоков - это миллион копий значения. Для корутин элемент контекста наследуется деревом и копий не создаёт.
|
||||
>
|
||||
> **Отладка: стектрейсы.** Поток даёт полный стектрейс бесплатно - это заметный плюс и потоков, и виртуальных потоков. У корутины стектрейс содержит кадры только от последней точки возобновления, а историю приостановок восстанавливает библиотека `kotlinx-coroutines-debug` или флаг `-Dkotlinx.coroutines.debug`. Без них отладка асинхронного кода труднее, чем у потоков.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокирующий вызов отменяет всю выгоду:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(2)) {
|
||||
> List(10) { launch { Thread.sleep(50) } }.forEach { it.join() }
|
||||
> }
|
||||
> }
|
||||
> println("10 задач по 50 мс на 2 потоках: ${t >= 240}") // 10 задач по 50 мс на 2 потоках: true
|
||||
> }
|
||||
> ```
|
||||
> Корутины превратились в обычный пул потоков с очередью. Весь смысл - в `delay` вместо `Thread.sleep`, то есть в приостановке вместо блокировки.
|
||||
>
|
||||
> **«Корутин больше - значит быстрее» неверно для вычислений.** На четырёх ядрах сорок счётных корутин работают не быстрее четырёх, а переключений становится больше. Число параллельных вычислительных задач должно соответствовать числу ядер.
|
||||
>
|
||||
> **Стектрейсы хуже, чем у потоков.** Исключение из корутины содержит кадры от точки возобновления, а не всю логическую цепочку вызовов. Это реальная цена корутин, которую частично компенсирует `kotlinx-coroutines-debug`.
|
||||
>
|
||||
> **Корутина без точек приостановки не отменяется и занимает поток целиком.** Бесконечный счётный цикл в корутине хуже, чем в потоке: поток можно хотя бы прервать средствами ОС, а корутину - нет.
|
||||
>
|
||||
> **`ThreadLocal` не работает так, как ожидается.** Любая библиотека, полагающаяся на `ThreadLocal` (MDC в логах, `SecurityContext` в Spring, транзакции Hibernate), в корутинах требует `asContextElement` или специальной интеграции. С виртуальными потоками такой проблемы нет.
|
||||
>
|
||||
> **Профилировщики и инструменты знают о потоках, а не о корутинах.** В дампе потоков вы увидите `DefaultDispatcher-worker-1`, а не свои корутины; счётчики JMX считают потоки. Для корутин нужны отдельные инструменты - `kotlinx-coroutines-debug` или поддержка в IDE.
|
||||
>
|
||||
> **Переход на корутины не бесплатен архитектурно.** `suspend` поднимается по всему стеку вызовов до точки входа. Если наверху - не-suspend интерфейс (`Comparator`, `equals`, Java-колбэк), приостановку туда не поместить, и придётся либо менять архитектуру, либо блокировать поток через `runBlocking`.
|
||||
>
|
||||
> **Смешивание корутин и блокирующего кода даёт худшее из двух миров.** Половина кода на `suspend`, половина блокирующая, `runBlocking` в середине - такая система и не масштабируется, и сложна в отладке. Границу между блокирующим и приостанавливаемым кодом нужно держать явной и единственной.
|
||||
>
|
||||
> **Корутины не отменяют гонок и deadlock'ов.** Разделяемое изменяемое состояние требует синхронизации точно так же, как в потоках, а два `Mutex` в разном порядке дают взаимную блокировку. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
>
|
||||
> **Измерения на микробенчмарках обманчивы.** JIT-компиляция, прогрев и GC искажают результаты в десятки раз. Цифры в этой заметке - порядки величин, а не точные значения; для реальных измерений нужен JMH.
|
||||
|
||||
Далее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
|
||||
@@ -1,277 +0,0 @@
|
||||
---
|
||||
title: Корутины
|
||||
description: Что такое приостанавливаемое вычисление, зачем оно нужно и как устроен раздел
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Корутина - это вычисление, которое можно **приостановить** и продолжить позже, не блокируя поток. Это главный инструмент Kotlin для асинхронного кода: вместо коллбэков и `CompletableFuture` вы пишете последовательный код, а компилятор превращает его в машину состояний.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadUser(id: Int): String {
|
||||
delay(100) // приостановка, а не блокировка
|
||||
return "пользователь $id"
|
||||
}
|
||||
|
||||
suspend fun loadOrders(userId: Int): List<String> {
|
||||
delay(100)
|
||||
return listOf("заказ 1", "заказ 2")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val user = loadUser(1)
|
||||
val orders = loadOrders(1)
|
||||
println("$user: $orders") // пользователь 1: [заказ 1, заказ 2]
|
||||
}
|
||||
```
|
||||
Код читается как последовательный, но на `delay` поток освобождается и может выполнять другие корутины. Это и есть вся идея.
|
||||
|
||||
## Почему не потоки
|
||||
Поток ОС стоит около мегабайта стека и переключается через ядро. Корутина - это объект в куче, её «переключение» - обычный вызов метода.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val jobs = List(50_000) {
|
||||
launch { delay(1000) }
|
||||
}
|
||||
println("запущено корутин: ${jobs.size}") // запущено корутин: 50000
|
||||
jobs.forEach { it.join() }
|
||||
println("все завершились") // все завершились
|
||||
}
|
||||
```
|
||||
Пятьдесят тысяч потоков повалили бы JVM с `OutOfMemoryError`; пятьдесят тысяч корутин спокойно живут в нескольких потоках. Подробнее: [Корутины против потоков](Корутины%20против%20потоков.md)
|
||||
|
||||
## Приостановка вместо блокировки
|
||||
Разница принципиальная. Блокировка занимает поток, ничего не делая:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val blocking = measureTimeMillis {
|
||||
withContext(Dispatchers.Default) {
|
||||
List(4) { launch { Thread.sleep(500) } }.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
val suspending = measureTimeMillis {
|
||||
List(4) { launch { delay(500) } }.forEach { it.join() }
|
||||
}
|
||||
|
||||
println("блокировка дольше: ${blocking > suspending}") // блокировка дольше: true
|
||||
println("приостановка около 500 мс: ${suspending < 900}") // приостановка около 500 мс: true
|
||||
}
|
||||
```
|
||||
`Thread.sleep` держит поток; `delay` возвращает его в пул. Из-за этого четыре `delay(500)` на одном потоке занимают 500 мс, а четыре `Thread.sleep(500)` - столько, сколько позволит число потоков.
|
||||
|
||||
## Как это работает под капотом
|
||||
Компилятор превращает `suspend`-функцию в машину состояний с объектом-продолжением (`Continuation`). Каждая точка приостановки - это метка, куда вернётся выполнение.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["вызов suspend-функции"] --> B["выполнение до delay"]
|
||||
B --> C{"нужна приостановка?"}
|
||||
C -->|"нет, результат готов"| G["вернуть значение"]
|
||||
C -->|"да"| D["сохранить состояние<br/>в Continuation"]
|
||||
D --> E["вернуть COROUTINE_SUSPENDED<br/>поток свободен"]
|
||||
E --> F["внешнее событие:<br/>таймер, I/O, другая корутина"]
|
||||
F --> H["resume: восстановить состояние"]
|
||||
H --> B
|
||||
```
|
||||
|
||||
Практическое следствие: приостановка возможна **только** в `suspend`-функции, потому что только у неё есть скрытый параметр `Continuation`. Подробнее: [Suspend-функции](Suspend-функции.md)
|
||||
|
||||
## Параллелизм
|
||||
Последовательный код легко превратить в параллельный:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun fetchA(): Int { delay(200); return 1 }
|
||||
suspend fun fetchB(): Int { delay(200); return 2 }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
val sum = fetchA() + fetchB()
|
||||
println("последовательно: $sum") // последовательно: 3
|
||||
}
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
val a = async { fetchA() }
|
||||
val b = async { fetchB() }
|
||||
println("параллельно: ${a.await() + b.await()}") // параллельно: 3
|
||||
}
|
||||
|
||||
println("ускорение примерно вдвое: ${sequential > parallel + 100}") // ускорение примерно вдвое: true
|
||||
}
|
||||
```
|
||||
Разница - в `async`: он запускает корутину и сразу возвращает `Deferred`, а `await` ждёт результат. Подробнее: [Билдеры корутин](Билдеры%20корутин.md)
|
||||
|
||||
## Структурированная конкурентность
|
||||
Главное архитектурное решение библиотеки: у каждой корутины есть родитель, и родитель не завершится раньше детей.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("начало")
|
||||
coroutineScope {
|
||||
launch { delay(100); println("ребёнок 1") }
|
||||
launch { delay(50); println("ребёнок 2") }
|
||||
}
|
||||
println("конец — после всех детей")
|
||||
// начало
|
||||
// ребёнок 2
|
||||
// ребёнок 1
|
||||
// конец — после всех детей
|
||||
}
|
||||
```
|
||||
Из этого бесплатно получаются: ожидание завершения, распространение отмены вниз и ошибок вверх, отсутствие «потерянных» задач. Это то, чего в Java приходится добиваться вручную. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [Suspend-функции](Suspend-функции.md) | `suspend`, `Continuation`, машина состояний |
|
||||
| [Структурированная конкурентность](Структурированная%20конкурентность.md) | `CoroutineScope`, иерархия, жизненный цикл |
|
||||
| [Билдеры корутин](Билдеры%20корутин.md) | `launch`, `async`, `runBlocking`, `withContext` |
|
||||
| [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) | на каком потоке выполняется код |
|
||||
| [Отмена и таймауты](Отмена%20и%20таймауты.md) | кооперативная отмена, `withTimeout`, `NonCancellable` |
|
||||
| [Исключения в корутинах](Исключения%20в%20корутинах.md) | распространение ошибок, `SupervisorJob`, обработчики |
|
||||
| [Flow](Flow.md) | асинхронный поток значений, операторы, backpressure |
|
||||
| [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) | горячие потоки и состояние |
|
||||
| [Channels](Channels.md) | передача значений между корутинами |
|
||||
| [Разделяемое состояние](Разделяемое%20состояние.md) | `Mutex`, атомарные типы, конфайнмент |
|
||||
| [Корутины против потоков](Корутины%20против%20потоков.md) | цифры, стоимость, когда нужны потоки |
|
||||
|
||||
## Подключение
|
||||
Корутины - не часть stdlib, это отдельная библиотека:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
}
|
||||
```
|
||||
В языке есть только `suspend`, `Continuation` и `suspendCoroutine` - минимальная основа. `launch`, `async`, `Flow`, диспетчеры и каналы живут в `kotlinx.coroutines`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Thread`, `ExecutorService` | корутины на пуле потоков |
|
||||
> | `CompletableFuture<T>` | `Deferred<T>` или просто `suspend fun` |
|
||||
> | `.thenApply().thenCompose()` | последовательный код |
|
||||
> | `CountDownLatch`, `Future.get()` | `join()`, `await()` |
|
||||
> | виртуальные потоки (Java 21+) | корутины |
|
||||
> | `Flow` из Reactive Streams | `kotlinx.coroutines.Flow` |
|
||||
> | отмена через `Future.cancel` и флаги | отмена по иерархии |
|
||||
>
|
||||
> **Главное отличие - асинхронный код выглядит последовательным.** В Java до виртуальных потоков асинхронность означала коллбэки или цепочки `CompletableFuture`:
|
||||
> ```java
|
||||
> loadUser(id)
|
||||
> .thenCompose(user -> loadOrders(user.getId()))
|
||||
> .thenApply(orders -> format(orders))
|
||||
> .exceptionally(e -> "ошибка: " + e.getMessage());
|
||||
> ```
|
||||
> Каждый шаг - лямбда, переменные приходится протаскивать через замыкания, а `try/catch`, циклы и `if` в такой цепочке выражаются неудобно. В Kotlin то же самое пишется обычным кодом:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadUser2(id: Int): String { delay(10); return "user$id" }
|
||||
> suspend fun loadOrders2(user: String): List<String> { delay(10); return listOf("$user-заказ") }
|
||||
>
|
||||
> suspend fun report(id: Int): String = try {
|
||||
> val user = loadUser2(id)
|
||||
> val orders = loadOrders2(user)
|
||||
> orders.joinToString()
|
||||
> } catch (e: Exception) {
|
||||
> "ошибка: ${e.message}"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(report(1)) } // user1-заказ
|
||||
> ```
|
||||
> Работают обычные `try/catch`, `for`, `return` и локальные переменные.
|
||||
>
|
||||
> **Виртуальные потоки (Java 21) решают ту же задачу иначе.** Project Loom делает блокировку дешёвой: виртуальный поток при блокирующем вызове освобождает несущий поток ОС, поэтому старый блокирующий код внезапно масштабируется. Разница в подходе: Loom прячет асинхронность от программиста (`Thread.sleep` просто становится дешёвым), корутины делают её видимой в типах (`suspend` в сигнатуре). Плюс Loom - работает весь существующий код; плюс корутин - компилятор знает, где функция может приостановиться, и из этого вырастают структурированная конкурентность, `Flow` и отмена. Корутины также работают на Android и в Kotlin/Native, где Loom недоступен.
|
||||
>
|
||||
> **Отмена в Java не распространяется.** `Future.cancel(true)` ставит флаг прерывания одному потоку; задачи, которые он породил, продолжат работу. В Kotlin отмена идёт по дереву корутин автоматически. Для Java это пришлось бы строить руками поверх `ExecutorService`. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
>
|
||||
> **Реактивные библиотеки закрывают часть задач.** RxJava и Project Reactor дают потоки данных и операторы, то есть примерный аналог `Flow`. Но у них свой стиль (всё - цепочка операторов), высокий порог входа и плохие стектрейсы. `Flow` при этом - обычные `suspend`-функции внутри, поэтому в нём можно писать `for` и `try`.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Корутины не ускоряют вычисления на CPU.** Они экономят потоки при ожидании (сеть, диск, таймеры). Если код считает, а не ждёт, выгоды от корутин нет - там нужен параллелизм по числу ядер:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = withContext(Dispatchers.Default) { // пул по числу ядер
|
||||
> (1..4).map { async { (1..1_000_000).sumOf { x -> x.toLong() } } }.awaitAll()
|
||||
> }
|
||||
> println(result.size) // 4
|
||||
> }
|
||||
> ```
|
||||
> На `Dispatchers.IO` такой код только создаст лишние потоки. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
>
|
||||
> **Блокирующий вызов внутри корутины блокирует поток целиком:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
> List(3) { launch { Thread.sleep(100) } }.forEach { it.join() }
|
||||
> }
|
||||
> }
|
||||
> println("последовательно, не 100 мс: ${t >= 300}") // последовательно, не 100 мс: true
|
||||
> }
|
||||
> ```
|
||||
> `Thread.sleep`, `JDBC`, старый `HttpURLConnection`, `File.readText` - всё это блокирует. Такие вызовы нужно оборачивать в `withContext(Dispatchers.IO)`.
|
||||
>
|
||||
> **`runBlocking` блокирует вызывающий поток** - это мост из обычного кода в корутины, а не способ запускать их в продакшене. В Android и серверных фреймворках у вас уже есть scope; `runBlocking` уместен в `main`, тестах и утилитах.
|
||||
>
|
||||
> **Корутина без scope теряется.** `GlobalScope.launch` не привязан ни к чему: его никто не отменит и никто не узнает об ошибке. Это главный антипаттерн в корутинах. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
>
|
||||
> **Исключение в `launch` не ловится снаружи:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> try {
|
||||
> launch { throw IllegalStateException("внутри launch") }
|
||||
> } catch (e: Exception) {
|
||||
> println("не поймается здесь")
|
||||
> }
|
||||
> delay(50)
|
||||
> println("а это выполнится")
|
||||
> }
|
||||
> ```
|
||||
> Этот код напечатает `а это выполнится`, а затем уронит программу: `launch` передаёт ошибку родителю, а не выбрасывает её в месте вызова. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
>
|
||||
> **Отмена кооперативная.** Корутина, которая не вызывает `suspend`-функций и не проверяет `isActive`, не отменяется:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(Dispatchers.Default) {
|
||||
> var i = 0
|
||||
> while (i < 100_000_000) i++ // не проверяет отмену
|
||||
> println("досчитал несмотря на отмену")
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> println("cancel вернулся")
|
||||
> }
|
||||
> ```
|
||||
> Цикл без точек приостановки игнорирует `cancel`.
|
||||
>
|
||||
> **`suspend` в сигнатуре - это заразно.** Вызвать `suspend`-функцию можно только из другой `suspend`-функции или из билдера. Поэтому `suspend` поднимается вверх по стеку до точки входа, и добавить его в середину существующего синхронного кода не получится без переделки всей цепочки вызовов.
|
||||
>
|
||||
> **Версия библиотеки имеет значение.** API корутин развивается: `limitedParallelism`, `Flow`-операторы, `SupervisorJob` поведение менялись между версиями. Код из статей 2019 года может не компилироваться или вести себя иначе.
|
||||
|
||||
Далее: [Suspend-функции](Suspend-функции.md)
|
||||
@@ -1,525 +0,0 @@
|
||||
---
|
||||
title: Отмена и таймауты
|
||||
description: Кооперативная отмена, CancellationException, withTimeout, NonCancellable и освобождение ресурсов
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Отмена корутин **кооперативная**: библиотека не может остановить код насильно, она лишь сообщает корутине, что её просят завершиться. Код должен это проверять - сам или через `suspend`-функции.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
repeat(100) { i ->
|
||||
delay(10)
|
||||
println("шаг $i")
|
||||
}
|
||||
}
|
||||
delay(35)
|
||||
job.cancelAndJoin()
|
||||
println("отменено")
|
||||
// шаг 0
|
||||
// шаг 1
|
||||
// шаг 2
|
||||
// отменено
|
||||
}
|
||||
```
|
||||
Все `suspend`-функции из `kotlinx.coroutines` (`delay`, `join`, `await`, `withContext`) проверяют отмену и бросают `CancellationException`.
|
||||
|
||||
## Как работает отмена
|
||||
`cancel()` переводит `Job` в состояние Cancelling и бросает `CancellationException` в точке приостановки:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} catch (e: CancellationException) {
|
||||
println("поймали: ${e::class.simpleName}")
|
||||
throw e // обязательно пробросить!
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("состояние: отменён=${job.isCancelled}, завершён=${job.isCompleted}")
|
||||
// поймали: JobCancellationException
|
||||
// состояние: отменён=true, завершён=true
|
||||
}
|
||||
```
|
||||
`CancellationException` - особое исключение: оно не считается ошибкой и не приводит к падению родителя. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
|
||||
## Код без точек приостановки не отменяется
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val t = measureTimeMillis {
|
||||
val job = launch(Dispatchers.Default) {
|
||||
var sum = 0L
|
||||
repeat(200_000_000) { sum += it } // ни одной точки приостановки
|
||||
println("досчитал несмотря на отмену: ${sum > 0}")
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
println("cancel ждал окончания цикла: ${t > 100}")
|
||||
// досчитал несмотря на отмену: true
|
||||
// cancel ждал окончания цикла: true
|
||||
}
|
||||
```
|
||||
Два способа сделать такой код отменяемым:
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// 1. проверять isActive
|
||||
val job1 = launch(Dispatchers.Default) {
|
||||
var i = 0
|
||||
while (isActive && i < 200_000_000) i++
|
||||
println("вышел по isActive: ${i < 200_000_000}")
|
||||
}
|
||||
delay(20)
|
||||
job1.cancelAndJoin()
|
||||
|
||||
// 2. вызывать ensureActive() — бросит CancellationException
|
||||
val job2 = launch(Dispatchers.Default) {
|
||||
try {
|
||||
repeat(200_000_000) {
|
||||
if (it % 100_000 == 0) ensureActive()
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
println("вышел по ensureActive")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job2.cancelAndJoin()
|
||||
// вышел по isActive: true
|
||||
// вышел по ensureActive
|
||||
}
|
||||
```
|
||||
Есть и третий вариант - `yield()`: он и проверяет отмену, и даёт другим корутинам шанс выполниться.
|
||||
|
||||
| Способ | Бросает исключение | Когда применять |
|
||||
|---|---|---|
|
||||
| `isActive` | нет, возвращает `Boolean` | условие цикла |
|
||||
| `ensureActive()` | да, `CancellationException` | внутри тела, быстрый выход |
|
||||
| `yield()` | да | ещё и уступает поток другим |
|
||||
|
||||
## Освобождение ресурсов
|
||||
`finally` работает как обычно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
println("ресурс открыт")
|
||||
delay(1000)
|
||||
} finally {
|
||||
println("ресурс закрыт")
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("готово")
|
||||
// ресурс открыт
|
||||
// ресурс закрыт
|
||||
// готово
|
||||
}
|
||||
```
|
||||
Но внутри `finally` отменённой корутины **нельзя приостанавливаться**:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} finally {
|
||||
val failed = runCatching { delay(50) }.isFailure
|
||||
println("delay в finally упал: $failed") // delay в finally упал: true
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
```
|
||||
Корутина уже отменена, поэтому любая приостановка немедленно бросает `CancellationException`. Если в очистке нужна приостановка - оберните её в `NonCancellable`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
delay(20)
|
||||
println("асинхронная очистка выполнена")
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("готово")
|
||||
// асинхронная очистка выполнена
|
||||
// готово
|
||||
}
|
||||
```
|
||||
`NonCancellable` нужен именно и только для этого. Использовать его, чтобы «защитить» обычную работу от отмены - ошибка: такая корутина станет неостановимой.
|
||||
|
||||
Для `Closeable`-ресурсов проще `use`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class Connection : AutoCloseable {
|
||||
fun query() = "результат"
|
||||
override fun close() = println("соединение закрыто")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
Connection().use { conn ->
|
||||
println(conn.query())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
// результат
|
||||
// соединение закрыто
|
||||
}
|
||||
```
|
||||
|
||||
## withTimeout
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun slowOperation(): String {
|
||||
delay(1000)
|
||||
return "готово"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
try {
|
||||
withTimeout(50) { slowOperation() }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
println("не успели: ${e.message}")
|
||||
}
|
||||
// не успели: Timed out waiting for 50 ms
|
||||
}
|
||||
```
|
||||
`withTimeoutOrNull` вместо исключения возвращает `null` - обычно это удобнее:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val fast = withTimeoutOrNull(100) { delay(20); "успели" }
|
||||
val slow = withTimeoutOrNull(20) { delay(100); "не успели" }
|
||||
|
||||
println(fast) // успели
|
||||
println(slow) // null
|
||||
}
|
||||
```
|
||||
`TimeoutCancellationException` - подкласс `CancellationException`, поэтому он отменяет только свой блок, а не всю область.
|
||||
|
||||
## Таймаут и ресурсы
|
||||
Опасное место: таймаут может сработать между получением ресурса и входом в `try`.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class Resource2 {
|
||||
var closed = false
|
||||
fun close() { closed = true }
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var resource: Resource2? = null
|
||||
withTimeoutOrNull(30) {
|
||||
resource = Resource2() // создали
|
||||
try {
|
||||
delay(1000) // здесь таймаут
|
||||
} finally {
|
||||
resource?.close() // закрыли
|
||||
}
|
||||
}
|
||||
println("ресурс закрыт: ${resource?.closed}") // ресурс закрыт: true
|
||||
}
|
||||
```
|
||||
Правило: открывайте ресурс **внутри** блока и закрывайте в `finally` или через `use`. Открытие снаружи с закрытием внутри - источник утечек.
|
||||
|
||||
## Отмена распространяется вниз
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val parent = launch {
|
||||
launch {
|
||||
try { delay(1000) } finally { println("ребёнок 1 завершён") }
|
||||
}
|
||||
launch {
|
||||
try { delay(1000) } finally { println("ребёнок 2 завершён") }
|
||||
}
|
||||
try { delay(1000) } finally { println("родитель завершён") }
|
||||
}
|
||||
delay(20)
|
||||
parent.cancelAndJoin()
|
||||
println("всё дерево остановлено")
|
||||
// ребёнок 1 завершён
|
||||
// ребёнок 2 завершён
|
||||
// родитель завершён
|
||||
// всё дерево остановлено
|
||||
}
|
||||
```
|
||||
Это бесплатное следствие структурированной конкурентности: отменив одну корутину, вы останавливаете всё поддерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant В as Вызывающий
|
||||
participant Р as Родитель
|
||||
participant Д as Дети
|
||||
В->>Р: cancel()
|
||||
Р->>Р: состояние Cancelling
|
||||
Р->>Д: отменить всех детей
|
||||
Д->>Д: CancellationException<br/>в точке приостановки
|
||||
Д->>Д: выполнить finally
|
||||
Д-->>Р: ребёнок Cancelled
|
||||
Р->>Р: выполнить finally
|
||||
Р-->>В: состояние Cancelled, join() вернулся
|
||||
```
|
||||
|
||||
## cancel против cancelAndJoin
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job1 = launch { try { delay(1000) } finally { println("1: очистка") } }
|
||||
job1.cancel() // не ждём завершения
|
||||
println("после cancel: завершён=${job1.isCompleted}")
|
||||
|
||||
val job2 = launch { try { delay(1000) } finally { println("2: очистка") } }
|
||||
job2.cancelAndJoin() // ждём
|
||||
println("после cancelAndJoin: завершён=${job2.isCompleted}")
|
||||
// после cancel: завершён=false
|
||||
// 1: очистка
|
||||
// 2: очистка
|
||||
// после cancelAndJoin: завершён=true
|
||||
}
|
||||
```
|
||||
`cancel()` только отправляет сигнал; `cancelAndJoin()` дожидается, пока корутина действительно остановится и выполнит очистку. Для корректного освобождения ресурсов нужен второй вариант.
|
||||
|
||||
## Отмена и async
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val deferred = async { delay(1000); "результат" }
|
||||
delay(20)
|
||||
deferred.cancel()
|
||||
|
||||
val error = runCatching { deferred.await() }.exceptionOrNull()
|
||||
println("await после cancel бросил: ${error is CancellationException}") // await после cancel бросил: true
|
||||
}
|
||||
```
|
||||
Отменённый `Deferred` при `await()` бросает `CancellationException`, а не возвращает `null`. Для «результат или ничего» используйте `withTimeoutOrNull`.
|
||||
|
||||
## Таймаут на весь scope
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadWithDeadline(): List<String>? = withTimeoutOrNull(60) {
|
||||
coroutineScope {
|
||||
val a = async { delay(20); "быстрый" }
|
||||
val b = async { delay(1000); "медленный" }
|
||||
listOf(a.await(), b.await())
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadWithDeadline()) // null — медленный не успел, отменились оба
|
||||
}
|
||||
```
|
||||
Таймаут поверх `coroutineScope` даёт дедлайн на всю группу задач: не успел один - отменяются все. Это правильное поведение для запроса с общим SLA.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Thread.interrupt()` | `job.cancel()` |
|
||||
> | `Thread.isInterrupted()` | `isActive` |
|
||||
> | `InterruptedException` | `CancellationException` |
|
||||
> | `Future.cancel(true)` - одна задача | отмена всего поддерева |
|
||||
> | `ExecutorService.shutdownNow()` | `scope.cancel()` |
|
||||
> | `future.get(50, MILLISECONDS)` | `withTimeout(50)` |
|
||||
> | `ScheduledExecutorService` для дедлайна | `withTimeout` |
|
||||
> | флаг `volatile boolean stopped` | `isActive` |
|
||||
>
|
||||
> **Прерывание в Java тоже кооперативное, но работает хуже.** `Thread.interrupt()` ставит флаг и прерывает только блокирующие вызовы, которые умеют его замечать (`sleep`, `wait`, `join`, часть NIO). Проблемы знакомые: `InterruptedException` - проверяемое исключение, которое приходится ловить в каждом блокирующем вызове, а при перехвате легко потерять флаг:
|
||||
> ```java
|
||||
> try {
|
||||
> Thread.sleep(1000);
|
||||
> } catch (InterruptedException e) {
|
||||
> // забыли Thread.currentThread().interrupt() — прерывание потеряно
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `CancellationException` непроверяемое, поэтому его не нужно ловить везде, а `catch (e: Exception)` без проброса ломает отмену ровно так же - разница в том, что IDE и детекторы это подсказывают.
|
||||
>
|
||||
> **Отмена по дереву против отмены по задаче.** `Future.cancel(true)` прерывает один поток. Если задача породила подзадачи через другой `submit`, они продолжат работу, и отследить их можно только вручную - списком `Future`. В Kotlin `cancel()` на родителе гарантированно останавливает всё поддерево, включая задачи, запущенные тремя уровнями ниже.
|
||||
>
|
||||
> **Таймаут не требует отдельного планировщика.** В Java `future.get(50, MILLISECONDS)` прерывает **ожидание**, но не саму задачу - она продолжит выполняться в пуле. Чтобы остановить и её, нужен `cancel(true)` плюс поддержка прерывания в коде задачи. `withTimeout` отменяет сам блок:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var finished = false
|
||||
> withTimeoutOrNull(30) {
|
||||
> try { delay(1000); finished = true } finally { println("задача остановлена") }
|
||||
> }
|
||||
> println("тело доработало: $finished")
|
||||
> // задача остановлена
|
||||
> // тело доработало: false
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`shutdownNow()` против `scope.cancel()`.** `shutdownNow` возвращает список незапущенных задач и прерывает потоки работающих - результат зависит от того, умеет ли код реагировать на прерывание. `scope.cancel()` отменяет корутины в известных точках приостановки, а `join()` даёт дождаться завершения очистки.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Перехват `CancellationException` без проброса ломает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> repeat(5) {
|
||||
> try {
|
||||
> delay(20)
|
||||
> } catch (e: CancellationException) {
|
||||
> println("проглотили отмену на шаге $it")
|
||||
> // нет throw — цикл продолжается!
|
||||
> }
|
||||
> }
|
||||
> println("корутина доработала до конца")
|
||||
> }
|
||||
> delay(30)
|
||||
> job.cancelAndJoin()
|
||||
> // проглотили отмену на шаге 1
|
||||
> // проглотили отмену на шаге 2
|
||||
> // проглотили отмену на шаге 3
|
||||
> // проглотили отмену на шаге 4
|
||||
> // корутина доработала до конца
|
||||
> }
|
||||
> ```
|
||||
> Корутина стала неотменяемой. Та же беда возникает от `catch (e: Exception)` - `CancellationException` в него попадает.
|
||||
>
|
||||
> **`try/catch (Exception)` вокруг `suspend`-вызова перехватывает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } catch (e: Exception) {
|
||||
> println("перехвачено как обычная ошибка: ${e is CancellationException}")
|
||||
> }
|
||||
> println("продолжили работу после отмены")
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> // перехвачено как обычная ошибка: true
|
||||
> // продолжили работу после отмены
|
||||
> }
|
||||
> ```
|
||||
> Правильно: сначала `catch (e: CancellationException) { throw e }`, затем `catch (e: Exception)`.
|
||||
>
|
||||
> **`runCatching` глотает `CancellationException`** по той же причине - он ловит `Throwable`. В корутинном коде это регулярная проблема: `runCatching { suspendCall() }` делает блок неотменяемым. Для `suspend`-кода нужен явный `try/catch` с пробросом отмены.
|
||||
>
|
||||
> **Приостановка в `finally` отменённой корутины не работает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } finally {
|
||||
> val ok = runCatching { delay(50); "очистка" }.isSuccess
|
||||
> println("приостановка в finally удалась: $ok") // приостановка в finally удалась: false
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> }
|
||||
> ```
|
||||
> Нужен `withContext(NonCancellable)`.
|
||||
>
|
||||
> **`NonCancellable` вокруг обычной работы делает корутину неостановимой:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> val job = launch {
|
||||
> withContext(NonCancellable) {
|
||||
> delay(200) // отмена проигнорирована
|
||||
> println("доработал несмотря на cancel")
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> }
|
||||
> println("cancel ждал 200 мс: ${t >= 190}")
|
||||
> // доработал несмотря на cancel
|
||||
> // cancel ждал 200 мс: true
|
||||
> }
|
||||
> ```
|
||||
> `NonCancellable` оправдан только для короткой очистки в `finally`.
|
||||
>
|
||||
> **`cancel()` без `join()` не ждёт очистки.** Если после `cancel()` сразу закрыть ресурс или выйти из процесса, `finally` может не успеть выполниться. Используйте `cancelAndJoin()`.
|
||||
>
|
||||
> **Чистый вычислительный цикл игнорирует отмену.** Любой `while`/`repeat` без `suspend`-вызовов нужно дополнять `isActive` или `ensureActive()` - иначе корутина отработает до конца, а `cancelAndJoin()` будет её ждать.
|
||||
>
|
||||
> **`withTimeout` бросает исключение, которое легко не заметить:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = runCatching { withTimeout(20) { delay(100); "значение" } }
|
||||
> println("упало с таймаутом: ${result.exceptionOrNull() is TimeoutCancellationException}")
|
||||
> // упало с таймаутом: true
|
||||
> }
|
||||
> ```
|
||||
> `TimeoutCancellationException` - подкласс `CancellationException`, поэтому обычный `catch (e: CancellationException) { throw e }` его пробросит, и таймаут «не сработает» там, где вы ждали обработки. Используйте `withTimeoutOrNull` или ловите `TimeoutCancellationException` до отмены.
|
||||
>
|
||||
> **Ресурс, открытый до `withTimeout`, может утечь:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var opened = 0
|
||||
> var closed = 0
|
||||
> withTimeoutOrNull(30) {
|
||||
> opened++ // открыли внутри — хорошо
|
||||
> try { delay(1000) } finally { closed++ }
|
||||
> }
|
||||
> println("открыто $opened, закрыто $closed") // открыто 1, закрыто 1
|
||||
> }
|
||||
> ```
|
||||
> Если бы `opened++` стоял перед `withTimeoutOrNull`, а `closed++` внутри - при срабатывании таймаута до входа в блок ресурс остался бы открытым.
|
||||
>
|
||||
> **Отменённый scope нельзя переиспользовать** - `launch` в нём сразу возвращает отменённый `Job`. Для перезапуска создавайте новый scope.
|
||||
>
|
||||
> **`await()` на отменённом `Deferred` бросает исключение, а не возвращает `null`.** Код, ожидающий `null` при отмене, получит падение.
|
||||
>
|
||||
> **Отмена не мгновенна.** Между `cancel()` и фактическим завершением проходит время: корутина должна дойти до точки приостановки и выполнить `finally`. Проверка `isCompleted` сразу после `cancel()` обычно даёт `false`.
|
||||
|
||||
Далее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
@@ -1,516 +0,0 @@
|
||||
---
|
||||
title: Разделяемое состояние
|
||||
description: Mutex, Semaphore, атомарные типы, конфайнмент потока и подход «не разделять состояние вообще»
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Корутины не отменяют гонок. Если несколько корутин меняют одну переменную, нужна синхронизация - причём такая, которая приостанавливает, а не блокирует поток.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { counter++ } } }.forEach { it.join() }
|
||||
}
|
||||
println("потеряно инкрементов: ${counter < 100_000}") // потеряно инкрементов: true
|
||||
}
|
||||
```
|
||||
`counter++` - это чтение, увеличение и запись: три операции, между которыми может вклиниться другая корутина. На одном потоке проблемы не было бы, но `Dispatchers.Default` использует все ядра.
|
||||
|
||||
## Способы, по возрастанию цены
|
||||
| Подход | Стоимость | Когда применять |
|
||||
|---|---|---|
|
||||
| не разделять состояние | нулевая | всегда, когда возможно |
|
||||
| неизменяемые данные | нулевая | состояние целиком заменяется |
|
||||
| атомарные типы | очень низкая | счётчики, флаги, одна переменная |
|
||||
| `MutableStateFlow.update` | низкая | наблюдаемое состояние |
|
||||
| конфайнмент в один поток | низкая | группа связанных переменных |
|
||||
| `Mutex` | средняя | составная операция, вызов `suspend` внутри |
|
||||
| `Semaphore` | средняя | ограничение числа одновременных операций |
|
||||
|
||||
Порядок не случаен: первые два варианта убирают проблему, остальные её решают.
|
||||
|
||||
## Атомарные типы
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
fun main() = runBlocking {
|
||||
val counter = AtomicInteger(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { counter.incrementAndGet() } } }.forEach { it.join() }
|
||||
}
|
||||
println("точный результат: ${counter.get()}") // точный результат: 100000
|
||||
|
||||
val ref = AtomicReference(listOf<String>())
|
||||
withContext(Dispatchers.Default) {
|
||||
List(10) { i -> launch { ref.updateAndGet { it + "элемент $i" } } }.forEach { it.join() }
|
||||
}
|
||||
println("добавлено без потерь: ${ref.get().size}") // добавлено без потерь: 10
|
||||
}
|
||||
```
|
||||
Атомарные типы работают через CAS (compare-and-set) - никаких блокировок, только повтор при конфликте. Это самый дешёвый способ, но он покрывает только операции с **одной** переменной.
|
||||
|
||||
## Mutex
|
||||
`Mutex` - взаимное исключение для корутин. В отличие от `synchronized`, он приостанавливает, а не блокирует поток.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mutex = Mutex()
|
||||
var counter = 0
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { mutex.withLock { counter++ } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("результат точный: ${counter == 100_000}") // результат точный: true
|
||||
}
|
||||
```
|
||||
Главное преимущество - внутри `withLock` можно вызывать `suspend`-функции:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class Cache {
|
||||
private val mutex = Mutex()
|
||||
private val data = mutableMapOf<Int, String>()
|
||||
|
||||
suspend fun get(id: Int): String = mutex.withLock {
|
||||
data[id] ?: load(id).also { data[id] = it }
|
||||
}
|
||||
|
||||
private suspend fun load(id: Int): String {
|
||||
delay(10) // suspend внутри критической секции
|
||||
return "значение $id"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val cache = Cache()
|
||||
val results = List(5) { async { cache.get(1) } }.awaitAll()
|
||||
println("все получили одно значение: ${results.toSet().size == 1}") // все получили одно значение: true
|
||||
}
|
||||
```
|
||||
С `synchronized` так нельзя: `suspend`-вызов внутри `synchronized`-блока не компилируется, потому что приостановка освободила бы поток, а монитор остался бы захваченным.
|
||||
|
||||
## Mutex не реентерабелен
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mutex = Mutex()
|
||||
|
||||
val deadlocked = withTimeoutOrNull(100) {
|
||||
mutex.withLock {
|
||||
mutex.withLock { // вторичный захват тем же владельцем
|
||||
"недостижимо"
|
||||
}
|
||||
}
|
||||
}
|
||||
println("повторный захват повис: ${deadlocked == null}") // повторный захват повис: true
|
||||
}
|
||||
```
|
||||
Это осознанное решение: реентерабельность в корутинах определить корректно нельзя - «тот же владелец» для корутины понятие нечёткое, ведь она может перемещаться между потоками. Практический вывод: функция под `withLock` не должна вызывать другую функцию, которая берёт тот же мьютекс. Приватные методы, вызываемые из критической секции, не должны блокироваться сами.
|
||||
|
||||
## Semaphore
|
||||
Ограничивает число одновременных операций:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val semaphore = Semaphore(permits = 3)
|
||||
val inFlight = AtomicInteger(0)
|
||||
val maxInFlight = AtomicInteger(0)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(20) {
|
||||
launch {
|
||||
semaphore.withPermit {
|
||||
val now = inFlight.incrementAndGet()
|
||||
maxInFlight.updateAndGet { m -> maxOf(m, now) }
|
||||
delay(20) // ожидание ответа API
|
||||
inFlight.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
println("одновременно не больше 3: ${maxInFlight.get() <= 3}") // одновременно не больше 3: true
|
||||
}
|
||||
```
|
||||
Важное отличие от `limitedParallelism`: семафор считает операции **в полёте**, включая приостановленные. Именно он нужен, чтобы не превысить лимит внешнего API. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
|
||||
## Конфайнмент в один поток
|
||||
Если состояние меняется только в одном потоке, синхронизация не нужна вовсе:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val single = Dispatchers.Default.limitedParallelism(1)
|
||||
var counter = 0
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) {
|
||||
launch {
|
||||
repeat(1000) {
|
||||
withContext(single) { counter++ } // все изменения в одном потоке
|
||||
}
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
println("точный результат: ${counter == 100_000}") // точный результат: true
|
||||
}
|
||||
```
|
||||
Работает, но дорого: 100 000 переключений контекста. Правильная версия конфайнмента - **грубая**, когда весь блок работы уходит в один поток:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val single = Dispatchers.Default.limitedParallelism(1)
|
||||
var counter = 0
|
||||
|
||||
val coarse = measureTimeMillis {
|
||||
withContext(single) { // переключение один раз
|
||||
repeat(100_000) { counter++ }
|
||||
}
|
||||
}
|
||||
println("результат: $counter")
|
||||
println("грубый конфайнмент быстр: ${coarse < 500}")
|
||||
// результат: 100000
|
||||
// грубый конфайнмент быстр: true
|
||||
}
|
||||
```
|
||||
|
||||
## Актор: состояние внутри корутины
|
||||
Самый чистый подход - состояние не разделяется вообще, им владеет одна корутина:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
sealed interface AccountCommand {
|
||||
data class Deposit(val amount: Int) : AccountCommand
|
||||
data class Withdraw(val amount: Int, val ok: CompletableDeferred<Boolean>) : AccountCommand
|
||||
data class Balance(val reply: CompletableDeferred<Int>) : AccountCommand
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val commands = Channel<AccountCommand>()
|
||||
|
||||
launch {
|
||||
var balance = 0 // никто больше не видит эту переменную
|
||||
for (cmd in commands) {
|
||||
when (cmd) {
|
||||
is AccountCommand.Deposit -> balance += cmd.amount
|
||||
is AccountCommand.Withdraw -> {
|
||||
val enough = balance >= cmd.amount
|
||||
if (enough) balance -= cmd.amount
|
||||
cmd.ok.complete(enough)
|
||||
}
|
||||
is AccountCommand.Balance -> cmd.reply.complete(balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(100) { commands.send(AccountCommand.Deposit(10)) }
|
||||
|
||||
val ok = CompletableDeferred<Boolean>()
|
||||
commands.send(AccountCommand.Withdraw(500, ok))
|
||||
println("списание прошло: ${ok.await()}")
|
||||
|
||||
val tooMuch = CompletableDeferred<Boolean>()
|
||||
commands.send(AccountCommand.Withdraw(10_000, tooMuch))
|
||||
println("превышение отклонено: ${!tooMuch.await()}")
|
||||
|
||||
val balance = CompletableDeferred<Int>()
|
||||
commands.send(AccountCommand.Balance(balance))
|
||||
println("баланс: ${balance.await()}")
|
||||
commands.close()
|
||||
// списание прошло: true
|
||||
// превышение отклонено: true
|
||||
// баланс: 500
|
||||
}
|
||||
```
|
||||
Проверка «хватает ли денег» и списание происходят атомарно без всякой блокировки - просто потому, что их выполняет одна корутина. Подробнее: [Channels](Channels.md)
|
||||
|
||||
## StateFlow для наблюдаемого состояния
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { state.update { v -> v + 1 } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("результат точный: ${state.value == 100_000}") // результат точный: true
|
||||
}
|
||||
```
|
||||
`update` - это CAS-цикл, поэтому он и атомарен, и даёт оповещение подписчикам. Если состояние нужно наблюдать, это лучший вариант. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
|
||||
## Как выбрать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Состояние меняют<br/>несколько корутин"] --> B{"Можно вообще<br/>не разделять?"}
|
||||
B -->|"да"| C["локальная переменная<br/>или актор"]
|
||||
B -->|"нет"| D{"За состоянием<br/>наблюдают?"}
|
||||
D -->|"да"| E["MutableStateFlow<br/>+ update"]
|
||||
D -->|"нет"| F{"Одна переменная?"}
|
||||
F -->|"да"| G["AtomicInteger / AtomicReference"]
|
||||
F -->|"нет, составная операция"| H{"Нужен suspend<br/>внутри секции?"}
|
||||
H -->|"да"| I["Mutex.withLock"]
|
||||
H -->|"нет"| J["Mutex или<br/>конфайнмент в поток"]
|
||||
```
|
||||
|
||||
## Неизменяемость убирает проблему
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class Stats(val requests: Int = 0, val errors: Int = 0) {
|
||||
val successRate: Double
|
||||
get() = if (requests == 0) 0.0 else (requests - errors).toDouble() / requests
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val stats = MutableStateFlow(Stats())
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i ->
|
||||
launch {
|
||||
stats.update { it.copy(requests = it.requests + 1, errors = it.errors + i % 10 / 9) }
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
val final = stats.value
|
||||
println("запросов: ${final.requests}, ошибок: ${final.errors}")
|
||||
println("согласованный снимок: ${final.requests == 100}")
|
||||
// запросов: 100, ошибок: 10
|
||||
// согласованный снимок: true
|
||||
}
|
||||
```
|
||||
Два связанных поля обновляются одним атомарным `copy`, поэтому несогласованного состояния («запросов 5, ошибок 7») не возникает никогда. Это главный довод за неизменяемые `data class` в качестве состояния. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
## Коллекции
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
fun main() = runBlocking {
|
||||
// обычный MutableList не потокобезопасен
|
||||
val unsafe = mutableListOf<Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { repeat(100) { unsafe.add(i) } } }.forEach { it.join() }
|
||||
}
|
||||
println("размер может быть неверным: ${unsafe.size <= 10_000}")
|
||||
|
||||
// вариант 1: Mutex
|
||||
val mutex = Mutex()
|
||||
val guarded = mutableListOf<Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { repeat(100) { mutex.withLock { guarded.add(i) } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("с Mutex точно: ${guarded.size == 10_000}")
|
||||
|
||||
// вариант 2: потокобезопасная коллекция из JDK
|
||||
val concurrent = ConcurrentHashMap<Int, Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { concurrent[i] = i } }.forEach { it.join() }
|
||||
}
|
||||
println("ConcurrentHashMap точно: ${concurrent.size == 100}")
|
||||
// размер может быть неверным: true
|
||||
// с Mutex точно: true
|
||||
// ConcurrentHashMap точно: true
|
||||
}
|
||||
```
|
||||
В Kotlin нет своих потокобезопасных коллекций для JVM - используются `java.util.concurrent`. Но учтите: `ConcurrentHashMap` безопасен для отдельных операций, а составные («прочитать, изменить, записать») всё равно требуют `compute`/`merge` или мьютекса. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
|
||||
|
||||
## Что не нужно делать
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
val lock = Any()
|
||||
|
||||
val time = measureTimeMillis {
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { synchronized(lock) { counter++ } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
println("результат верный: ${counter == 100_000}")
|
||||
println("но поток блокировался: ${time >= 0}")
|
||||
// результат верный: true
|
||||
// но поток блокировался: true
|
||||
}
|
||||
```
|
||||
`synchronized` работает и даёт правильный результат, но блокирует поток диспетчера: пока корутина ждёт монитор, поток не может выполнять другие корутины. На ограниченном диспетчере это прямой путь к deadlock, а `suspend`-вызов внутри `synchronized` вообще не скомпилируется.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `synchronized` (блокирует поток) | `Mutex.withLock` (приостанавливает) |
|
||||
> | `ReentrantLock` | `Mutex` (не реентерабелен) |
|
||||
> | `Semaphore` из `j.u.c.` (блокирует) | `kotlinx.coroutines.sync.Semaphore` |
|
||||
> | `AtomicInteger` | он же, или `MutableStateFlow` |
|
||||
> | `volatile` | `@Volatile` или `StateFlow` |
|
||||
> | `ThreadLocal` для конфайнмента | элемент контекста корутины |
|
||||
> | `ConcurrentHashMap` | он же |
|
||||
> | нет аналога | актор на `Channel` |
|
||||
>
|
||||
> **`synchronized` занимает поток, `Mutex` - нет.** Это принципиальное различие. В пуле из 8 потоков восемь корутин, ждущих монитор, парализуют весь диспетчер - включая корутины, которым этот монитор не нужен. `Mutex` приостанавливает корутину и освобождает поток:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val mutex = Mutex()
|
||||
> val single = Dispatchers.Default.limitedParallelism(1)
|
||||
> var progress = ""
|
||||
> withContext(single) {
|
||||
> launch { mutex.withLock { delay(30); progress += "A" } }
|
||||
> launch { mutex.withLock { progress += "B" } }
|
||||
> launch { progress += "C" } // не ждёт мьютекс
|
||||
> }
|
||||
> println("порядок выполнения: $progress")
|
||||
> // порядок выполнения: CAB
|
||||
> }
|
||||
> ```
|
||||
> На одном потоке корутина `C` выполнилась первой, пока `A` держала мьютекс и ждала на `delay`. С `synchronized` и `Thread.sleep` поток был бы занят и `C` стояла бы в очереди.
|
||||
>
|
||||
> **`suspend` внутри `synchronized` не компилируется.** Это не ограничение, а защита: приостановка освободила бы поток, а монитор остался бы захваченным - потенциальный deadlock, который JVM не заметит. `Mutex` снимает запрет, потому что владение не привязано к потоку.
|
||||
>
|
||||
> **`ReentrantLock` реентерабелен, `Mutex` - нет.** В Java повторный захват тем же потоком разрешён, что делает рекурсивные вызовы под локом безопасными. В корутинах понятие «тот же владелец» не определено: корутина мигрирует между потоками, а `withLock` в дочерней корутине - это уже другой владелец. Поэтому `Mutex` принципиально не реентерабелен, и рекурсию под локом приходится исключать архитектурно.
|
||||
>
|
||||
> **`volatile` против `StateFlow`.** `@Volatile` в Kotlin есть и работает так же: гарантирует видимость записи между потоками, но не атомарность составных операций. `MutableStateFlow` даёт и видимость, и атомарный `update`, и оповещение - три вещи вместо одной.
|
||||
>
|
||||
> **Актор не имеет аналога в Java.** Паттерн «состояние внутри потока, снаружи только сообщения» в Java требует `BlockingQueue` + выделенный поток + «отравленную пилюлю» для остановки + ручную реализацию ответов (обычно через `CompletableFuture` в сообщении). В Kotlin это `Channel` + `launch` + `CompletableDeferred` - примерно двадцать строк и никаких потоков.
|
||||
>
|
||||
> **Проблемы те же.** Корутины не отменяют ни гонок, ни deadlock'ов, ни инверсии приоритетов. Два мьютекса, захваченные в разном порядке, дают взаимную блокировку точно так же, как два `synchronized`:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val a = Mutex()
|
||||
> val b = Mutex()
|
||||
> val result = withTimeoutOrNull(100) {
|
||||
> coroutineScope {
|
||||
> launch { a.withLock { delay(20); b.withLock { } } }
|
||||
> launch { b.withLock { delay(20); a.withLock { } } }
|
||||
> }
|
||||
> }
|
||||
> println("классический deadlock: ${result == null}") // классический deadlock: true
|
||||
> coroutineContext.cancelChildren()
|
||||
> }
|
||||
> ```
|
||||
> Разница только в том, что корутинный deadlock можно разорвать таймаутом, а поток, стоящий на мониторе, прервать нельзя.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Mutex` не реентерабелен - повторный захват это deadlock:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> class Service {
|
||||
> private val mutex = Mutex()
|
||||
> suspend fun outer() = mutex.withLock { inner() }
|
||||
> suspend fun inner() = mutex.withLock { "никогда" } // тот же мьютекс
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = withTimeoutOrNull(100) { Service().outer() }
|
||||
> println("повисло: ${result == null}") // повисло: true
|
||||
> }
|
||||
> ```
|
||||
> Публичный метод, берущий лок, не должен вызывать другой публичный метод того же класса. Выносите логику в приватные методы без блокировки.
|
||||
>
|
||||
> **`synchronized` в корутине блокирует поток диспетчера.** Результат будет верным, но пропускная способность упадёт, а на `limitedParallelism(1)` возможен deadlock. Используйте `Mutex`.
|
||||
>
|
||||
> **`suspend` внутри `synchronized` не компилируется** - и это хорошо. Попытка обойти запрет через `runBlocking` внутри `synchronized` создаёт именно тот deadlock, от которого защищает компилятор.
|
||||
>
|
||||
> **Отмена внутри `withLock` освобождает лок, но работа может остаться незавершённой:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val mutex = Mutex()
|
||||
> var step = 0
|
||||
> val job = launch {
|
||||
> mutex.withLock {
|
||||
> step = 1
|
||||
> delay(1000) // здесь отмена
|
||||
> step = 2 // не выполнится
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> println("лок свободен: ${!mutex.isLocked}, состояние на середине: ${step == 1}")
|
||||
> // лок свободен: true, состояние на середине: true
|
||||
> }
|
||||
> ```
|
||||
> Критическая секция прервалась на середине, оставив несогласованное состояние. Либо делайте секцию атомарной (одна операция `copy`), либо восстанавливайте инвариант в `finally`.
|
||||
>
|
||||
> **`AtomicInteger` защищает одну переменную, а не инвариант:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import java.util.concurrent.atomic.AtomicInteger
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val total = AtomicInteger(0)
|
||||
> val errors = AtomicInteger(0)
|
||||
> withContext(Dispatchers.Default) {
|
||||
> List(100) { launch { total.incrementAndGet(); errors.incrementAndGet() } }
|
||||
> .forEach { it.join() }
|
||||
> }
|
||||
> println("каждый атомарен: ${total.get()} ${errors.get()}")
|
||||
> // каждый атомарен: 100 100
|
||||
> }
|
||||
> ```
|
||||
> Итог верный, но в любой момент между двумя инкрементами снимок несогласован: читатель может увидеть `total = 50, errors = 49`. Для связанных значений нужен один объект состояния.
|
||||
>
|
||||
> **`ConcurrentHashMap` не делает составные операции атомарными.** `if (!map.containsKey(k)) map[k] = v` - гонка. Нужны `computeIfAbsent`, `putIfAbsent` или `merge`.
|
||||
>
|
||||
> **Обычный `mutableListOf` в корутинах небезопасен**, даже если кажется, что обращения редки. `ArrayList.add` при конкурентном вызове может потерять элемент или бросить `ArrayIndexOutOfBoundsException` из-за состояния расширения массива.
|
||||
>
|
||||
> **Мелкозернистый конфайнмент убивает производительность.** `withContext(single) { counter++ }` внутри цикла - это переключение контекста на каждую операцию. Выносите весь блок работы в один `withContext`.
|
||||
>
|
||||
> **Два мьютекса в разном порядке - deadlock**, как и в Java. Правило то же: фиксированный порядок захвата или единый лок.
|
||||
>
|
||||
> **`Semaphore` с утечкой разрешений исчерпывается.** Ручная пара `acquire()`/`release()` без `try/finally` при исключении потеряет разрешение навсегда. Используйте `withPermit`.
|
||||
>
|
||||
> **`limitedParallelism(1)` - не замена `Mutex`.** Он гарантирует один поток для **своих** задач, но состояние может меняться и из других контекстов. Конфайнмент работает только если **все** обращения идут через этот диспетчер.
|
||||
>
|
||||
> **`@Volatile` не делает `++` атомарным.** Он даёт только видимость записи. `@Volatile var counter = 0` с `counter++` из нескольких корутин теряет инкременты точно так же, как обычное поле.
|
||||
>
|
||||
> **Актор сериализует все операции.** Если команд много, одна корутина станет узким местом - её очередь будет расти. Для высокой нагрузки нужны шардирование по ключу или атомарные типы.
|
||||
|
||||
Далее: [Корутины против потоков](Корутины%20против%20потоков.md)
|
||||
@@ -1,478 +0,0 @@
|
||||
---
|
||||
title: Структурированная конкурентность
|
||||
description: CoroutineScope, иерархия Job, coroutineScope и supervisorScope, жизненный цикл и GlobalScope
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Структурированная конкурентность - главное архитектурное решение корутин: каждая корутина принадлежит области видимости (scope) и имеет родителя. Из этого следуют три гарантии:
|
||||
|
||||
1. родитель не завершится, пока не завершатся дети
|
||||
2. отмена родителя отменяет всех детей
|
||||
3. ошибка ребёнка доходит до родителя
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("начало")
|
||||
coroutineScope {
|
||||
launch { delay(100); println("медленный ребёнок") }
|
||||
launch { delay(20); println("быстрый ребёнок") }
|
||||
}
|
||||
println("конец — только после всех детей")
|
||||
// начало
|
||||
// быстрый ребёнок
|
||||
// медленный ребёнок
|
||||
// конец — только после всех детей
|
||||
}
|
||||
```
|
||||
Без этого свойства асинхронный код неизбежно порождает «потерянные» задачи: никто не знает, сколько их работает и когда они закончатся.
|
||||
|
||||
## CoroutineScope
|
||||
Scope - это объект с одним свойством `coroutineContext`, в котором есть `Job`. Билдеры (`launch`, `async`) - расширения на `CoroutineScope`, поэтому без scope их просто не вызвать.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class DataLoader : CoroutineScope {
|
||||
private val job = SupervisorJob()
|
||||
override val coroutineContext = job + Dispatchers.Default
|
||||
|
||||
fun load(id: Int) = launch {
|
||||
delay(20L * id) // разные задержки — порядок предсказуем
|
||||
println("загружено $id")
|
||||
}
|
||||
|
||||
fun close() {
|
||||
job.cancel() // отменяет всё, что запущено этим scope
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val loader = DataLoader()
|
||||
loader.load(1)
|
||||
loader.load(2)
|
||||
delay(80)
|
||||
loader.close()
|
||||
println("загрузчик закрыт")
|
||||
// загружено 1
|
||||
// загружено 2
|
||||
// загрузчик закрыт
|
||||
}
|
||||
```
|
||||
Такой класс владеет своими корутинами: `close()` гарантированно останавливает всё. Это основной способ связать корутины с жизненным циклом компонента.
|
||||
|
||||
Чаще используют не наследование, а поле - так интерфейс класса не загрязняется методами scope:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class Service2 {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
fun start() {
|
||||
scope.launch {
|
||||
delay(20)
|
||||
println("работа выполнена")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() = scope.cancel()
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val s = Service2()
|
||||
s.start()
|
||||
delay(50)
|
||||
s.stop()
|
||||
println("остановлено")
|
||||
// работа выполнена
|
||||
// остановлено
|
||||
}
|
||||
```
|
||||
Функция `CoroutineScope(context)` - фабрика, а не конструктор интерфейса. Обратите внимание: в `SupervisorJob()` - именно скобки, иначе получится ссылка на конструктор.
|
||||
|
||||
## coroutineScope: вложенная область
|
||||
`coroutineScope { }` - `suspend`-функция, которая создаёт дочернюю область и ждёт её завершения:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadAll(): List<String> = coroutineScope {
|
||||
val a = async { delay(50); "первый" }
|
||||
val b = async { delay(30); "второй" }
|
||||
listOf(a.await(), b.await())
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadAll()) // [первый, второй]
|
||||
}
|
||||
```
|
||||
Это стандартный способ сделать `suspend`-функцию, которая внутри работает параллельно: снаружи она выглядит как обычная функция, возвращающая значение, и гарантированно не оставляет фоновых задач после возврата.
|
||||
|
||||
Ошибка внутри отменяет остальных детей:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
try {
|
||||
coroutineScope {
|
||||
launch {
|
||||
try {
|
||||
delay(1000)
|
||||
println("не напечатается")
|
||||
} finally {
|
||||
println("ребёнка 1 отменили")
|
||||
}
|
||||
}
|
||||
launch {
|
||||
delay(20)
|
||||
throw IllegalStateException("ошибка во втором")
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
println("поймано: ${e.message}")
|
||||
}
|
||||
// ребёнка 1 отменили
|
||||
// поймано: ошибка во втором
|
||||
}
|
||||
```
|
||||
Это свойство «всё или ничего»: если часть параллельной работы провалилась, продолжать остальную обычно бессмысленно.
|
||||
|
||||
## supervisorScope: независимые дети
|
||||
Когда дети не должны зависеть друг от друга, нужен `supervisorScope`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
supervisorScope {
|
||||
launch {
|
||||
delay(20)
|
||||
throw IllegalStateException("первый упал")
|
||||
}
|
||||
launch {
|
||||
delay(50)
|
||||
println("второй выжил")
|
||||
}
|
||||
}
|
||||
println("область завершилась")
|
||||
// Exception in thread ... IllegalStateException: первый упал
|
||||
// второй выжил
|
||||
// область завершилась
|
||||
}
|
||||
```
|
||||
Ошибка первого не затронула второго. Обратите внимание: исключение всё равно печатается - его нужно обработать, иначе оно уйдёт в обработчик по умолчанию. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
|
||||
| | `coroutineScope` | `supervisorScope` |
|
||||
|---|---|---|
|
||||
| ошибка ребёнка | отменяет остальных детей и область | не затрагивает остальных |
|
||||
| ошибка пробрасывается наверх | да, из блока | нет, идёт в `CoroutineExceptionHandler` |
|
||||
| отмена сверху | отменяет всех детей | отменяет всех детей |
|
||||
| когда применять | части одной задачи | независимые подзадачи |
|
||||
|
||||
## Иерархия Job
|
||||
`Job` - дескриптор корутины: её состояние и связи с родителем и детьми.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val parent = launch {
|
||||
val child1 = launch { delay(1000) }
|
||||
val child2 = launch { delay(1000) }
|
||||
println("детей у родителя: ${coroutineContext[Job]!!.children.count()}")
|
||||
child1.cancel()
|
||||
println("child1 отменён: ${child1.isCancelled}")
|
||||
println("child2 активен: ${child2.isActive}")
|
||||
child2.cancel()
|
||||
}
|
||||
parent.join()
|
||||
println("родитель завершён: ${parent.isCompleted}")
|
||||
// детей у родителя: 2
|
||||
// child1 отменён: true
|
||||
// child2 активен: true
|
||||
// родитель завершён: true
|
||||
}
|
||||
```
|
||||
|
||||
Состояния `Job` и переходы между ними:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> New: "launch(start = LAZY)"
|
||||
[*] --> Active: "launch / async"
|
||||
New --> Active: "start() / join()"
|
||||
Active --> Completing: "тело завершилось"
|
||||
Completing --> Completed: "все дети завершились"
|
||||
Active --> Cancelling: "cancel() / ошибка"
|
||||
Completing --> Cancelling: "cancel() / ошибка ребёнка"
|
||||
Cancelling --> Cancelled: "все дети отменены"
|
||||
Completed --> [*]
|
||||
Cancelled --> [*]
|
||||
```
|
||||
|
||||
Флаги состояния:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch { delay(1000) }
|
||||
println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // true false false
|
||||
|
||||
job.cancel()
|
||||
job.join()
|
||||
println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // false true true
|
||||
}
|
||||
```
|
||||
Важная тонкость: у отменённой корутины `isCompleted` тоже `true` - завершение включает и успешное, и отменённое. Различать их нужно через `isCancelled`.
|
||||
|
||||
## Job как часть контекста
|
||||
`Job` можно передать явно, и тогда корутина станет ребёнком именно его:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val group = Job()
|
||||
|
||||
launch(group) { delay(1000); println("не напечатается 1") }
|
||||
launch(group) { delay(1000); println("не напечатается 2") }
|
||||
val independent = launch { delay(30); println("независимая выполнилась") }
|
||||
|
||||
delay(10)
|
||||
group.cancel() // отменяет только своих детей
|
||||
independent.join()
|
||||
println("готово")
|
||||
// независимая выполнилась
|
||||
// готово
|
||||
}
|
||||
```
|
||||
Так делают «группы» задач, отменяемые вместе. Но осторожно: при явном `Job` корутина перестаёт быть ребёнком `runBlocking`, и тот не будет её ждать.
|
||||
|
||||
## GlobalScope: почему его не надо
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch {
|
||||
delay(1000)
|
||||
println("этого вы не увидите")
|
||||
}
|
||||
delay(20)
|
||||
println("main завершился, не дождавшись")
|
||||
// main завершился, не дождавшись
|
||||
}
|
||||
```
|
||||
Корутина в `GlobalScope` живёт до конца процесса, её никто не отменит и никто не узнает об ошибке. Аннотация `@DelicateCoroutinesApi` поставлена специально - API помечено как опасное.
|
||||
|
||||
Правильные альтернативы:
|
||||
| Вместо `GlobalScope` | Используйте |
|
||||
|---|---|
|
||||
| фоновая задача в сервисе | свой `CoroutineScope` с явным `cancel()` |
|
||||
| задача на время запроса | `coroutineScope` внутри обработчика |
|
||||
| задача в Android ViewModel | `viewModelScope` |
|
||||
| задача в Android UI | `lifecycleScope` |
|
||||
| задача в тесте | `runTest` |
|
||||
| утилита или `main` | `runBlocking` |
|
||||
|
||||
Единственный оправданный случай - задача, которая действительно должна жить весь процесс (например, периодическая отправка метрик), и даже тогда лучше завести явный `applicationScope`.
|
||||
|
||||
## Утечка через внешний launch
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// плохо: функция возвращается, а работа продолжается неизвестно где
|
||||
fun CoroutineScope.badLoad(): Unit {
|
||||
launch { delay(100); println("ещё работаю") }
|
||||
}
|
||||
|
||||
// хорошо: функция suspend и не оставляет хвостов
|
||||
suspend fun goodLoad(): String = coroutineScope {
|
||||
val d = async { delay(20); "результат" }
|
||||
d.await()
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(goodLoad()) // результат
|
||||
badLoad()
|
||||
println("badLoad вернулся, но задача ещё жива")
|
||||
delay(150)
|
||||
// результат
|
||||
// badLoad вернулся, но задача ещё жива
|
||||
// ещё работаю
|
||||
}
|
||||
```
|
||||
Правило: публичная функция, выполняющая асинхронную работу, должна быть `suspend` и возвращать результат. Расширение на `CoroutineScope`, запускающее `launch`, оправдано только там, где «запустить и забыть» - это и есть контракт.
|
||||
|
||||
## Ожидание нескольких результатов
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadPage(n: Int): String { delay(20L * n); return "страница $n" }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val pages = coroutineScope {
|
||||
(1..3).map { async { loadPage(it) } }.awaitAll()
|
||||
}
|
||||
println(pages) // [страница 1, страница 2, страница 3]
|
||||
|
||||
val results = coroutineScope {
|
||||
(1..3).map { async { loadPage(it) } }.map { it.await() }
|
||||
}
|
||||
println(results) // [страница 1, страница 2, страница 3]
|
||||
}
|
||||
```
|
||||
`awaitAll` отличается от `map { it.await() }` поведением при ошибке: `awaitAll` падает сразу, как только упал любой из `Deferred`, а последовательные `await` дождутся своей очереди.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `ExecutorService` + `shutdown()` | `CoroutineScope` + `cancel()` |
|
||||
> | `CountDownLatch`, `invokeAll` | `coroutineScope`, `awaitAll` |
|
||||
> | `Future.cancel` по одной задаче | отмена по дереву |
|
||||
> | `StructuredTaskScope` (Java 21+) | `coroutineScope` |
|
||||
> | ошибка в `Runnable` теряется | ошибка идёт родителю |
|
||||
> | задачи не знают о родителе | иерархия `Job` |
|
||||
>
|
||||
> **В Java задачи плоские.** `ExecutorService` - это пул: вы отправляете задачу и получаете `Future`. Если задача запустит ещё три, пул о них ничего не узнает, `shutdown()` их не дождётся, а `Future.cancel` на родителе их не остановит. Отслеживать такие связи приходится вручную - списками `Future`, `CountDownLatch` и флагами.
|
||||
>
|
||||
> **`StructuredTaskScope` в Java 21 - прямой аналог `coroutineScope`.** Он появился в том же Project Loom именно потому, что проблема была признана фундаментальной:
|
||||
> ```java
|
||||
> try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
|
||||
> var user = scope.fork(() -> loadUser(id));
|
||||
> var orders = scope.fork(() -> loadOrders(id));
|
||||
> scope.join().throwIfFailed();
|
||||
> return new Result(user.get(), orders.get());
|
||||
> }
|
||||
> ```
|
||||
> Сравните с Kotlin:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadUser3(id: Int): String { delay(10); return "u$id" }
|
||||
> suspend fun loadOrders3(id: Int): List<String> { delay(10); return listOf("o$id") }
|
||||
>
|
||||
> suspend fun result(id: Int): String = coroutineScope {
|
||||
> val user = async { loadUser3(id) }
|
||||
> val orders = async { loadOrders3(id) }
|
||||
> "${user.await()} ${orders.await()}"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(result(1)) } // u1 [o1]
|
||||
> ```
|
||||
> Идея одна и та же: блок не завершится, пока не завершатся подзадачи. `ShutdownOnFailure` соответствует `coroutineScope`, `ShutdownOnSuccess` - гонке через `select`. Отличие в том, что в Kotlin это единственный способ работы, а в Java - новый опциональный API поверх существующих плоских пулов.
|
||||
>
|
||||
> **Ошибка в `Runnable` исчезает.** В `executor.submit(task)` исключение складывается в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто. В `executor.execute(task)` оно уйдёт в `UncaughtExceptionHandler` потока. Корутины гарантируют, что ошибка дойдёт до родителя и, если не обработана, до обработчика области.
|
||||
>
|
||||
> **Завершение пула против отмены области.** `shutdown()` ждёт текущих задач, `shutdownNow()` прерывает потоки, а прерывание работает только на блокирующих вызовах. `scope.cancel()` отменяет всё дерево кооперативно и предсказуемо. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`GlobalScope` - утечка по определению.** Задача не привязана ни к чему, не отменяется при закрытии компонента и может переживать его. В Android это классическая причина обращения к уничтоженному экрану.
|
||||
>
|
||||
> **`CoroutineScope(...)` без `cancel()` - та же утечка:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class Leaky {
|
||||
> private val scope = CoroutineScope(Dispatchers.Default)
|
||||
> fun start() { scope.launch { delay(10_000) } }
|
||||
> // нет метода stop — корутина живёт до конца процесса
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> Leaky().start()
|
||||
> println("объект больше не нужен, а корутина работает")
|
||||
> delay(20)
|
||||
> // объект больше не нужен, а корутина работает
|
||||
> }
|
||||
> ```
|
||||
> Если класс создаёт scope, у него обязан быть метод освобождения.
|
||||
>
|
||||
> **`Job()` вместо `SupervisorJob()` в собственном scope роняет весь scope при первой ошибке:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val scope = CoroutineScope(Job())
|
||||
> scope.launch { throw IllegalStateException("первая задача") }
|
||||
> delay(20)
|
||||
> val second = scope.launch { println("не запустится") }
|
||||
> delay(20)
|
||||
> println("scope отменён: ${!scope.isActive}, вторая отменена: ${second.isCancelled}")
|
||||
> // scope отменён: true, вторая отменена: true
|
||||
> }
|
||||
> ```
|
||||
> Для долгоживущего scope почти всегда нужен `SupervisorJob`, иначе одна ошибка выводит из строя весь компонент.
|
||||
>
|
||||
> **Передача `Job` в билдер разрывает связь с родителем:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> launch(Job()) { // свой Job — runBlocking НЕ ждёт
|
||||
> delay(200)
|
||||
> println("не напечатается")
|
||||
> }
|
||||
> println("main вышел")
|
||||
> // main вышел
|
||||
> }
|
||||
> ```
|
||||
> `launch(Job())` выглядит безобидно, но делает корутину сиротой. Чтобы просто сменить диспетчер, передавайте только диспетчер.
|
||||
>
|
||||
> **`supervisorScope` не защищает от ошибки в своём теле:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> try {
|
||||
> supervisorScope {
|
||||
> launch { delay(1000) }
|
||||
> throw IllegalStateException("ошибка в теле, не в ребёнке")
|
||||
> }
|
||||
> } catch (e: IllegalStateException) {
|
||||
> println("поймано: ${e.message}") // поймано: ошибка в теле, не в ребёнке
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Supervisor меняет поведение только для **детей**; код самого блока ведёт себя обычно и отменяет область.
|
||||
>
|
||||
> **`coroutineScope` внутри `launch` не делает дочерние ошибки локальными.** `coroutineScope` пробрасывает исключение наверх; если наверху `launch`, оно уйдёт родителю и может уронить всю область. Локализовать нужно через `try/catch` вокруг `coroutineScope` или через `supervisorScope`.
|
||||
>
|
||||
> **Наследование `CoroutineScope` классом публикует `launch` в API:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class Exposed : CoroutineScope {
|
||||
> override val coroutineContext = SupervisorJob() + Dispatchers.Default
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val e = Exposed()
|
||||
> e.launch { println("любой может запустить корутину в чужом scope") }
|
||||
> delay(20)
|
||||
> e.cancel()
|
||||
> // любой может запустить корутину в чужом scope
|
||||
> }
|
||||
> ```
|
||||
> Внешний код получает возможность запускать задачи в вашем scope. Поле `private val scope` предпочтительнее наследования.
|
||||
>
|
||||
> **`cancel()` на scope делает его непригодным навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val scope = CoroutineScope(SupervisorJob())
|
||||
> scope.cancel()
|
||||
> val job = scope.launch { println("не выполнится") }
|
||||
> delay(20)
|
||||
> println("корутина сразу отменена: ${job.isCancelled}") // корутина сразу отменена: true
|
||||
> }
|
||||
> ```
|
||||
> Отменённый scope не «перезапускается» - нужно создавать новый. В Android это причина ошибок при повторном использовании ViewModel-scope.
|
||||
>
|
||||
> **`awaitAll` и `map { await() }` ведут себя по-разному при ошибке.** `awaitAll` падает при первой ошибке любого элемента; последовательные `await()` сначала дождутся предыдущих. Для fail-fast нужен `awaitAll`.
|
||||
>
|
||||
> **Возврат `Deferred` из публичной функции ломает структурированность.** Вызывающий может не вызвать `await()`, и ошибка потеряется, либо вызвать его после закрытия scope. Возвращайте готовое значение из `suspend`-функции.
|
||||
|
||||
Далее: [Билдеры корутин](Билдеры%20корутин.md)
|
||||
@@ -1,306 +0,0 @@
|
||||
---
|
||||
title: Абстрактные классы
|
||||
description: abstract против open, шаблонный метод и выбор между абстрактным классом и интерфейсом
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Абстрактный класс нельзя создать - только унаследовать.
|
||||
|
||||
```kotlin
|
||||
abstract class Shape4 {
|
||||
abstract val area: Double // без реализации
|
||||
abstract fun name(): String
|
||||
|
||||
fun describe() = "${name()}: площадь %.2f".format(java.util.Locale.US, area)
|
||||
}
|
||||
|
||||
class Circle3(val radius: Double) : Shape4() {
|
||||
override val area get() = Math.PI * radius * radius
|
||||
override fun name() = "круг"
|
||||
}
|
||||
|
||||
class Square3(val side: Double) : Shape4() {
|
||||
override val area get() = side * side
|
||||
override fun name() = "квадрат"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val shapes = listOf(Circle3(1.0), Square3(2.0))
|
||||
shapes.forEach { println(it.describe()) }
|
||||
// круг: площадь 3.14
|
||||
// квадрат: площадь 4.00
|
||||
// println(Shape4()) // ошибка: нельзя создать абстрактный класс
|
||||
}
|
||||
```
|
||||
|
||||
## abstract и open
|
||||
`abstract` подразумевает `open` - писать его не нужно:
|
||||
```kotlin
|
||||
abstract class Base32 {
|
||||
abstract fun required() // open не нужен, реализации нет
|
||||
open fun optional() = "по умолчанию" // есть реализация, но переопределяема
|
||||
fun fixed() = "не переопределить" // final
|
||||
}
|
||||
|
||||
class Impl6 : Base32() {
|
||||
override fun required() = Unit
|
||||
override fun optional() = "своё"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val i = Impl6()
|
||||
println("${i.optional()} / ${i.fixed()}") // своё / не переопределить
|
||||
}
|
||||
```
|
||||
|
||||
| Модификатор | Реализация | Нужно переопределять |
|
||||
|---|---|---|
|
||||
| `abstract` | нет | обязательно |
|
||||
| `open` | есть | можно |
|
||||
| нет модификатора | есть | нельзя |
|
||||
|
||||
Абстрактный класс может не иметь ни одного абстрактного члена - тогда он просто запрещает создание экземпляра:
|
||||
```kotlin
|
||||
abstract class Utils {
|
||||
fun helper() = "общая логика"
|
||||
}
|
||||
|
||||
class Concrete : Utils()
|
||||
|
||||
fun main() { println(Concrete().helper()) } // общая логика
|
||||
```
|
||||
|
||||
## Абстрактный класс может иметь состояние
|
||||
Это главное отличие от интерфейса:
|
||||
```kotlin
|
||||
abstract class Repository2(protected val tableName: String) {
|
||||
private val cache = mutableMapOf<Int, String>() // состояние
|
||||
|
||||
protected abstract fun loadFromDb(id: Int): String?
|
||||
|
||||
fun find(id: Int): String? = cache.getOrPut(id) {
|
||||
loadFromDb(id) ?: return null
|
||||
}
|
||||
|
||||
fun cacheSize() = cache.size
|
||||
}
|
||||
|
||||
class UserRepository2 : Repository2("users") {
|
||||
override fun loadFromDb(id: Int) = if (id == 1) "Иван" else null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = UserRepository2()
|
||||
println(repo.find(1)) // Иван
|
||||
println(repo.find(1)) // Иван — из кеша
|
||||
println(repo.cacheSize()) // 1
|
||||
println(repo.find(99)) // null
|
||||
}
|
||||
```
|
||||
Конструктор, приватные поля, инициализация - всё доступно. У интерфейса этого нет.
|
||||
|
||||
## Шаблонный метод
|
||||
Самое частое применение: алгоритм зафиксирован в базовом классе, шаги переопределяются.
|
||||
```kotlin
|
||||
abstract class Importer {
|
||||
fun import(data: String): String {
|
||||
val parsed = parse(data)
|
||||
val valid = parsed.filter { validate(it) }
|
||||
return "загружено ${valid.size} из ${parsed.size}: $valid"
|
||||
}
|
||||
|
||||
protected abstract fun parse(data: String): List<String>
|
||||
protected open fun validate(item: String) = item.isNotBlank()
|
||||
}
|
||||
|
||||
class CsvImporter : Importer() {
|
||||
override fun parse(data: String) = data.split(",").map { it.trim() }
|
||||
}
|
||||
|
||||
class StrictCsvImporter : Importer() {
|
||||
override fun parse(data: String) = data.split(",").map { it.trim() }
|
||||
override fun validate(item: String) = item.length >= 3
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(CsvImporter().import("a, b, , c"))
|
||||
// загружено 3 из 4: [a, b, c]
|
||||
println(StrictCsvImporter().import("abc, b, defg"))
|
||||
// загружено 2 из 3: [abc, defg]
|
||||
}
|
||||
```
|
||||
`import` не `open` - порядок шагов менять нельзя. `parse` обязателен, `validate` имеет разумное поведение по умолчанию.
|
||||
|
||||
## protected для членов, нужных подклассам
|
||||
```kotlin
|
||||
abstract class Connection {
|
||||
protected var isOpen = false
|
||||
private set
|
||||
|
||||
protected abstract fun doConnect(): Boolean
|
||||
|
||||
fun connect(): String {
|
||||
isOpen = doConnect()
|
||||
return if (isOpen) "подключено" else "ошибка"
|
||||
}
|
||||
}
|
||||
|
||||
class TcpConnection(private val host: String) : Connection() {
|
||||
override fun doConnect() = host.isNotBlank()
|
||||
fun status() = if (isOpen) "$host: активно" else "$host: закрыто"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = TcpConnection("example.com")
|
||||
println(c.connect()) // подключено
|
||||
println(c.status()) // example.com: активно
|
||||
}
|
||||
```
|
||||
|
||||
## Абстрактный класс или интерфейс
|
||||
```kotlin
|
||||
interface Drawable { fun draw(): String } // контракт
|
||||
|
||||
abstract class Widget(val id: String) : Drawable { // + состояние и общая логика
|
||||
protected var visible = true
|
||||
fun hide() { visible = false }
|
||||
override fun draw() = if (visible) render() else ""
|
||||
protected abstract fun render(): String
|
||||
}
|
||||
|
||||
class Button(id: String, val label: String) : Widget(id) {
|
||||
override fun render() = "[$label]"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val b = Button("b1", "ОК")
|
||||
println(b.draw()) // [ОК]
|
||||
b.hide()
|
||||
println("'${b.draw()}'") // ''
|
||||
}
|
||||
```
|
||||
|
||||
| | Интерфейс | Абстрактный класс |
|
||||
|---|---|---|
|
||||
| сколько можно реализовать | много | один |
|
||||
| состояние (свойство с полем) | нет | да |
|
||||
| конструктор | нет | да |
|
||||
| `init` | нет | да |
|
||||
| `private`-члены | да (с реализацией) | да |
|
||||
| `protected`-члены | **нет** | да |
|
||||
| видимость членов | только `public` или `private` | любая |
|
||||
|
||||
Правило выбора:
|
||||
- **интерфейс** - когда описываете возможность («умеет рисоваться», «сравним»), особенно если её могут захотеть добавить к существующим классам
|
||||
- **абстрактный класс** - когда у наследников общее состояние или нужен шаблонный метод с `protected`-шагами
|
||||
|
||||
На практике часто сочетают: публичный интерфейс для клиентов и абстрактный класс как база для реализаций.
|
||||
|
||||
Подробнее: [Интерфейсы](Интерфейсы.md)
|
||||
|
||||
## Абстрактный класс и sealed
|
||||
```kotlin
|
||||
sealed class Result4 {
|
||||
abstract val isSuccess: Boolean
|
||||
}
|
||||
|
||||
class Ok2(val data: String) : Result4() {
|
||||
override val isSuccess = true
|
||||
}
|
||||
|
||||
class Fail(val error: String) : Result4() {
|
||||
override val isSuccess = false
|
||||
}
|
||||
|
||||
fun handle(r: Result4) = when (r) { // else не нужен
|
||||
is Ok2 -> "данные: ${r.data}"
|
||||
is Fail -> "ошибка: ${r.error}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle(Ok2("готово"))) // данные: готово
|
||||
println(Fail("сбой").isSuccess) // false
|
||||
}
|
||||
```
|
||||
`sealed` класс абстрактен по определению. Разница: `abstract` допускает наследников где угодно, `sealed` - только в своём модуле и пакете, зато даёт исчерпывающий `when`. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `abstract class A` | то же |
|
||||
> | `abstract void f();` | `abstract fun f()` |
|
||||
> | поля `protected` | свойства `protected` |
|
||||
> | абстрактное поле невозможно | `abstract val` |
|
||||
> | `default`-методы в интерфейсе (Java 8+) | реализация в интерфейсе без ключевого слова |
|
||||
>
|
||||
> **`abstract val` - то, чего в Java нет.** Java позволяет объявить абстрактным только метод, поэтому «абстрактное свойство» выражается парой геттер/сеттер:
|
||||
> ```java
|
||||
> abstract class Shape {
|
||||
> abstract double getArea(); // абстрактный геттер
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `abstract val area: Double` оставляет наследнику выбор: хранимое свойство, геттер или параметр конструктора. Клиент разницы не видит.
|
||||
>
|
||||
> **`abstract` не требует `open`**, потому что отсутствие реализации уже означает необходимость переопределения. В Java `final abstract` - ошибка компиляции по той же причине.
|
||||
>
|
||||
> **Доля абстрактных классов в Kotlin ниже.** Часть их задач закрывают другие средства: `sealed` для закрытых иерархий, делегирование для переиспользования кода, функции-расширения для добавления поведения без наследования.
|
||||
>
|
||||
> Сравнить: [Абстрактные классы в Java](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Вызов абстрактного метода из `init` - гарантированная ловушка:**
|
||||
> ```kotlin
|
||||
> abstract class Base33 {
|
||||
> init { println("из init: ${describe()}") }
|
||||
> abstract fun describe(): String
|
||||
> }
|
||||
>
|
||||
> class Derived32 : Base33() {
|
||||
> private val name: String = "Иван"
|
||||
> override fun describe() = "name=$name"
|
||||
> }
|
||||
>
|
||||
> fun main() { Derived32() } // из init: name=null
|
||||
> ```
|
||||
> Подкласс ещё не инициализирован, поэтому non-null свойство оказывается `null`. Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Абстрактный класс расходует единственный слот наследования.** Класс, унаследованный от абстрактного, больше ни от чего наследоваться не сможет. Поэтому публичный API лучше начинать с интерфейса.
|
||||
>
|
||||
> **`protected abstract` - часть контракта с наследниками**, и менять его так же больно, как публичный API: изменение сигнатуры сломает все подклассы.
|
||||
>
|
||||
> **Абстрактный класс с конструктором усложняет тестирование.** Чтобы проверить логику шаблонного метода, нужен наследник; с интерфейсом достаточно лямбды или объекта-выражения.
|
||||
>
|
||||
> **`abstract` и `private` несовместимы** - приватный член невозможно переопределить:
|
||||
> ```kotlin
|
||||
> abstract class Base34 {
|
||||
> // private abstract fun f() // ошибка
|
||||
> protected abstract fun f(): String
|
||||
> fun run3() = f()
|
||||
> }
|
||||
>
|
||||
> class Impl7 : Base34() { override fun f() = "готово" }
|
||||
>
|
||||
> fun main() { println(Impl7().run3()) } // готово
|
||||
> ```
|
||||
>
|
||||
> **Абстрактное свойство легко переопределить несогласованно:**
|
||||
> ```kotlin
|
||||
> abstract class Base35 { abstract val id: Int }
|
||||
>
|
||||
> class Unstable : Base35() {
|
||||
> override val id: Int get() = (0..100).random() // разное при каждом чтении
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val u = Unstable()
|
||||
> println(u.id == u.id) // чаще false
|
||||
> }
|
||||
> ```
|
||||
> Контракт «свойство стабильно» компилятором не проверяется; при использовании в `equals` или как ключа это даст тяжёлые для отладки ошибки.
|
||||
>
|
||||
> **Глубокие абстрактные иерархии трудны для чтения.** Если шаблонный метод вызывает абстрактные шаги, которые вызывают другие шаблонные методы, поведение собирается из четырёх файлов.
|
||||
|
||||
Дополнительно: [Интерфейсы](Интерфейсы.md)
|
||||
Далее: [Интерфейсы](Интерфейсы.md)
|
||||
@@ -1,284 +0,0 @@
|
||||
---
|
||||
title: Вложенные и внутренние классы
|
||||
description: Вложенный класс по умолчанию, inner для ссылки на внешний объект и утечки памяти
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Класс внутри класса по умолчанию **не** имеет ссылки на внешний объект. Чтобы её получить, нужен `inner`.
|
||||
|
||||
```kotlin
|
||||
class Outer5 {
|
||||
private val secret = "скрыто"
|
||||
|
||||
class Nested2 {
|
||||
fun describe() = "вложенный, внешний объект недоступен"
|
||||
// fun bad() = secret // ошибка: нет доступа
|
||||
}
|
||||
|
||||
inner class Inner5 {
|
||||
fun describe() = "внутренний, вижу: $secret"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Outer5.Nested2().describe()) // внешний объект не нужен
|
||||
println(Outer5().Inner5().describe()) // нужен экземпляр Outer5
|
||||
}
|
||||
```
|
||||
|
||||
| | вложенный (по умолчанию) | `inner` |
|
||||
|---|---|---|
|
||||
| ссылка на внешний объект | нет | есть |
|
||||
| доступ к членам внешнего класса | только к членам `companion object` | ко всем, включая `private` |
|
||||
| как создать | `Outer.Nested()` | `outer.Inner()` |
|
||||
| аналог в Java | `static class` | обычный вложенный класс |
|
||||
| риск утечки памяти | нет | есть |
|
||||
|
||||
Обратите внимание на создание: `Outer5().Inner5()` - сначала внешний объект, затем внутренний на нём.
|
||||
|
||||
## Зачем вложенные классы
|
||||
Группировка по смыслу: класс нужен только в контексте внешнего и не имеет смысла отдельно.
|
||||
```kotlin
|
||||
class HttpClient2 {
|
||||
class Request2(val url: String, val method: String = "GET")
|
||||
class Response(val code: Int, val body: String)
|
||||
|
||||
fun send(request: Request2): Response =
|
||||
Response(200, "ответ на ${request.method} ${request.url}")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val client = HttpClient2()
|
||||
val response = client.send(HttpClient2.Request2("http://x.ru"))
|
||||
println("${response.code}: ${response.body}") // 200: ответ на GET http://x.ru
|
||||
}
|
||||
```
|
||||
Имя `HttpClient2.Request2` само документирует принадлежность, и пакет не засоряется.
|
||||
|
||||
Частый случай - варианты sealed-иерархии:
|
||||
```kotlin
|
||||
sealed interface State3 {
|
||||
data object Loading3 : State3
|
||||
data class Ready(val data: String) : State3
|
||||
data class Error3(val message: String) : State3
|
||||
}
|
||||
|
||||
fun render(s: State3) = when (s) {
|
||||
State3.Loading3 -> "загрузка"
|
||||
is State3.Ready -> "готово: ${s.data}"
|
||||
is State3.Error3 -> "ошибка: ${s.message}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(render(State3.Ready("данные"))) // готово: данные
|
||||
println(render(State3.Loading3)) // загрузка
|
||||
}
|
||||
```
|
||||
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Зачем inner
|
||||
Когда внутренний объект логически часть конкретного экземпляра и должен видеть его состояние.
|
||||
```kotlin
|
||||
class Table(private val name: String) {
|
||||
private val rows = mutableListOf<String>()
|
||||
|
||||
inner class Row(private val data: String) {
|
||||
fun insert(): String {
|
||||
rows.add(data) // меняем состояние внешнего
|
||||
return "в таблицу $name добавлено '$data', всего ${rows.size}"
|
||||
}
|
||||
}
|
||||
|
||||
fun newRow(data: String) = Row(data)
|
||||
fun count() = rows.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val t = Table("users")
|
||||
println(t.newRow("Иван").insert()) // в таблицу users добавлено 'Иван', всего 1
|
||||
println(t.newRow("Пётр").insert()) // в таблицу users добавлено 'Пётр', всего 2
|
||||
println(t.count()) // 2
|
||||
}
|
||||
```
|
||||
|
||||
## this в inner-классе
|
||||
При совпадении имён нужна квалификация метками:
|
||||
```kotlin
|
||||
class Outer6(val name: String = "внешний") {
|
||||
inner class Inner6(val name: String = "внутренний") {
|
||||
fun describe() = "${this.name} / ${this@Outer6.name}"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Outer6().Inner6().describe()) // внутренний / внешний
|
||||
}
|
||||
```
|
||||
`this@Outer6` - ссылка на внешний объект. Без метки `this` относится к ближайшему классу.
|
||||
|
||||
То же для `super`:
|
||||
```kotlin
|
||||
open class Base36 { open fun f() = "база" }
|
||||
|
||||
class Outer7 : Base36() {
|
||||
override fun f() = "внешний"
|
||||
|
||||
inner class Inner7 {
|
||||
fun both() = "${f()} / ${super@Outer7.f()}"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() { println(Outer7().Inner7().both()) } // внешний / база
|
||||
```
|
||||
|
||||
## Локальные классы
|
||||
Класс можно объявить внутри функции:
|
||||
```kotlin
|
||||
fun process(items: List<Int>): String {
|
||||
val prefix = ">> "
|
||||
|
||||
class Formatter(val value: Int) {
|
||||
fun format() = "$prefix$value" // захватывает prefix
|
||||
}
|
||||
|
||||
return items.map { Formatter(it).format() }.joinToString()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(process(listOf(1, 2))) // >> 1, >> 2
|
||||
}
|
||||
```
|
||||
Локальный класс видит переменные функции, включая изменяемые. Нужен редко: обычно хватает [объекта-выражения](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) или лямбды.
|
||||
|
||||
## Вложенный интерфейс и object
|
||||
```kotlin
|
||||
class Service17 {
|
||||
fun interface Listener { // интерфейс всегда "вложенный"
|
||||
fun onEvent(name: String): String
|
||||
}
|
||||
|
||||
object Defaults { // object не может быть inner
|
||||
const val TIMEOUT = 30
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create() = Service17()
|
||||
}
|
||||
|
||||
fun notify2(l: Listener) = l.onEvent("старт")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service17.create()
|
||||
println(s.notify2 { "получено: $it" }) // получено: старт
|
||||
println(Service17.Defaults.TIMEOUT) // 30
|
||||
}
|
||||
```
|
||||
Интерфейсы, `object`, `enum` и `data class` не могут быть `inner` - у них нет смысла быть привязанными к экземпляру.
|
||||
|
||||
## Вложенность и дженерики
|
||||
`inner`-класс видит параметры типа внешнего:
|
||||
```kotlin
|
||||
class Box3<T>(private val value: T) {
|
||||
inner class Wrapper { // T доступен
|
||||
fun get(): T = value
|
||||
fun describe() = "обёртка над ${value}"
|
||||
}
|
||||
|
||||
class Helper4<U>(val other: U) // вложенный: свой параметр типа
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val b = Box3("данные")
|
||||
println(b.Wrapper().get()) // данные
|
||||
println(b.Wrapper().describe()) // обёртка над данные
|
||||
println(Box3.Helper4(42).other) // 42
|
||||
}
|
||||
```
|
||||
Вложенный класс параметров внешнего не видит - ему нужны свои.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `static class Nested` | `class Nested` (по умолчанию) |
|
||||
> | `class Inner` (без static) | `inner class Inner` |
|
||||
> | `outer.new Inner()` | `outer.Inner()` |
|
||||
> | `Outer.this.field` | `this@Outer.field` |
|
||||
> | `Outer.super.method()` | `super@Outer.method()` |
|
||||
> | анонимный класс | объект-выражение |
|
||||
>
|
||||
> **Значения по умолчанию перевёрнуты - и это важно.** В Java вложенный класс по умолчанию держит ссылку на внешний объект, и чтобы этого избежать, нужно написать `static`. Разработчики об этом забывают, из-за чего возникают утечки памяти: внутренний объект, переживший внешний, не даёт собрать его сборщику мусора.
|
||||
>
|
||||
> В Kotlin по умолчанию ссылки нет - её нужно запросить словом `inner`. Безопасный вариант стал умолчанием, а опасный требует явного решения. Это особенно заметно в Android, где утечки активити через нестатические внутренние классы и анонимные обработчики были массовой проблемой.
|
||||
>
|
||||
> **`Outer.this` против `this@Outer`.** Синтаксис меток в Kotlin единообразен: та же `@`-нотация используется для `return@forEach` и `this@apply` в scope-функциях.
|
||||
>
|
||||
> Сравнить: [Вложенные классы в Java](../../Программирование%20на%20Java/ООП/Вложенные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`inner` держит внешний объект в памяти - это утечка.** Если внутренний объект переживает внешний (попал в долгоживущую коллекцию, кеш или обработчик), внешний не будет собран:
|
||||
> ```kotlin
|
||||
> class Activity2(val heavyData: ByteArray = ByteArray(1024)) {
|
||||
> inner class Handler3 {
|
||||
> fun handle() = "обработка, внешний объект жив"
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> val globalHandlers = mutableListOf<Activity2.Handler3>()
|
||||
>
|
||||
> fun main() {
|
||||
> globalHandlers.add(Activity2().Handler3())
|
||||
> println(globalHandlers.first().handle())
|
||||
> println("Activity2 не может быть собран: он держится ссылкой из Handler3")
|
||||
> }
|
||||
> ```
|
||||
> Правило: `inner` только когда нужен доступ к состоянию внешнего объекта и время жизни внутреннего не превышает внешнего.
|
||||
>
|
||||
> **Вложенный класс не видит членов экземпляра внешнего класса** - частая ошибка при переносе кода из Java, где поведение обратное:
|
||||
> ```kotlin
|
||||
> class Outer8 {
|
||||
> private val value = 1
|
||||
> class Nested3 {
|
||||
> // fun bad() = value // ошибка
|
||||
> fun ok() = "нужен inner или параметр"
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Outer8.Nested3().ok()) }
|
||||
> ```
|
||||
>
|
||||
> **Создание `inner`-класса требует экземпляра внешнего:**
|
||||
> ```kotlin
|
||||
> class Outer9 {
|
||||
> inner class Inner8
|
||||
> class Nested4
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Outer9.Nested4()::class.simpleName) // Nested4
|
||||
> println(Outer9().Inner8()::class.simpleName) // Inner8
|
||||
> // Outer9.Inner8() // ошибка
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`data class` не может быть `inner`:**
|
||||
> ```kotlin
|
||||
> class Outer10 {
|
||||
> data class Point5(val x: Int) // вложенный — ок
|
||||
> // inner data class Bad(val x: Int) // ошибка
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Outer10.Point5(1)) } // Point5(x=1)
|
||||
> ```
|
||||
> Сгенерированные `equals` и `copy` не учитывали бы ссылку на внешний объект, что дало бы противоречивое поведение.
|
||||
>
|
||||
> **Глубокая вложенность ухудшает читаемость.** `Outer.Middle.Inner.Deep` в сигнатурах - сигнал, что классы стоит вынести на верхний уровень или в отдельный файл.
|
||||
>
|
||||
> **Вложенный класс попадает в публичный API под составным именем.** Переименование внешнего класса меняет полное имя вложенного - для библиотеки это ломающее изменение.
|
||||
>
|
||||
> **Локальный класс в функции захватывает переменные** и продлевает их жизнь, как и лямбда. Если объект локального класса возвращается из функции, захваченные значения остаются в памяти.
|
||||
|
||||
Дополнительно: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
Далее: [Полиморфизм](Полиморфизм.md)
|
||||
@@ -1,372 +0,0 @@
|
||||
---
|
||||
title: Делегирование
|
||||
description: Делегирование интерфейсов через by, делегаты свойств и границы автоматического перенаправления
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Делегирование - передача работы другому объекту. Kotlin поддерживает его на уровне языка словом `by`, поэтому композиция требует не больше кода, чем наследование.
|
||||
|
||||
## Делегирование интерфейса
|
||||
```kotlin
|
||||
interface Logger5 {
|
||||
fun log(msg: String): String
|
||||
fun warn(msg: String): String
|
||||
}
|
||||
|
||||
class ConsoleLogger3 : Logger5 {
|
||||
override fun log(msg: String) = "[LOG] $msg"
|
||||
override fun warn(msg: String) = "[WARN] $msg"
|
||||
}
|
||||
|
||||
class Service15(logger: Logger5) : Logger5 by logger {
|
||||
fun work() = log("начинаю работу")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service15(ConsoleLogger3())
|
||||
println(s.work()) // [LOG] начинаю работу
|
||||
println(s.warn("внимание")) // [WARN] внимание — метод перенаправлен
|
||||
}
|
||||
```
|
||||
`Service15` реализует `Logger5`, но код методов не пишет: компилятор генерирует перенаправление на `logger`. Унаследованной реализации при этом нет - есть ссылка на другой объект.
|
||||
|
||||
## Переопределение части методов
|
||||
Делегировать можно всё, кроме того, что нужно изменить:
|
||||
```kotlin
|
||||
interface Repository3 {
|
||||
fun find(id: Int): String?
|
||||
fun save(id: Int, value: String): String
|
||||
}
|
||||
|
||||
class DbRepository : Repository3 {
|
||||
private val data = mutableMapOf<Int, String>()
|
||||
override fun find(id: Int) = data[id]
|
||||
override fun save(id: Int, value: String): String {
|
||||
data[id] = value
|
||||
return "сохранено"
|
||||
}
|
||||
}
|
||||
|
||||
class CachingRepository(private val origin: Repository3) : Repository3 by origin {
|
||||
private val cache = mutableMapOf<Int, String>()
|
||||
|
||||
override fun find(id: Int): String? = // только этот метод свой
|
||||
cache.getOrPut(id) { origin.find(id) ?: return null }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val repo = CachingRepository(DbRepository())
|
||||
println(repo.save(1, "Иван")) // сохранено — делегировано
|
||||
println(repo.find(1)) // Иван — через кеш
|
||||
println(repo.find(1)) // Иван — уже из кеша
|
||||
println(repo.find(99)) // null
|
||||
}
|
||||
```
|
||||
Это декоратор в три строки. На наследовании он потребовал бы `open`-класса и был бы связан с его внутренностями.
|
||||
|
||||
## Делегирование нескольких интерфейсов
|
||||
```kotlin
|
||||
interface Reader { fun read(): String }
|
||||
interface Writer2 { fun write(data: String): String }
|
||||
|
||||
class FileReader2 : Reader { override fun read() = "данные из файла" }
|
||||
class FileWriter2 : Writer2 { override fun write(data: String) = "записано: $data" }
|
||||
|
||||
class ReadWriteFile(
|
||||
reader: Reader,
|
||||
writer: Writer2,
|
||||
) : Reader by reader, Writer2 by writer
|
||||
|
||||
fun main() {
|
||||
val f = ReadWriteFile(FileReader2(), FileWriter2())
|
||||
println(f.read()) // данные из файла
|
||||
println(f.write("текст")) // записано: текст
|
||||
}
|
||||
```
|
||||
Это даёт эффект множественного наследования реализации - но без его проблем, потому что каждый интерфейс обслуживает отдельный объект.
|
||||
|
||||
## Расширение коллекции
|
||||
Классическая задача, которую нельзя безопасно решить наследованием:
|
||||
```kotlin
|
||||
class CountingList3<T>(private val inner: MutableList<T> = mutableListOf())
|
||||
: MutableList<T> by inner {
|
||||
|
||||
var addedCount = 0
|
||||
private set
|
||||
|
||||
override fun add(element: T): Boolean {
|
||||
addedCount++
|
||||
return inner.add(element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>): Boolean {
|
||||
addedCount += elements.size
|
||||
return inner.addAll(elements)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val list = CountingList3<Int>()
|
||||
list.add(1)
|
||||
list.addAll(listOf(2, 3))
|
||||
println("${list.size} элементов, добавлений: ${list.addedCount}") // 3 элементов, добавлений: 3
|
||||
println(list.toList()) // [1, 2, 3]
|
||||
}
|
||||
```
|
||||
При наследовании от `ArrayList` счётчик мог бы удвоиться, если `addAll` внутри вызывает `add`. С делегированием внутренние вызовы `inner` остаются внутри `inner` и наших переопределений не касаются.
|
||||
|
||||
Обратите внимание на `list.toList()`: сам `println(list)` вывел бы `CountingList3@5fa7e7ff`, потому что `toString` - член `Any`, а члены `Any` делегирование не перенаправляет.
|
||||
|
||||
Эта же особенность - источник главной ловушки делегирования, см. подводные камни.
|
||||
|
||||
## Делегаты свойств
|
||||
Тот же `by` для свойства передаёт работу объекту с методами `getValue` и `setValue`.
|
||||
|
||||
### Готовые делегаты
|
||||
```kotlin
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class Form3 {
|
||||
val config: String by lazy { "вычислено один раз" }
|
||||
|
||||
var name: String by Delegates.observable("") { _, old, new ->
|
||||
println("name: '$old' -> '$new'")
|
||||
}
|
||||
|
||||
var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 }
|
||||
|
||||
var port: Int by Delegates.notNull()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val f = Form3()
|
||||
println(f.config) // вычислено один раз
|
||||
f.name = "Иван" // name: '' -> 'Иван'
|
||||
f.age = 200 // отклонено
|
||||
println(f.age) // 0
|
||||
f.port = 8080
|
||||
println(f.port) // 8080
|
||||
}
|
||||
```
|
||||
Подробнее: [Свойства](Свойства.md)
|
||||
|
||||
### Делегирование свойства в Map
|
||||
```kotlin
|
||||
class Settings(private val map: Map<String, Any?>) {
|
||||
val host: String by map
|
||||
val port: Int by map
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Settings(mapOf("host" to "localhost", "port" to 8080))
|
||||
println("${s.host}:${s.port}") // localhost:8080
|
||||
}
|
||||
```
|
||||
Имя свойства становится ключом. Для `var` нужен `MutableMap`.
|
||||
|
||||
### Свой делегат
|
||||
Нужны операторы `getValue` и (для `var`) `setValue`:
|
||||
```kotlin
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class NonBlank(private var value: String = "") {
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
|
||||
require(newValue.isNotBlank()) { "${property.name} не может быть пустым" }
|
||||
value = newValue.trim()
|
||||
}
|
||||
}
|
||||
|
||||
class User6 {
|
||||
var name: String by NonBlank()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User6()
|
||||
u.name = " Иван "
|
||||
println("'${u.name}'") // 'Иван'
|
||||
try {
|
||||
u.name = " "
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // name не может быть пустым
|
||||
}
|
||||
}
|
||||
```
|
||||
`property` даёт имя и метаданные свойства - удобно для сообщений об ошибках и для чтения из конфигурации по имени.
|
||||
|
||||
Для делегата только на чтение достаточно реализовать `ReadOnlyProperty`:
|
||||
```kotlin
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
|
||||
fun envVar(name: String) = ReadOnlyProperty<Any?, String> { _, _ ->
|
||||
System.getenv(name) ?: "<не задано>"
|
||||
}
|
||||
|
||||
class Config7 {
|
||||
val home: String by envVar("HOME")
|
||||
val missing: String by envVar("НЕТ_ТАКОЙ_ПЕРЕМЕННОЙ")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config7().home.isNotEmpty()) // true
|
||||
println(Config7().missing) // <не задано>
|
||||
}
|
||||
```
|
||||
|
||||
### provideDelegate
|
||||
Позволяет проверить свойство при создании делегата - например, что имя есть в конфигурации:
|
||||
```kotlin
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class Required(private val source: Map<String, String>) {
|
||||
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): Required {
|
||||
require(property.name in source) { "нет ключа '${property.name}'" }
|
||||
return this
|
||||
}
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = source[property.name]!!
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val source = mapOf("host" to "localhost")
|
||||
|
||||
class Ok3 { val host: String by Required(source) }
|
||||
println(Ok3().host) // localhost
|
||||
|
||||
try {
|
||||
class Bad4 { val port: String by Required(source) }
|
||||
Bad4()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // нет ключа 'port'
|
||||
}
|
||||
}
|
||||
```
|
||||
Проверка происходит при создании объекта, а не при первом чтении - ошибка обнаруживается раньше.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | все методы перенаправлять вручную | `by` генерирует их |
|
||||
> | декоратор = класс с полем и 10 методами | `class D(x: I) : I by x` |
|
||||
> | `Supplier` + `volatile` для ленивости | `by lazy` |
|
||||
> | `PropertyChangeSupport` | `Delegates.observable` |
|
||||
> | нет аналога делегатов свойств | `by` с `getValue`/`setValue` |
|
||||
>
|
||||
> **Делегирование интерфейса - главная экономия.** В Java декоратор над интерфейсом из десяти методов требует десять методов-перенаправлений, даже если менять нужно один. Любой новый метод в интерфейсе нужно добавить во все декораторы. Kotlin генерирует перенаправление сам:
|
||||
> ```java
|
||||
> class CachingRepository implements Repository {
|
||||
> private final Repository origin;
|
||||
> CachingRepository(Repository origin) { this.origin = origin; }
|
||||
> public String save(int id, String v) { return origin.save(id, v); } // шум
|
||||
> public String find(int id) { /* своя логика */ }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Делегатов свойств в Java нет совсем.** Ленивое поле требует ручной двойной проверки блокировки, наблюдаемое - `PropertyChangeSupport` и явных вызовов. В Kotlin это `by lazy` и `by Delegates.observable`.
|
||||
>
|
||||
> **Паттерн Декоратор перестаёт быть паттерном** - он становится однострочной конструкцией языка, поэтому его применяют там, где в Java предпочли бы наследование «чтобы не писать много кода».
|
||||
>
|
||||
> Сравнить: [Полиморфизм подтипов в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Абстрактные классы](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Делегат не знает о переопределениях - это главная ловушка.** Если один метод делегата вызывает другой, он вызовет **свою** версию, а не вашу:
|
||||
> ```kotlin
|
||||
> interface Sender {
|
||||
> fun send(msg: String): String
|
||||
> fun sendAll(msgs: List<String>): String
|
||||
> }
|
||||
>
|
||||
> class BasicSender : Sender {
|
||||
> override fun send(msg: String) = "отправлено: $msg"
|
||||
> override fun sendAll(msgs: List<String>) = msgs.joinToString { send(it) }
|
||||
> }
|
||||
>
|
||||
> class LoggingSender(private val inner: Sender) : Sender by inner {
|
||||
> var logged = 0
|
||||
> private set
|
||||
>
|
||||
> override fun send(msg: String): String {
|
||||
> logged++
|
||||
> return inner.send(msg)
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s = LoggingSender(BasicSender())
|
||||
> s.sendAll(listOf("a", "b"))
|
||||
> println("залогировано: ${s.logged}") // 0 — наш send не вызвался!
|
||||
> }
|
||||
> ```
|
||||
> `sendAll` делегирован и внутри вызывает `send` объекта `inner`. Чтобы счётчик работал, нужно переопределить и `sendAll`. Это обратная сторона изоляции: наследование здесь сработало бы «как ожидается», но стало бы хрупким.
|
||||
>
|
||||
> **Члены `Any` не делегируются.** `toString`, `equals` и `hashCode` остаются реализациями по умолчанию, то есть по ссылке:
|
||||
> ```kotlin
|
||||
> class Wrapped<T>(private val inner: MutableList<T> = mutableListOf()) : MutableList<T> by inner
|
||||
>
|
||||
> fun main() {
|
||||
> val a = Wrapped<Int>().apply { add(1) }
|
||||
> val b = Wrapped<Int>().apply { add(1) }
|
||||
> println(a.toString()) // Wrapped@4554617c, а не [1]
|
||||
> println(a == b) // false, хотя содержимое одинаково
|
||||
> println(a == listOf(1)) // false
|
||||
> }
|
||||
> ```
|
||||
> Обёртка над коллекцией, попавшая в `Set` или в сравнение, поведёт себя не как коллекция. Эти три метода нужно переопределять вручную. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
>
|
||||
> **Делегат фиксируется при создании** и не может быть заменён:
|
||||
> ```kotlin
|
||||
> fun interface Source { fun get(): String }
|
||||
>
|
||||
> class Service16(private var source: Source) : Source by source {
|
||||
> fun replace(newSource: Source) { source = newSource } // не влияет на by!
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s = Service16 { "первый" }
|
||||
> s.replace { "второй" }
|
||||
> println(s.get()) // первый — by хранит исходную ссылку
|
||||
> }
|
||||
> ```
|
||||
> `by` копирует ссылку в скрытое поле при создании объекта. Для подменяемого делегата нужно перенаправлять вручную.
|
||||
>
|
||||
> **Делегирование нельзя сделать `by this`** или сослаться на собственное свойство в заголовке - объект ещё не создан.
|
||||
>
|
||||
> **Из делегированных методов нет доступа к `this` обёртки.** Делегат не знает, что его кто-то оборачивает, - он не может вызвать методы обёртки.
|
||||
>
|
||||
> **`by lazy` на `var` невозможен** - только `val`.
|
||||
>
|
||||
> **Делегат свойства - объект на каждый экземпляр:**
|
||||
> ```kotlin
|
||||
> class Heavy {
|
||||
> val a: String by lazy { "a" }
|
||||
> val b: String by lazy { "b" } // ещё один объект Lazy
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Heavy().a + Heavy().b) } // ab
|
||||
> ```
|
||||
> Для класса с десятками ленивых свойств, создаваемого миллионами экземпляров, это заметный расход памяти.
|
||||
>
|
||||
> **Делегирование в `Map` падает в рантайме при отсутствии ключа:**
|
||||
> ```kotlin
|
||||
> class Settings2(map: Map<String, Any?>) {
|
||||
> val host: String by map
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s = Settings2(emptyMap())
|
||||
> try {
|
||||
> println(s.host)
|
||||
> } catch (e: NoSuchElementException) {
|
||||
> println("ключ отсутствует") // ключ отсутствует
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Проверить это заранее помогает `provideDelegate`.
|
||||
>
|
||||
> **Делегирование `MutableList` раскрывает полный изменяемый API.** Обёртка, делегирующая `MutableList`, позволяет внешнему коду добавлять элементы в обход вашей логики - через `iterator().remove()`, `subList()` и другие пути.
|
||||
|
||||
Дополнительно: [Свойства](Свойства.md)
|
||||
Далее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md)
|
||||
@@ -1,320 +0,0 @@
|
||||
---
|
||||
title: Инициализация
|
||||
description: Порядок выполнения init и инициализаторов, инициализация при наследовании и ловушка open-метода
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Инициализаторы свойств и блоки `init` выполняются **в порядке их объявления** в теле класса - как единая последовательность.
|
||||
|
||||
```kotlin
|
||||
class Order4 {
|
||||
val a = println("1: свойство a").let { "a" }
|
||||
init { println("2: первый init") }
|
||||
val b = println("3: свойство b").let { "b" }
|
||||
init { println("4: второй init") }
|
||||
|
||||
constructor() { println("5: тело конструктора") }
|
||||
}
|
||||
|
||||
fun main() { Order4() }
|
||||
```
|
||||
Вывод: `1`, `2`, `3`, `4`, `5`. Тело вторичного конструктора идёт **последним**, после всех инициализаторов.
|
||||
|
||||
Блоков `init` может быть несколько; их используют, когда инициализация не сводится к одному выражению.
|
||||
|
||||
## Параметры конструктора доступны сразу
|
||||
```kotlin
|
||||
class User3(name: String, age: Int) {
|
||||
val displayName = name.uppercase()
|
||||
val category: String
|
||||
|
||||
init {
|
||||
category = when {
|
||||
age < 18 -> "несовершеннолетний"
|
||||
age < 65 -> "взрослый"
|
||||
else -> "пенсионер"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User3("иван", 20)
|
||||
println("${u.displayName}: ${u.category}") // ИВАН: взрослый
|
||||
}
|
||||
```
|
||||
|
||||
## Порядок при наследовании
|
||||
Базовый класс инициализируется **полностью** до начала инициализации подкласса:
|
||||
```kotlin
|
||||
open class Base4 {
|
||||
val baseField = println("1: свойство Base").let { "base" }
|
||||
init { println("2: init Base") }
|
||||
}
|
||||
|
||||
class Derived4 : Base4() {
|
||||
val derivedField = println("3: свойство Derived").let { "derived" }
|
||||
init { println("4: init Derived") }
|
||||
}
|
||||
|
||||
fun main() { Derived4() }
|
||||
```
|
||||
Вывод: `1`, `2`, `3`, `4`. Внутри каждого класса - строго порядок объявления; базовый класс целиком раньше подкласса. Переставьте `init` выше свойства - и номера пойдут как `2`, `1`, `4`, `3`.
|
||||
|
||||
Аргументы для конструктора базового класса вычисляются **до** всего остального:
|
||||
```kotlin
|
||||
open class Base5(val value: String) {
|
||||
init { println("2: init Base, value=$value") }
|
||||
}
|
||||
|
||||
class Derived5(input: String) : Base5(input.uppercase().also { println("1: аргумент=$it") }) {
|
||||
init { println("3: init Derived") }
|
||||
}
|
||||
|
||||
fun main() { Derived5("иван") }
|
||||
```
|
||||
|
||||
## Ловушка: open-метод в конструкторе
|
||||
Это главная ошибка инициализации. Вызов `open`-метода из конструктора базового класса попадёт в переопределённую версию - **до** того, как подкласс инициализирован.
|
||||
|
||||
```kotlin
|
||||
open class Base6 {
|
||||
init { show() } // опасно
|
||||
open fun show() = println("Base")
|
||||
}
|
||||
|
||||
class Derived6 : Base6() {
|
||||
val name: String = "Иван"
|
||||
override fun show() = println("Derived, name='$name'")
|
||||
}
|
||||
|
||||
fun main() { Derived6() }
|
||||
```
|
||||
Вывод: `Derived, name='null'`. Свойство `name` объявлено non-null, но в этот момент ещё не присвоено - и мы наблюдаем `null` в типе `String`.
|
||||
|
||||
Если свойство подкласса имеет примитивный тип, вместо `null` будет `0` - и это ещё труднее заметить, потому что исключения не возникает:
|
||||
```kotlin
|
||||
open class Base7 {
|
||||
init { println("размер при инициализации: ${size()}") }
|
||||
open fun size() = 0
|
||||
}
|
||||
|
||||
class Derived7 : Base7() {
|
||||
private val count: Int = 3
|
||||
override fun size() = count
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val d = Derived7()
|
||||
println("размер после: ${d.size()}")
|
||||
}
|
||||
```
|
||||
Вывод: `0`, затем `3`. Ошибки нет, просто неверное значение.
|
||||
|
||||
А вот если переопределённый метод обращается к объекту, результат - `NullPointerException` в коде, где ни одного nullable-типа не видно:
|
||||
```kotlin
|
||||
open class Base7b {
|
||||
init { println("размер: ${size()}") }
|
||||
open fun size() = 0
|
||||
}
|
||||
|
||||
class Derived7b : Base7b() {
|
||||
private val items = listOf(1, 2, 3)
|
||||
override fun size() = items.size // items ещё null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
try {
|
||||
Derived7b()
|
||||
} catch (e: NullPointerException) {
|
||||
println("NPE: ${e.message}")
|
||||
// NPE: Cannot invoke "java.util.List.size()" because "this.items" is null
|
||||
}
|
||||
}
|
||||
```
|
||||
Та же проблема существует в Java; разница в том, что Kotlin позволяет её избежать, закрывая классы по умолчанию.
|
||||
|
||||
Как не попадать:
|
||||
- не вызывать `open`-члены из конструкторов и `init`
|
||||
- делать такие методы `private` или `final`
|
||||
- если поведение подкласса нужно при создании - передавать его как параметр конструктора
|
||||
|
||||
```kotlin
|
||||
open class Base8(private val label: String) {
|
||||
init { println("метка: $label") } // параметр уже готов
|
||||
}
|
||||
|
||||
class Derived8 : Base8("Derived") {
|
||||
val name = "Иван"
|
||||
}
|
||||
|
||||
fun main() { Derived8() } // метка: Derived
|
||||
```
|
||||
|
||||
## Порядок объявления имеет значение
|
||||
Свойство нельзя использовать до его объявления:
|
||||
```kotlin
|
||||
class Bad3 {
|
||||
// val b = a + 1 // ошибка: variable 'a' must be initialized
|
||||
val a = 1
|
||||
val b = a + 1
|
||||
}
|
||||
|
||||
fun main() { println(Bad3().b) } // 2
|
||||
```
|
||||
Компилятор ловит это для прямых обращений. Но через метод проверка обходится:
|
||||
```kotlin
|
||||
class Sneaky {
|
||||
val b = compute() // вызывается до инициализации a
|
||||
val a = 10
|
||||
private fun compute() = a * 2
|
||||
|
||||
override fun toString() = "a=$a b=$b"
|
||||
}
|
||||
|
||||
fun main() { println(Sneaky()) } // a=10 b=0
|
||||
```
|
||||
`b` равно `0`, потому что `a` на момент вызова ещё не присвоено. Ошибки компиляции нет - компилятор не отслеживает порядок через вызовы методов.
|
||||
|
||||
## Ленивая инициализация как решение
|
||||
Если порядок становится проблемой, `by lazy` снимает её: значение вычисляется при первом обращении, когда объект уже собран.
|
||||
```kotlin
|
||||
class Fixed {
|
||||
val b: Int by lazy { a * 2 } // вычислится после создания
|
||||
val a = 10
|
||||
|
||||
override fun toString() = "a=$a b=$b"
|
||||
}
|
||||
|
||||
fun main() { println(Fixed()) } // a=10 b=20
|
||||
```
|
||||
Подробнее: [Свойства](Свойства.md)
|
||||
|
||||
## Исключение в инициализации
|
||||
```kotlin
|
||||
class Validated(val age: Int) {
|
||||
init {
|
||||
require(age >= 0) { "возраст отрицательный: $age" }
|
||||
println("объект создан")
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
try {
|
||||
Validated(-1)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println("не создан: ${e.message}") // не создан: возраст отрицательный: -1
|
||||
}
|
||||
println(Validated(20).age) // объект создан, 20
|
||||
}
|
||||
```
|
||||
Ссылка на объект не вернётся, но побочные эффекты, выполненные до исключения, останутся. Поэтому в `init` держат только проверки и присваивания - без регистрации в реестрах, открытия файлов и запуска потоков.
|
||||
|
||||
## Инициализация object и companion object
|
||||
```kotlin
|
||||
object Registry2 {
|
||||
init { println("инициализация Registry") }
|
||||
val items = mutableListOf<String>()
|
||||
}
|
||||
|
||||
class Service9 {
|
||||
companion object {
|
||||
init { println("инициализация companion") }
|
||||
val created = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println("старт")
|
||||
println(Registry2.items.size) // инициализация Registry, затем 0
|
||||
println(Service9.created) // инициализация companion, затем 0
|
||||
}
|
||||
```
|
||||
Оба инициализируются лениво - при первом обращении, потокобезопасно средствами JVM. `companion object` инициализируется при первом обращении к нему или при первом создании экземпляра класса.
|
||||
|
||||
Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | блок `{ }` в теле класса | `init { }` |
|
||||
> | `static { }` | `init` в `companion object` |
|
||||
> | присваивание в конструкторе | инициализатор свойства |
|
||||
> | `super(args)` первой строкой | `: Base(args)` в заголовке |
|
||||
> | нет проверки порядка | ошибка при прямом обращении вперёд |
|
||||
>
|
||||
> **Порядок инициализации одинаков**: базовый класс, потом подкласс; внутри класса - по порядку объявления. Проблема вызова переопределяемого метода из конструктора тоже одинакова.
|
||||
>
|
||||
> **Kotlin частично защищает от неинициализированных свойств.** Прямое обращение к свойству до его объявления - ошибка компиляции, тогда как Java молча даёт значение по умолчанию:
|
||||
> ```java
|
||||
> class Sneaky {
|
||||
> int b = a + 1; // компилируется, b = 1, потому что a == 0
|
||||
> int a = 10;
|
||||
> }
|
||||
> ```
|
||||
> Через вызов метода обход возможен в обоих языках.
|
||||
>
|
||||
> **Главная защита - закрытые классы.** Ловушка с `open`-методом в конструкторе требует, чтобы класс был наследуемым. Поскольку в Kotlin класс закрыт по умолчанию, большинство классов в эту ловушку попасть не может в принципе.
|
||||
>
|
||||
> Сравнить: [Блоки инициализации в Java](../../Программирование%20на%20Java/ООП/Блоки%20инициализации.md), [Конструкторы](../../Программирование%20на%20Java/ООП/Конструкторы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Вызов `open`-метода из `init` видит неинициализированный подкласс** - значение будет `null` даже у non-null свойства:
|
||||
> ```kotlin
|
||||
> open class Base9 {
|
||||
> init { check() }
|
||||
> open fun check() {}
|
||||
> }
|
||||
>
|
||||
> class Derived9 : Base9() {
|
||||
> private val name: String = "Иван"
|
||||
> override fun check() {
|
||||
> @Suppress("SENSELESS_COMPARISON")
|
||||
> println("name инициализирован: " + (name != null))
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { Derived9() } // name инициализирован: false
|
||||
> ```
|
||||
> Это единственный штатный способ увидеть `null` в переменной типа `String`.
|
||||
>
|
||||
> **Порядок объявления свойств меняет поведение**, если между ними есть зависимость через методы. Перестановка двух строк может дать `0` вместо правильного значения - без ошибки компиляции.
|
||||
>
|
||||
> **Тело вторичного конструктора выполняется последним**, уже после всех `init`. Код, который ожидает обратного порядка, получит неожиданный результат.
|
||||
>
|
||||
> **`lateinit` не инициализируется автоматически** - обращение до присваивания даёт `UninitializedPropertyAccessException`.
|
||||
>
|
||||
> **Круговая зависимость между `object` даёт `null` или `0` без ошибки:**
|
||||
> ```kotlin
|
||||
> object First { val value: String = Second.value + "-first" }
|
||||
> object Second { val value: String = "second" }
|
||||
>
|
||||
> fun main() { println(First.value) } // second-first
|
||||
> ```
|
||||
> Если поменять зависимость местами, получится `null-second`: один из объектов будет читать поле другого во время его инициализации.
|
||||
>
|
||||
> **Исключение в `init` оставляет побочные эффекты.** Частая ошибка - регистрация `this` в глобальном реестре до валидации: объект создан не будет, а ссылка на него останется.
|
||||
>
|
||||
> **Утечка `this` из конструктора.** Передача `this` куда-либо в `init` открывает доступ к недосозданному объекту:
|
||||
> ```kotlin
|
||||
> class Leaky {
|
||||
> val items = mutableListOf<Leaky>()
|
||||
> init { register(this) } // объект ещё не готов
|
||||
> val name: String = "готов"
|
||||
>
|
||||
> companion object {
|
||||
> val all = mutableListOf<Leaky>()
|
||||
> fun register(l: Leaky) = all.add(l)
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> Leaky()
|
||||
> println("в реестре: ${Leaky.all.size}") // в реестре: 1
|
||||
> }
|
||||
> ```
|
||||
> Другой поток, получивший такую ссылку, увидит `name` как `null`.
|
||||
|
||||
Дополнительно: [Классы и конструкторы](Классы%20и%20конструкторы.md)
|
||||
Далее: [Модификаторы видимости](Модификаторы%20видимости.md)
|
||||
@@ -1,332 +0,0 @@
|
||||
---
|
||||
title: Интерфейсы
|
||||
description: Реализация по умолчанию, свойства без состояния, ромбовидное наследование и fun interface
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Интерфейс описывает контракт. В Kotlin он может содержать и реализацию.
|
||||
|
||||
```kotlin
|
||||
interface Drawable2 {
|
||||
fun draw(): String // только объявление
|
||||
fun show() = "рисую: ${draw()}" // реализация по умолчанию
|
||||
}
|
||||
|
||||
class Circle4 : Drawable2 {
|
||||
override fun draw() = "круг"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Circle4().show()) // рисую: круг
|
||||
}
|
||||
```
|
||||
Скобок после имени интерфейса в списке супертипов нет - конструктора у него не бывает.
|
||||
|
||||
## Свойства в интерфейсе
|
||||
Свойство объявить можно, но **хранить значение - нет**:
|
||||
```kotlin
|
||||
interface Named2 {
|
||||
val name: String // абстрактное
|
||||
val upper: String get() = name.uppercase() // через геттер — можно
|
||||
// val cached: String = "x" // ошибка: поля в интерфейсе запрещены
|
||||
}
|
||||
|
||||
class User5(override val name: String) : Named2
|
||||
|
||||
class Admin2 : Named2 {
|
||||
override val name get() = "админ" // геттер вместо поля
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(User5("иван").upper) // ИВАН
|
||||
println(Admin2().name) // админ
|
||||
}
|
||||
```
|
||||
Реализовать свойство можно параметром конструктора, хранимым свойством или геттером - интерфейсу это безразлично.
|
||||
|
||||
## Множественная реализация
|
||||
```kotlin
|
||||
interface Readable2 { fun read(): String }
|
||||
interface Writable { fun write(data: String): String }
|
||||
interface Closeable2 { fun close() = "закрыто" }
|
||||
|
||||
class File2(private val name: String) : Readable2, Writable, Closeable2 {
|
||||
override fun read() = "чтение $name"
|
||||
override fun write(data: String) = "запись '$data' в $name"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val f = File2("a.txt")
|
||||
println(f.read()) // чтение a.txt
|
||||
println(f.write("текст")) // запись 'текст' в a.txt
|
||||
println(f.close()) // закрыто
|
||||
}
|
||||
```
|
||||
Ограничений на число интерфейсов нет - в отличие от классов.
|
||||
|
||||
## Ромбовидное наследование
|
||||
Если два интерфейса дают реализацию одного метода, компилятор требует разрешить конфликт:
|
||||
```kotlin
|
||||
interface A7 { fun f() = "A" }
|
||||
interface B7 { fun f() = "B" }
|
||||
|
||||
class C7 : A7, B7 {
|
||||
override fun f() = "${super<A7>.f()}${super<B7>.f()}"
|
||||
}
|
||||
|
||||
fun main() { println(C7().f()) } // AB
|
||||
```
|
||||
Без `override` класс не скомпилируется: `class C7 : A7, B7` даст ошибку `inherits multiple interface methods`.
|
||||
|
||||
Требование строгое: `override` нужен, даже если реализацию даёт только **один** из интерфейсов:
|
||||
```kotlin
|
||||
interface A8 { fun f(): String } // абстрактный
|
||||
interface B8 { fun f() = "B" } // с реализацией
|
||||
|
||||
// class C8 : A8, B8 // ошибка: inherits multiple interface methods
|
||||
class C8 : A8, B8 {
|
||||
override fun f() = super<B8>.f() // выбираем явно
|
||||
}
|
||||
|
||||
fun main() { println(C8().f()) } // B
|
||||
```
|
||||
А вот реализация из **класса** абстрактный метод интерфейса покрывает - здесь писать `override` не нужно:
|
||||
```kotlin
|
||||
interface Named3 { fun name(): String }
|
||||
|
||||
abstract class Base3 : Named3 {
|
||||
override fun name() = "из класса"
|
||||
}
|
||||
|
||||
class Impl10 : Base3(), Named3 // реализация унаследована от Base3
|
||||
|
||||
fun main() { println(Impl10().name()) } // из класса
|
||||
```
|
||||
|
||||
Для свойств правило то же:
|
||||
```kotlin
|
||||
interface A9 { val name: String get() = "A" }
|
||||
interface B9 { val name: String get() = "B" }
|
||||
|
||||
class C9 : A9, B9 {
|
||||
override val name get() = super<A9>.name + super<B9>.name
|
||||
}
|
||||
|
||||
fun main() { println(C9().name) } // AB
|
||||
```
|
||||
|
||||
## Наследование интерфейсов
|
||||
```kotlin
|
||||
interface Entity2 { val id: Int }
|
||||
|
||||
interface Timestamped : Entity2 {
|
||||
val createdAt: Long
|
||||
fun age(now: Long) = now - createdAt
|
||||
}
|
||||
|
||||
class Record2(override val id: Int, override val createdAt: Long) : Timestamped
|
||||
|
||||
fun main() {
|
||||
val r = Record2(1, 1000)
|
||||
println("${r.id} ${r.age(1500)}") // 1 500
|
||||
}
|
||||
```
|
||||
Интерфейс может наследовать несколько интерфейсов - ограничения на один супертип здесь нет.
|
||||
|
||||
## private-члены
|
||||
```kotlin
|
||||
interface Validator2 {
|
||||
fun validate(value: String): Boolean
|
||||
|
||||
private fun isBlank2(value: String) = value.trim().isEmpty()
|
||||
|
||||
fun check(value: String) = when {
|
||||
isBlank2(value) -> "пусто"
|
||||
validate(value) -> "ок"
|
||||
else -> "неверно"
|
||||
}
|
||||
}
|
||||
|
||||
class EmailValidator : Validator2 {
|
||||
override fun validate(value: String) = "@" in value
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val v = EmailValidator()
|
||||
println(v.check(" ")) // пусто
|
||||
println(v.check("a@b.ru")) // ок
|
||||
println(v.check("нет")) // неверно
|
||||
}
|
||||
```
|
||||
`private` в интерфейсе - способ вынести вспомогательную логику, не включая её в контракт. `protected` в интерфейсе **запрещён**: все члены либо публичны, либо приватны.
|
||||
|
||||
## fun interface - SAM
|
||||
Интерфейс с единственным абстрактным методом можно пометить `fun`, и тогда вместо объекта допустима лямбда:
|
||||
```kotlin
|
||||
fun interface Transformer {
|
||||
fun transform(input: String): String
|
||||
}
|
||||
|
||||
fun apply3(value: String, t: Transformer) = t.transform(value)
|
||||
|
||||
fun main() {
|
||||
println(apply3("abc", Transformer { it.uppercase() })) // ABC
|
||||
println(apply3("abc") { it.reversed() }) // cba
|
||||
|
||||
val obj = object : Transformer { // можно и так
|
||||
override fun transform(input: String) = "[$input]"
|
||||
}
|
||||
println(apply3("abc", obj)) // [abc]
|
||||
}
|
||||
```
|
||||
Для своего кода обычно лучше функциональный тип `(String) -> String` - он не требует объявления интерфейса. `fun interface` нужен, когда:
|
||||
- интерфейсу нужны дополнительные методы с реализацией
|
||||
- API должен быть удобным из Java
|
||||
- имя контракта важно для читаемости
|
||||
|
||||
```kotlin
|
||||
fun interface Predicate2<T> {
|
||||
fun test(value: T): Boolean
|
||||
fun negate(): Predicate2<T> = Predicate2 { !test(it) } // метод по умолчанию
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val positive = Predicate2<Int> { it > 0 }
|
||||
println(positive.test(5)) // true
|
||||
println(positive.negate().test(5)) // false
|
||||
}
|
||||
```
|
||||
|
||||
## Интерфейсы в делегировании
|
||||
```kotlin
|
||||
interface Logger4 { fun log(msg: String): String }
|
||||
|
||||
class ConsoleLogger2 : Logger4 {
|
||||
override fun log(msg: String) = "[LOG] $msg"
|
||||
}
|
||||
|
||||
class Service14(logger: Logger4) : Logger4 by logger {
|
||||
fun work() = log("работаю")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service14(ConsoleLogger2()).work()) // [LOG] работаю
|
||||
}
|
||||
```
|
||||
Подробнее: [Делегирование](Делегирование.md)
|
||||
|
||||
## Интерфейс как граница модуля
|
||||
```kotlin
|
||||
interface PaymentGateway {
|
||||
fun charge(amount: Long): String
|
||||
}
|
||||
|
||||
internal class StripeGateway : PaymentGateway {
|
||||
override fun charge(amount: Long) = "Stripe: списано $amount"
|
||||
}
|
||||
|
||||
internal class TestGateway : PaymentGateway {
|
||||
override fun charge(amount: Long) = "Тест: якобы списано $amount"
|
||||
}
|
||||
|
||||
fun createGateway(test: Boolean): PaymentGateway =
|
||||
if (test) TestGateway() else StripeGateway()
|
||||
|
||||
fun main() {
|
||||
println(createGateway(test = true).charge(100)) // Тест: якобы списано 100
|
||||
println(createGateway(test = false).charge(100)) // Stripe: списано 100
|
||||
}
|
||||
```
|
||||
Это основное практическое применение: клиент зависит от интерфейса, реализации скрыты за `internal` и подменяемы. Подробнее: [Модификаторы видимости](Модификаторы%20видимости.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `interface A { }` | то же |
|
||||
> | `default void f() { }` | `fun f() { }` - слово не нужно |
|
||||
> | `static` методы в интерфейсе | `companion object` в интерфейсе |
|
||||
> | `private` методы (Java 9+) | то же |
|
||||
> | поля `public static final` | только `const` в `companion object` |
|
||||
> | `A.super.f()` | `super<A>.f()` |
|
||||
> | `@FunctionalInterface` | `fun interface` |
|
||||
> | нет свойств | `val`/`var` в интерфейсе |
|
||||
>
|
||||
> **Реализация по умолчанию не требует ключевого слова.** В Java нужно `default`, потому что до версии 8 интерфейсы реализации не имели и слово понадобилось для совместимости. В Kotlin такого ограничения не было.
|
||||
>
|
||||
> **Свойства в интерфейсе** - то, чего в Java нет. Java-интерфейс объявляет геттер как метод, и реализация обязана быть методом:
|
||||
> ```java
|
||||
> interface Named { String getName(); }
|
||||
> ```
|
||||
> Kotlin позволяет реализовать `val name: String` параметром конструктора - короче и без ручного геттера.
|
||||
>
|
||||
> **Константы в интерфейсе работают иначе.** В Java поле интерфейса неявно `public static final`, что породило антипаттерн «интерфейс-константа». В Kotlin поля в интерфейсе запрещены; константы живут в `companion object`:
|
||||
> ```kotlin
|
||||
> interface Config6 {
|
||||
> companion object { const val MAX = 100 }
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Config6.MAX) } // 100
|
||||
> ```
|
||||
>
|
||||
> **Реализация по умолчанию и совместимость с Java.** По умолчанию Kotlin компилирует её в статический метод плюс `DefaultImpls`-класс, а не в Java `default`-метод. Java-классу, реализующему такой интерфейс, придётся реализовать метод самому. Флаг `-Xjvm-default=all` меняет это поведение. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> Сравнить: [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Свойство в интерфейсе не хранит значение.** Попытка задать значение - ошибка:
|
||||
> ```kotlin
|
||||
> interface Counter2 {
|
||||
> // var count: Int = 0 // ошибка: property initializers are not allowed
|
||||
> var count: Int // только объявление
|
||||
> }
|
||||
>
|
||||
> class Impl8 : Counter2 { override var count = 0 }
|
||||
>
|
||||
> fun main() {
|
||||
> val c = Impl8()
|
||||
> c.count++
|
||||
> println(c.count) // 1
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Геттер в интерфейсе вычисляется у каждой реализации заново** - это не кеш:
|
||||
> ```kotlin
|
||||
> interface Sized {
|
||||
> val items: List<Int>
|
||||
> val total: Int get() = items.sum().also { println("считаю") }
|
||||
> }
|
||||
>
|
||||
> class Impl9 : Sized { override val items = listOf(1, 2) }
|
||||
>
|
||||
> fun main() {
|
||||
> val i = Impl9()
|
||||
> println(i.total)
|
||||
> println(i.total) // "считаю" дважды
|
||||
> }
|
||||
> ```
|
||||
> Кешировать можно только в классе-реализации через `by lazy`.
|
||||
>
|
||||
> **Ромбовидный конфликт не решается автоматически** - нужен явный `override` с `super<T>`. Это строгость, а не недостаток: язык не выбирает реализацию за вас.
|
||||
>
|
||||
> **`protected` в интерфейсе запрещён.** Если нужна защищённая логика для наследников, нужен абстрактный класс. Подробнее: [Абстрактные классы](Абстрактные%20классы.md)
|
||||
>
|
||||
> **Добавление метода в интерфейс - ломающее изменение** для всех реализаций, если у него нет реализации по умолчанию. Для публичного API это означает, что интерфейс стоит проектировать узким.
|
||||
>
|
||||
> **`fun interface` позволяет только один абстрактный метод.** Добавив второй, вы сломаете все вызовы с лямбдой:
|
||||
> ```kotlin
|
||||
> fun interface Handler2 { fun handle(x: Int): String }
|
||||
>
|
||||
> fun main() {
|
||||
> val h = Handler2 { "получено $it" }
|
||||
> println(h.handle(1)) // получено 1
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Интерфейс с реализацией по умолчанию - уже не чистый контракт.** Изменение такой реализации меняет поведение всех классов, которые её не переопределили, - то есть это наследование со всеми его рисками.
|
||||
>
|
||||
> **Из Java реализация по умолчанию не видна** без `-Xjvm-default=all`. Java-код будет требовать реализовать метод, который в Kotlin считается необязательным.
|
||||
|
||||
Дополнительно: [Абстрактные классы](Абстрактные%20классы.md)
|
||||
Далее: [Делегирование](Делегирование.md)
|
||||
@@ -1,326 +0,0 @@
|
||||
---
|
||||
title: Классы и конструкторы
|
||||
description: Основной конструктор, параметры-свойства, вторичные конструкторы и фабрики
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Минимальный класс - одна строка:
|
||||
```kotlin
|
||||
class Empty
|
||||
|
||||
fun main() {
|
||||
val e = Empty() // new не нужен
|
||||
println(e::class.simpleName) // Empty
|
||||
}
|
||||
```
|
||||
Оператора `new` в Kotlin нет: конструктор вызывается как функция.
|
||||
|
||||
## Основной конструктор
|
||||
Объявляется в заголовке класса:
|
||||
```kotlin
|
||||
class User(name: String, age: Int) {
|
||||
val displayName = name.uppercase() // параметры доступны в теле
|
||||
val isAdult = age >= 18
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User("иван", 20)
|
||||
println("${u.displayName} ${u.isAdult}") // ИВАН true
|
||||
}
|
||||
```
|
||||
Параметры `name` и `age` - **только параметры**: они видны в инициализаторах свойств и в блоках `init`, но не становятся членами класса.
|
||||
|
||||
```kotlin
|
||||
class User(name: String) {
|
||||
val displayName = name.uppercase()
|
||||
// fun show() = name // ошибка: name недоступен в методах
|
||||
}
|
||||
|
||||
fun main() { println(User("иван").displayName) } // ИВАН
|
||||
```
|
||||
|
||||
## Параметры-свойства
|
||||
Добавьте `val` или `var` - параметр станет свойством:
|
||||
```kotlin
|
||||
class User(val name: String, var age: Int) {
|
||||
fun show() = "$name, $age" // теперь доступны везде
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User("Иван", 20)
|
||||
u.age = 21
|
||||
println(u.show()) // Иван, 21
|
||||
}
|
||||
```
|
||||
Это самая частая форма: одна строка вместо поля, конструктора, геттера и сеттера.
|
||||
|
||||
Можно смешивать: часть параметров сделать свойствами, часть оставить аргументами:
|
||||
```kotlin
|
||||
class User(val name: String, password: String) {
|
||||
private val hash = password.hashCode() // пароль не хранится как есть
|
||||
|
||||
fun check(input: String) = input.hashCode() == hash
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User("Иван", "secret")
|
||||
println(u.check("secret")) // true
|
||||
println(u.check("wrong")) // false
|
||||
}
|
||||
```
|
||||
|
||||
## Значения по умолчанию
|
||||
```kotlin
|
||||
class Server(
|
||||
val host: String = "localhost",
|
||||
val port: Int = 8080,
|
||||
val secure: Boolean = false,
|
||||
)
|
||||
|
||||
fun main() {
|
||||
println(Server().port) // 8080
|
||||
println(Server("example.com", 443, true).host) // example.com
|
||||
println(Server(port = 9090).port) // 9090 — именованный аргумент
|
||||
}
|
||||
```
|
||||
Это заменяет телескопические конструкторы и во многих случаях - паттерн Builder. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
|
||||
|
||||
Висячая запятая после последнего параметра разрешена - упрощает правки и diff.
|
||||
|
||||
## Модификаторы и аннотации
|
||||
Если у конструктора есть модификатор видимости или аннотация, слово `constructor` обязательно:
|
||||
```kotlin
|
||||
class Service private constructor(val id: Int) {
|
||||
companion object {
|
||||
fun create(id: Int) = if (id > 0) Service(id) else null
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service.create(1)?.id) // 1
|
||||
println(Service.create(-1)) // null
|
||||
// Service(1) // ошибка: конструктор приватный
|
||||
}
|
||||
```
|
||||
Приватный конструктор + фабрика в компаньоне - способ контролировать создание: валидация, кеширование, возврат `null` вместо исключения.
|
||||
|
||||
## Вторичные конструкторы
|
||||
Объявляются в теле класса словом `constructor`:
|
||||
```kotlin
|
||||
class Point(val x: Int, val y: Int) {
|
||||
constructor(value: Int) : this(value, value) // точка на диагонали
|
||||
constructor() : this(0, 0)
|
||||
|
||||
override fun toString() = "($x, $y)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Point(1, 2)) // (1, 2)
|
||||
println(Point(5)) // (5, 5)
|
||||
println(Point()) // (0, 0)
|
||||
}
|
||||
```
|
||||
Вторичный конструктор **обязан** делегировать основному через `this(...)` - напрямую инициализировать свойства он не может.
|
||||
|
||||
Класс без основного конструктора может иметь только вторичные:
|
||||
```kotlin
|
||||
class Config4 {
|
||||
val source: String
|
||||
|
||||
constructor(file: java.io.File) {
|
||||
source = "файл ${file.name}"
|
||||
}
|
||||
|
||||
constructor(url: String, timeout: Int) {
|
||||
source = "$url (таймаут $timeout)"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config4("http://x.ru", 30).source) // http://x.ru (таймаут 30)
|
||||
}
|
||||
```
|
||||
Здесь делегировать некому, поэтому свойство инициализируется в каждом конструкторе. Форма редкая - обычно хватает основного конструктора со значениями по умолчанию.
|
||||
|
||||
### Когда нужны вторичные
|
||||
Почти никогда. Значения по умолчанию покрывают большинство случаев:
|
||||
```kotlin
|
||||
// вместо трёх конструкторов
|
||||
class Point(val x: Int = 0, val y: Int = x)
|
||||
|
||||
fun main() {
|
||||
println("${Point().x} ${Point(5).y} ${Point(1, 2).y}") // 0 5 2
|
||||
}
|
||||
```
|
||||
Вторичный конструктор оправдан, когда нужно **преобразовать** аргументы другого типа или выполнить работу перед делегированием:
|
||||
```kotlin
|
||||
class Duration2(val millis: Long) {
|
||||
constructor(seconds: Int) : this(seconds * 1000L)
|
||||
constructor(text: String) : this(text.removeSuffix("s").toLong() * 1000)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Duration2(2).millis) // 2000
|
||||
println(Duration2("3s").millis) // 3000
|
||||
}
|
||||
```
|
||||
Но и здесь фабричные функции в компаньоне обычно читаются лучше - у них есть имена:
|
||||
```kotlin
|
||||
class Duration3 private constructor(val millis: Long) {
|
||||
companion object {
|
||||
fun ofSeconds(s: Int) = Duration3(s * 1000L)
|
||||
fun parse(text: String) = Duration3(text.removeSuffix("s").toLong() * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Duration3.ofSeconds(2).millis) // 2000
|
||||
println(Duration3.parse("3s").millis) // 3000
|
||||
}
|
||||
```
|
||||
Два конструктора с одинаковыми типами параметров объявить нельзя, а фабрик с разными именами - сколько угодно.
|
||||
|
||||
## Валидация в конструкторе
|
||||
```kotlin
|
||||
class Person(val name: String, val age: Int) {
|
||||
init {
|
||||
require(name.isNotBlank()) { "имя не может быть пустым" }
|
||||
require(age in 0..150) { "недопустимый возраст: $age" }
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Person("Иван", 20).name) // Иван
|
||||
try {
|
||||
Person("", 20)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // имя не может быть пустым
|
||||
}
|
||||
}
|
||||
```
|
||||
Блок `init` выполняется при создании объекта. Подробнее о порядке: [Инициализация](Инициализация.md)
|
||||
|
||||
## Создание объекта
|
||||
```kotlin
|
||||
class User(val name: String)
|
||||
|
||||
fun main() {
|
||||
val u = User("Иван") // просто вызов
|
||||
val list = List(3) { User("u$it") } // в лямбде
|
||||
val byRef = ::User // ссылка на конструктор
|
||||
println("${u.name} ${list.size} ${byRef("Пётр").name}") // Иван 3 Пётр
|
||||
}
|
||||
```
|
||||
`::User` - ссылка на конструктор, её можно передать как функцию. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `new User("Иван")` | `User("Иван")` |
|
||||
> | конструктор = метод с телом | основной конструктор в заголовке |
|
||||
> | поле + присваивание в конструкторе | `val` в параметрах конструктора |
|
||||
> | телескопические конструкторы | значения по умолчанию |
|
||||
> | Builder для многих параметров | именованные аргументы |
|
||||
> | `this(...)` для делегирования | то же |
|
||||
> | `private` конструктор + `static` фабрика | `private constructor` + `companion object` |
|
||||
>
|
||||
> **Главная экономия - параметры-свойства.** В Java класс из четырёх полей занимает 15+ строк:
|
||||
> ```java
|
||||
> public class User {
|
||||
> private final String name;
|
||||
> private final int age;
|
||||
> public User(String name, int age) {
|
||||
> this.name = name;
|
||||
> this.age = age;
|
||||
> }
|
||||
> public String getName() { return name; }
|
||||
> public int getAge() { return age; }
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin это `class User(val name: String, val age: Int)`.
|
||||
>
|
||||
> **Builder обычно не нужен.** В Java паттерн Builder решает проблему конструктора с десятком опциональных параметров. Именованные аргументы и значения по умолчанию дают то же самое без дополнительного класса:
|
||||
> ```kotlin
|
||||
> class Request(
|
||||
> val url: String,
|
||||
> val method: String = "GET",
|
||||
> val timeout: Int = 30,
|
||||
> val retries: Int = 0,
|
||||
> )
|
||||
>
|
||||
> fun main() {
|
||||
> println(Request("http://x.ru", timeout = 60).timeout) // 60
|
||||
> }
|
||||
> ```
|
||||
> Builder остаётся полезен, когда объект собирается по частям в разных местах кода.
|
||||
>
|
||||
> Сравнить: [Конструкторы в Java](../../Программирование%20на%20Java/ООП/Конструкторы.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Параметр без `val`/`var` недоступен в методах** - только в инициализаторах и `init`. Ошибка «unresolved reference» в методе обычно означает забытый `val`.
|
||||
>
|
||||
> **`val` в конструкторе делает свойство частью публичного API.** Если значение нужно только внутри, пишите `private val`:
|
||||
> ```kotlin
|
||||
> class Service4(private val apiKey: String) {
|
||||
> fun call() = "запрос с ключом длины ${apiKey.length}"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s = Service4("secret")
|
||||
> println(s.call()) // запрос с ключом длины 6
|
||||
> // println(s.apiKey) // ошибка: приватное свойство
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Значения по умолчанию не видны из Java.** Без `@JvmOverloads` Java-код обязан передать все аргументы:
|
||||
> ```kotlin
|
||||
> class Server2 @JvmOverloads constructor(
|
||||
> val host: String = "localhost",
|
||||
> val port: Int = 8080,
|
||||
> )
|
||||
>
|
||||
> fun main() { println(Server2().port) } // 8080
|
||||
> ```
|
||||
>
|
||||
> **Вторичный конструктор не может объявлять свойства:**
|
||||
> ```kotlin
|
||||
> class Point2(val x: Int) {
|
||||
> // constructor(val y: Int) : this(y) // ошибка: val запрещён здесь
|
||||
> constructor(y: Long) : this(y.toInt())
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Point2(5L).x) } // 5
|
||||
> ```
|
||||
>
|
||||
> **Порядок инициализации не совпадает с порядком конструкторов.** Блоки `init` и инициализаторы свойств выполняются **до** тела вторичного конструктора:
|
||||
> ```kotlin
|
||||
> class Order2 {
|
||||
> val a = "свойство".also { println("1: $it") }
|
||||
> init { println("2: init") }
|
||||
>
|
||||
> constructor() { println("3: тело вторичного") }
|
||||
> }
|
||||
>
|
||||
> fun main() { Order2() }
|
||||
> ```
|
||||
> Вывод: `1`, `2`, `3`. Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Два конструктора с одинаковой сигнатурой невозможны**, даже если смысл разный:
|
||||
> ```kotlin
|
||||
> class Range3 private constructor(val from: Int, val to: Int) {
|
||||
> companion object {
|
||||
> fun ofBounds(from: Int, to: Int) = Range3(from, to)
|
||||
> fun ofStart(start: Int, length: Int) = Range3(start, start + length)
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Range3.ofStart(1, 5).to) } // 6
|
||||
> ```
|
||||
> Фабричные функции решают это именами.
|
||||
>
|
||||
> **Исключение в конструкторе оставляет объект недосозданным.** Если в `init` что-то упало, ссылка на объект не вернётся - но уже выполненные побочные эффекты (запись в реестр, открытый файл) останутся. Поэтому в конструкторе делают только валидацию и присваивание.
|
||||
|
||||
Дополнительно: [Свойства](Свойства.md)
|
||||
Далее: [Свойства](Свойства.md)
|
||||
@@ -1,290 +0,0 @@
|
||||
---
|
||||
title: Модификаторы видимости
|
||||
description: public, internal, protected, private и чем internal отличается от package-private
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Четыре модификатора. По умолчанию - `public`.
|
||||
|
||||
| Модификатор | Где виден |
|
||||
|---|---|
|
||||
| `public` | везде (по умолчанию) |
|
||||
| `internal` | в пределах модуля |
|
||||
| `protected` | в классе и его подклассах |
|
||||
| `private` | в классе (или в файле, если член верхнего уровня) |
|
||||
|
||||
## На верхнем уровне
|
||||
```kotlin
|
||||
public val a = 1 // везде; слово public избыточно
|
||||
internal val b = 2 // в своём модуле
|
||||
private val c = 3 // только в этом файле
|
||||
|
||||
private fun helper() = c * 2
|
||||
|
||||
fun main() { println("$a $b ${helper()}") } // 1 2 6
|
||||
```
|
||||
Для членов верхнего уровня `private` означает «виден в файле» - аналога «виден в пакете» у Kotlin нет. `protected` на верхнем уровне применить нельзя.
|
||||
|
||||
## В классе
|
||||
```kotlin
|
||||
open class Service10 {
|
||||
val publicField = 1
|
||||
internal val internalField = 2
|
||||
protected val protectedField = 3
|
||||
private val privateField = 4
|
||||
|
||||
fun show() = "$publicField $internalField $protectedField $privateField"
|
||||
}
|
||||
|
||||
class Child2 : Service10() {
|
||||
fun access() = "$publicField $internalField $protectedField"
|
||||
// privateField недоступен
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service10()
|
||||
println(s.show()) // 1 2 3 4
|
||||
println(Child2().access()) // 1 2 3
|
||||
// println(s.protectedField) // ошибка: protected
|
||||
}
|
||||
```
|
||||
|
||||
`protected` в Kotlin **не** даёт видимости в пакете - только в классе и подклассах. Это строже, чем в Java.
|
||||
|
||||
## private в классе и вложенные классы
|
||||
```kotlin
|
||||
class Outer2 {
|
||||
private val secret = "скрыто"
|
||||
|
||||
class Nested {
|
||||
// fun bad() = secret // ошибка: нет доступа к членам Outer
|
||||
fun ok() = "вложенный класс"
|
||||
}
|
||||
|
||||
inner class Inner2 {
|
||||
fun read() = secret // inner видит внешний класс
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Outer2.Nested().ok()) // вложенный класс
|
||||
println(Outer2().Inner2().read()) // скрыто
|
||||
}
|
||||
```
|
||||
Подробнее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md)
|
||||
|
||||
## internal - видимость в модуле
|
||||
Модуль - это единица компиляции: модуль Gradle или Maven, набор файлов одной задачи `kotlinc`.
|
||||
```kotlin
|
||||
internal class Repository {
|
||||
internal fun load() = "данные"
|
||||
}
|
||||
|
||||
internal fun helper2() = Repository().load()
|
||||
|
||||
fun main() { println(helper2()) } // данные
|
||||
```
|
||||
`internal` - основной инструмент для библиотек: публичный API помечается `public`, всё остальное - `internal`, и клиент библиотеки не видит внутренностей.
|
||||
|
||||
Типичный приём - публичный интерфейс и внутренняя реализация:
|
||||
```kotlin
|
||||
interface UserRepository {
|
||||
fun find(id: Int): String?
|
||||
}
|
||||
|
||||
internal class UserRepositoryImpl : UserRepository {
|
||||
override fun find(id: Int) = if (id == 1) "Иван" else null
|
||||
}
|
||||
|
||||
fun createRepository(): UserRepository = UserRepositoryImpl()
|
||||
|
||||
fun main() {
|
||||
println(createRepository().find(1)) // Иван
|
||||
}
|
||||
```
|
||||
Снаружи модуля видны только `UserRepository` и `createRepository` - реализацию заменить можно без ломающих изменений.
|
||||
|
||||
## Видимость конструктора
|
||||
```kotlin
|
||||
class Service11 private constructor(val id: Int) {
|
||||
companion object {
|
||||
fun create(id: Int) = if (id > 0) Service11(id) else null
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service11.create(5)?.id) // 5
|
||||
println(Service11.create(0)) // null
|
||||
}
|
||||
```
|
||||
Слово `constructor` обязательно, когда у конструктора есть модификатор. Подробнее: [Классы и конструкторы](Классы%20и%20конструкторы.md)
|
||||
|
||||
## Видимость сеттера
|
||||
Геттер всегда имеет видимость самого свойства, а сеттер можно ограничить:
|
||||
```kotlin
|
||||
class Session2 {
|
||||
var token: String = ""
|
||||
private set
|
||||
|
||||
var attempts: Int = 0
|
||||
internal set
|
||||
|
||||
fun login(t: String) { token = t; attempts++ }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Session2()
|
||||
s.login("abc")
|
||||
println("${s.token} ${s.attempts}") // abc 1
|
||||
// s.token = "x" // ошибка
|
||||
}
|
||||
```
|
||||
|
||||
## Правила согласованности
|
||||
Публичный член не может раскрывать тип с меньшей видимостью:
|
||||
```kotlin
|
||||
internal class Internal2
|
||||
|
||||
class Public2 {
|
||||
// fun bad(): Internal2 = Internal2() // ошибка: public раскрывает internal
|
||||
internal fun ok(): Internal2 = Internal2()
|
||||
private fun alsoOk(): Internal2 = Internal2()
|
||||
}
|
||||
|
||||
fun main() { println(Public2().ok()::class.simpleName) } // Internal2
|
||||
```
|
||||
Компилятор проверяет это и для параметров, и для типов результата, и для супертипов.
|
||||
|
||||
## Видимость и переопределение
|
||||
Переопределённый член не может сужать видимость:
|
||||
```kotlin
|
||||
open class Base10 {
|
||||
open internal fun f() = "base"
|
||||
protected open fun g() = "g"
|
||||
}
|
||||
|
||||
class Derived10 : Base10() {
|
||||
override fun f() = "derived" // internal сохранена
|
||||
public override fun g() = "расширена" // protected → public можно
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Derived10().f()) // derived
|
||||
println(Derived10().g()) // расширена — теперь доступен снаружи
|
||||
}
|
||||
```
|
||||
Расширять видимость разрешено, сужать - нет: это нарушило бы контракт базового типа.
|
||||
|
||||
`protected`-член можно сделать `public` в подклассе - приём для постепенного раскрытия API.
|
||||
|
||||
## Что видно из Java
|
||||
Это важное практическое различие:
|
||||
|
||||
| Kotlin | В байт-коде | Доступно из Java |
|
||||
|---|---|---|
|
||||
| `public` | `public` | да |
|
||||
| `internal` | `public` + искажённое имя | **да** |
|
||||
| `protected` | `protected` | да, в подклассе и в пакете |
|
||||
| `private` (член класса) | `private` | нет |
|
||||
| `private` (верхний уровень) | `private` в файловом классе | нет |
|
||||
|
||||
```kotlin
|
||||
class Service12 {
|
||||
internal fun internalMethod() = "в байт-коде это public"
|
||||
private fun privateMethod() = "действительно private"
|
||||
|
||||
fun show() = "${internalMethod()} / ${privateMethod()}"
|
||||
}
|
||||
|
||||
fun main() { println(Service12().show()) }
|
||||
```
|
||||
`internal` существует только на уровне компилятора Kotlin. В байт-коде член становится `public`, а к его имени добавляется суффикс с именем модуля - чтобы случайный вызов из Java выглядел странно, но он возможен. Полагаться на `internal` как на защиту от Java-кода нельзя.
|
||||
|
||||
Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `public` | `public` |
|
||||
> | нет модификатора = package-private | **нет аналога** |
|
||||
> | `protected` = подклассы + пакет | `protected` = только подклассы |
|
||||
> | `private` | `private` |
|
||||
> | нет аналога | `internal` = модуль |
|
||||
> | модули JPMS (Java 9+) | `internal` работает всегда |
|
||||
>
|
||||
> **По умолчанию `public`, а не package-private.** Это ловушка при переходе: Java-разработчик не пишет модификатор, ожидая видимость в пакете, и случайно публикует API.
|
||||
>
|
||||
> **Package-private в Kotlin нет совсем.** Замена - `internal` (шире: весь модуль) или `private` на верхнем уровне (уже: один файл). Пакет в Kotlin не является границей инкапсуляции, потому что пакеты легко расширить извне - любой может объявить класс в вашем пакете.
|
||||
>
|
||||
> **`protected` строже.** В Java `protected`-член виден всему пакету, что часто неожиданно. В Kotlin - только подклассам.
|
||||
>
|
||||
> **`internal` решает задачу, для которой в Java нужны JPMS.** Модульная система Java появилась в 9 и применяется редко; `internal` работает в любом проекте с модулями Gradle и не требует настройки.
|
||||
>
|
||||
> Сравнить: [Модификаторы в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Модификаторы.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Забыли модификатор - получили публичный API.** Всё, что не помечено, видно всем и становится обязательством на поддержку.
|
||||
>
|
||||
> **`internal` не защищает от Java-кода** - в байт-коде это `public`. Единственная настоящая защита - `private` или отдельный модуль.
|
||||
>
|
||||
> **`private` на верхнем уровне - это «виден в файле», а не «в пакете»:**
|
||||
> ```kotlin
|
||||
> private val fileLocal = "виден только здесь"
|
||||
> private class Helper3 { fun work() = fileLocal }
|
||||
>
|
||||
> fun main() { println(Helper3().work()) }
|
||||
> ```
|
||||
> Другой файл того же пакета доступа не получит.
|
||||
>
|
||||
> **`private` член класса виден во всём классе, включая вложенные объявления:**
|
||||
> ```kotlin
|
||||
> class Outer3 {
|
||||
> private val secret = "скрыто"
|
||||
>
|
||||
> inner class Inner3 {
|
||||
> fun read() = secret // доступ есть
|
||||
> }
|
||||
>
|
||||
> companion object {
|
||||
> fun describe() = "компаньон не видит secret экземпляра"
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Outer3().Inner3().read()) // скрыто
|
||||
> println(Outer3.describe())
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`protected` недоступен в расширениях** - даже внутри того же класса:
|
||||
> ```kotlin
|
||||
> open class Base11 {
|
||||
> protected val value = 42
|
||||
> fun show() = value
|
||||
> }
|
||||
>
|
||||
> // fun Base11.bad() = value // ошибка: protected недоступен в расширении
|
||||
>
|
||||
> fun main() { println(Base11().show()) } // 42
|
||||
> ```
|
||||
>
|
||||
> **`private` конструктор не защищает `data class` от `copy`** в версиях до Kotlin 2.1: клиент мог обойти фабрику через `copy()`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
>
|
||||
> **Расширение видимости при переопределении легко сделать случайно.** `public override fun` у `protected`-метода публикует то, что автор базового класса прятал.
|
||||
>
|
||||
> **`internal`-классы в публичных сигнатурах - ошибка компиляции**, но `internal`-тип внутри дженерика может просочиться:
|
||||
> ```kotlin
|
||||
> internal class Impl2
|
||||
>
|
||||
> class Api2 {
|
||||
> internal val items = mutableListOf<Impl2>()
|
||||
> val count: Int get() = items.size // публично только число
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Api2().count) } // 0
|
||||
> ```
|
||||
|
||||
Дополнительно: [ООП](ООП.md)
|
||||
Далее: [Наследование](Наследование.md)
|
||||
@@ -1,307 +0,0 @@
|
||||
---
|
||||
title: Наследование
|
||||
description: open, вызов конструктора базового класса, наследование свойств и выбор в пользу композиции
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Класс закрыт для наследования, пока не объявлен `open`.
|
||||
|
||||
```kotlin
|
||||
open class Animal(val name: String) {
|
||||
open fun sound() = "..."
|
||||
fun describe() = "$name говорит ${sound()}" // final: переопределить нельзя
|
||||
}
|
||||
|
||||
class Dog(name: String) : Animal(name) {
|
||||
override fun sound() = "Гав"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Dog("Рекс").describe()) // Рекс говорит Гав
|
||||
}
|
||||
```
|
||||
`describe()` не `open`, но вызывает `sound()`, который переопределён - это и есть полиморфизм: шаблон поведения в базовом классе, детали в подклассе.
|
||||
|
||||
## Вызов конструктора базового класса
|
||||
Указывается в заголовке:
|
||||
```kotlin
|
||||
open class Base12(val id: Int)
|
||||
|
||||
class Derived12(id: Int, val name: String) : Base12(id)
|
||||
|
||||
fun main() {
|
||||
val d = Derived12(1, "Иван")
|
||||
println("${d.id} ${d.name}") // 1 Иван
|
||||
}
|
||||
```
|
||||
Аргументы можно вычислять:
|
||||
```kotlin
|
||||
open class Shape2(val area: Double)
|
||||
|
||||
class Circle2(val radius: Double) : Shape2(Math.PI * radius * radius)
|
||||
|
||||
fun main() {
|
||||
println("%.2f".format(java.util.Locale.US, Circle2(1.0).area)) // 3.14
|
||||
}
|
||||
```
|
||||
|
||||
Если у подкласса нет основного конструктора, базовый вызывается из каждого вторичного через `super`:
|
||||
```kotlin
|
||||
open class Base13(val source: String)
|
||||
|
||||
class Derived13 : Base13 {
|
||||
constructor(file: java.io.File) : super("файл ${file.name}")
|
||||
constructor(url: String, port: Int) : super("$url:$port")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Derived13("http://x.ru", 80).source) // http://x.ru:80
|
||||
}
|
||||
```
|
||||
|
||||
Скобки в заголовке обязательны, даже если конструктор без параметров:
|
||||
```kotlin
|
||||
open class Base14
|
||||
class Derived14 : Base14() // скобки = вызов конструктора
|
||||
|
||||
interface Marker2
|
||||
class Impl3 : Marker2 // у интерфейса скобок нет
|
||||
|
||||
fun main() { println(Derived14()::class.simpleName) } // Derived14
|
||||
```
|
||||
Наличие скобок различает класс и интерфейс в списке супертипов.
|
||||
|
||||
## Что можно наследовать
|
||||
```kotlin
|
||||
open class Base15 {
|
||||
open val version: Int = 1
|
||||
open var state: String = "новый"
|
||||
open fun run2() = "база"
|
||||
val fixed = "не переопределить" // не open
|
||||
}
|
||||
|
||||
class Derived15 : Base15() {
|
||||
override val version = 2
|
||||
override var state: String = "изменённый"
|
||||
override fun run2() = "подкласс"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val d: Base15 = Derived15()
|
||||
println("${d.version} ${d.state} ${d.run2()} ${d.fixed}")
|
||||
// 2 изменённый подкласс не переопределить
|
||||
}
|
||||
```
|
||||
`open` нужен каждому члену отдельно: открытый класс не делает своих членов открытыми.
|
||||
|
||||
### val можно переопределить как var
|
||||
```kotlin
|
||||
open class Base16 {
|
||||
open val value: Int = 1
|
||||
}
|
||||
|
||||
class Derived16 : Base16() {
|
||||
override var value: Int = 2 // добавили сеттер
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val d = Derived16()
|
||||
d.value = 5
|
||||
println(d.value) // 5
|
||||
}
|
||||
```
|
||||
Обратное запрещено: `var` нельзя сузить до `val` - это сломало бы код, который присваивает значение.
|
||||
|
||||
### Переопределение свойства геттером
|
||||
```kotlin
|
||||
open class Base17 {
|
||||
open val size: Int = 0
|
||||
}
|
||||
|
||||
class Derived17(private val items: List<Int>) : Base17() {
|
||||
override val size: Int get() = items.size // вычисляемое вместо хранимого
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Derived17(listOf(1, 2, 3)).size) // 3
|
||||
}
|
||||
```
|
||||
|
||||
## Свойство в конструкторе подкласса
|
||||
```kotlin
|
||||
open class Base18 {
|
||||
open val name: String = "базовое"
|
||||
}
|
||||
|
||||
class Derived18(override val name: String) : Base18()
|
||||
|
||||
fun main() {
|
||||
println(Derived18("своё").name) // своё
|
||||
}
|
||||
```
|
||||
`override` прямо в параметре конструктора - короткая и частая форма.
|
||||
|
||||
## Абстрактный член не требует open
|
||||
```kotlin
|
||||
abstract class Shape3 {
|
||||
abstract val area: Double // open подразумевается
|
||||
abstract fun describe(): String
|
||||
open fun show() = "фигура: ${describe()}"
|
||||
}
|
||||
|
||||
class Square2(val side: Double) : Shape3() {
|
||||
override val area get() = side * side
|
||||
override fun describe() = "квадрат со стороной $side"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Square2(2.0).show()) // фигура: квадрат со стороной 2.0
|
||||
}
|
||||
```
|
||||
Подробнее: [Абстрактные классы](Абстрактные%20классы.md)
|
||||
|
||||
## Одиночное наследование, множественные интерфейсы
|
||||
```kotlin
|
||||
open class Entity(val id: Int)
|
||||
interface Serializable2 { fun serialize(): String }
|
||||
interface Validatable { fun validate(): Boolean }
|
||||
|
||||
class User4(id: Int, val name: String) : Entity(id), Serializable2, Validatable {
|
||||
override fun serialize() = """{"id":$id,"name":"$name"}"""
|
||||
override fun validate() = name.isNotBlank()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User4(1, "Иван")
|
||||
println(u.serialize()) // {"id":1,"name":"Иван"}
|
||||
println(u.validate()) // true
|
||||
}
|
||||
```
|
||||
Класс в списке супертипов идёт первым - это соглашение, а не требование компилятора.
|
||||
|
||||
## Что наследовать нельзя
|
||||
```kotlin
|
||||
class Final2 // закрыт по умолчанию
|
||||
data class Point3(val x: Int) // data class не может быть open
|
||||
@JvmInline value class Id2(val value: Int) // value class тоже
|
||||
object Singleton2 // object
|
||||
enum class Status2 { OK } // enum
|
||||
sealed class Limited // только в своём модуле и пакете
|
||||
|
||||
fun main() { println("перечислены закрытые формы") }
|
||||
```
|
||||
Для `enum` есть исключение: отдельная константа может переопределить метод, образуя анонимный подкласс. Подробнее: [Enum class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md)
|
||||
|
||||
## Композиция вместо наследования
|
||||
Наследование связывает подкласс с реализацией базового класса. Изменение базового класса может сломать подкласс, даже если публичный контракт не менялся - это проблема хрупкого базового класса.
|
||||
|
||||
Классический пример - подсчёт добавленных элементов:
|
||||
```kotlin
|
||||
// плохо: наследование от реализации
|
||||
class CountingList<T> : ArrayList<T>() {
|
||||
var added = 0
|
||||
override fun add(element: T): Boolean {
|
||||
added++
|
||||
return super.add(element)
|
||||
}
|
||||
override fun addAll(elements: Collection<T>): Boolean {
|
||||
added += elements.size
|
||||
return super.addAll(elements) // внутри может вызвать add()!
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val list = CountingList<Int>()
|
||||
list.addAll(listOf(1, 2, 3))
|
||||
println("добавлено: ${list.added}, реально: ${list.size}")
|
||||
}
|
||||
```
|
||||
На текущем JDK выведется `добавлено: 3, реально: 3` - потому что `ArrayList.addAll` копирует массив напрямую и `add` не вызывает. Но это не гарантия: реализация, которая реализует `addAll` через цикл с `add`, удвоит счётчик. Корректность класса зависит от внутренностей базового, а они не входят в его контракт и могут измениться между версиями JDK.
|
||||
|
||||
Композиция такой зависимости не создаёт:
|
||||
```kotlin
|
||||
class CountingList2<T> {
|
||||
private val items = mutableListOf<T>()
|
||||
var added = 0
|
||||
private set
|
||||
|
||||
fun add(element: T) { added++; items.add(element) }
|
||||
fun addAll(elements: Collection<T>) { elements.forEach { add(it) } }
|
||||
val size: Int get() = items.size
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val list = CountingList2<Int>()
|
||||
list.addAll(listOf(1, 2, 3))
|
||||
println("добавлено: ${list.added}, реально: ${list.size}") // 3, 3
|
||||
}
|
||||
```
|
||||
Kotlin делает композицию такой же короткой, как наследование, - через делегирование `by`. Подробнее: [Делегирование](Делегирование.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `class A extends B` | `class A : B()` |
|
||||
> | `implements I` | та же запись через запятую |
|
||||
> | класс открыт, `final` закрывает | закрыт, `open` открывает |
|
||||
> | метод виртуален по умолчанию | финален по умолчанию |
|
||||
> | `super(args)` первой строкой тела | `: Base(args)` в заголовке |
|
||||
> | `@Override` опциональна | `override` обязательно |
|
||||
> | нельзя переопределить поле | свойство переопределяется |
|
||||
>
|
||||
> **Закрытость по умолчанию - осознанный выбор.** Проектировать класс под наследование дорого: нужно документировать, какие методы вызывают какие, и поддерживать это между версиями. Java по умолчанию делает всё наследуемым, из-за чего библиотечные классы либо получают `final`, либо навсегда остаются связанными своими внутренностями.
|
||||
>
|
||||
> **Переопределение свойств** в Java невозможно - поля скрываются, а не переопределяются:
|
||||
> ```java
|
||||
> class Base { int value = 1; }
|
||||
> class Derived extends Base { int value = 2; } // сокрытие, не переопределение
|
||||
> // ((Base) new Derived()).value == 1
|
||||
> ```
|
||||
> Обращение по ссылке базового типа даст поле базового класса. В Kotlin свойство - это методы доступа, поэтому `override` работает полиморфно и такой ловушки нет.
|
||||
>
|
||||
> Сравнить: [Наследование в Java](../../Программирование%20на%20Java/ООП/Наследование/Наследование.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`open` у класса не открывает его члены** - каждый метод и свойство нужно помечать отдельно.
|
||||
>
|
||||
> **Переопределённый метод остаётся `open`.** Чтобы закрыть ветку иерархии, нужен `final override`:
|
||||
> ```kotlin
|
||||
> open class A4 { open fun f() = "A" }
|
||||
> open class B4 : A4() { final override fun f() = "B" }
|
||||
> // class C4 : B4() { override fun f() = "C" } // ошибка: f закрыт
|
||||
>
|
||||
> fun main() { println(B4().f()) } // B
|
||||
> ```
|
||||
>
|
||||
> **Вызов `open`-метода из конструктора видит неинициализированный подкласс:**
|
||||
> ```kotlin
|
||||
> open class Base19 {
|
||||
> init { println("из init: ${describe()}") }
|
||||
> open fun describe() = "база"
|
||||
> }
|
||||
>
|
||||
> class Derived19 : Base19() {
|
||||
> private val name: String = "Иван"
|
||||
> override fun describe() = "подкласс, name=$name"
|
||||
> }
|
||||
>
|
||||
> fun main() { Derived19() } // из init: подкласс, name=null
|
||||
> ```
|
||||
> Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Скобки обязательны при наследовании класса**: `: Base()`, а не `: Base`. Без скобок компилятор ищет интерфейс.
|
||||
>
|
||||
> **`protected` в Kotlin не даёт доступа из пакета** - только из подклассов. Код, перенесённый из Java, может перестать компилироваться.
|
||||
>
|
||||
> **`var` нельзя переопределить как `val`** - только наоборот.
|
||||
>
|
||||
> **Наследование от классов стандартной библиотеки хрупко.** `ArrayList`, `HashMap` и подобные не документируют, какие методы вызывают друг друга внутри. Используйте делегирование.
|
||||
>
|
||||
> **Глубокие иерархии трудно читать.** Чтобы понять поведение класса в иерархии из пяти уровней, нужно просмотреть все пять. Практическое правило: наследование - для «является» и когда базовый класс спроектирован под него; во всех остальных случаях - композиция.
|
||||
>
|
||||
> **Нельзя наследоваться от `data class`**, поэтому иерархии носителей данных строят на `sealed`-интерфейсах с `data class`-реализациями.
|
||||
|
||||
Дополнительно: [Переопределение](Переопределение.md)
|
||||
Далее: [Переопределение](Переопределение.md)
|
||||
@@ -1,197 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,341 +0,0 @@
|
||||
---
|
||||
title: Переопределение
|
||||
description: override и final, вызов super, конфликт реализаций и ковариантность результата
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
`override` обязательно. Без него член не переопределяет, а конфликтует с родительским.
|
||||
|
||||
```kotlin
|
||||
open class Animal2 {
|
||||
open fun sound() = "..."
|
||||
}
|
||||
|
||||
class Cat : Animal2() {
|
||||
override fun sound() = "Мяу"
|
||||
// fun sound() = "Мяу" // ошибка: 'sound' hides member and needs 'override'
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a: Animal2 = Cat()
|
||||
println(a.sound()) // Мяу — вызов по фактическому типу
|
||||
}
|
||||
```
|
||||
|
||||
Это закрывает два класса ошибок: опечатка в имени создала бы новый метод, а добавление метода в базовый класс случайно «переопределилось» бы существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции.
|
||||
|
||||
## final override
|
||||
Переопределённый член остаётся открытым для дальнейшего переопределения. Закрыть ветку - `final`:
|
||||
```kotlin
|
||||
open class A5 { open fun f() = "A" }
|
||||
open class B5 : A5() { final override fun f() = "B" }
|
||||
// class C5 : B5() { override fun f() = "C" } // ошибка: f закрыт
|
||||
|
||||
fun main() { println(B5().f()) } // B
|
||||
```
|
||||
|
||||
## super
|
||||
```kotlin
|
||||
open class Logger3 {
|
||||
open fun log(msg: String) = "[базовый] $msg"
|
||||
}
|
||||
|
||||
class TimestampLogger : Logger3() {
|
||||
override fun log(msg: String) = super.log("[время] $msg")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(TimestampLogger().log("привет")) // [базовый] [время] привет
|
||||
}
|
||||
```
|
||||
`super` работает и для свойств - через их геттеры:
|
||||
```kotlin
|
||||
open class Base20 {
|
||||
open val items: List<Int> get() = listOf(1, 2)
|
||||
}
|
||||
|
||||
class Derived20 : Base20() {
|
||||
override val items: List<Int> get() = super.items + 3
|
||||
}
|
||||
|
||||
fun main() { println(Derived20().items) } // [1, 2, 3]
|
||||
```
|
||||
|
||||
Из вложенного класса к `super` внешнего обращаются через `super@Outer`:
|
||||
```kotlin
|
||||
open class Base21 {
|
||||
open fun name() = "база"
|
||||
}
|
||||
|
||||
class Outer4 : Base21() {
|
||||
override fun name() = "внешний"
|
||||
|
||||
inner class Inner4 {
|
||||
fun bothNames() = "${name()} / ${super@Outer4.name()}"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() { println(Outer4().Inner4().bothNames()) } // внешний / база
|
||||
```
|
||||
|
||||
## Конфликт реализаций
|
||||
Если два супертипа дают реализацию одного метода, компилятор требует разрешить конфликт явно:
|
||||
```kotlin
|
||||
interface A6 { fun f() = "A" }
|
||||
interface B6 { fun f() = "B" }
|
||||
|
||||
class C6 : A6, B6 {
|
||||
override fun f() = super<A6>.f() + super<B6>.f() // указываем источник
|
||||
}
|
||||
|
||||
fun main() { println(C6().f()) } // AB
|
||||
```
|
||||
Без `override` такой класс не скомпилируется - Kotlin не выбирает реализацию за вас.
|
||||
|
||||
То же с классом и интерфейсом:
|
||||
```kotlin
|
||||
open class Base22 { open fun f() = "класс" }
|
||||
interface Mixin { fun f() = "интерфейс" }
|
||||
|
||||
class Impl4 : Base22(), Mixin {
|
||||
override fun f() = "${super<Base22>.f()} + ${super<Mixin>.f()}"
|
||||
}
|
||||
|
||||
fun main() { println(Impl4().f()) } // класс + интерфейс
|
||||
```
|
||||
Подробнее: [Интерфейсы](Интерфейсы.md)
|
||||
|
||||
## Что можно переопределять
|
||||
| Член | Условие |
|
||||
|---|---|
|
||||
| метод | `open` или `abstract` |
|
||||
| `val` | `open` или `abstract`; можно расширить до `var` |
|
||||
| `var` | `open` или `abstract`; сузить до `val` нельзя |
|
||||
| свойство в конструкторе | `override val` в параметре |
|
||||
|
||||
```kotlin
|
||||
abstract class Base23 {
|
||||
abstract val size: Int
|
||||
abstract var label: String
|
||||
abstract fun compute(): Int
|
||||
}
|
||||
|
||||
class Impl5(override var label: String, private val items: List<Int>) : Base23() {
|
||||
override val size get() = items.size
|
||||
override fun compute() = items.sum()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val i = Impl5("данные", listOf(1, 2, 3))
|
||||
println("${i.label} ${i.size} ${i.compute()}") // данные 3 6
|
||||
}
|
||||
```
|
||||
|
||||
## Ковариантность результата
|
||||
Переопределение может сузить тип результата:
|
||||
```kotlin
|
||||
open class Animal3
|
||||
class Dog2 : Animal3()
|
||||
|
||||
open class Shelter {
|
||||
open fun adopt(): Animal3 = Animal3()
|
||||
}
|
||||
|
||||
class DogShelter : Shelter() {
|
||||
override fun adopt(): Dog2 = Dog2() // Dog2 — подтип Animal3
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s: Shelter = DogShelter()
|
||||
println(s.adopt()::class.simpleName) // Dog2
|
||||
}
|
||||
```
|
||||
Это безопасно: клиент ожидает `Animal3`, а получает `Dog2`, который им является.
|
||||
|
||||
Параметры сузить нельзя - это сломало бы контракт:
|
||||
```kotlin
|
||||
open class Base24 {
|
||||
open fun process(x: Any) = "any: $x"
|
||||
}
|
||||
|
||||
class Derived24 : Base24() {
|
||||
override fun process(x: Any) = "переопределено: $x"
|
||||
fun process(x: String) = "перегрузка: $x" // это перегрузка, не override
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val d = Derived24()
|
||||
println(d.process("текст")) // перегрузка: текст
|
||||
println((d as Base24).process("текст")) // переопределено: текст
|
||||
}
|
||||
```
|
||||
Вызов зависит от статического типа ссылки - классическая ловушка смешения перегрузки и переопределения.
|
||||
|
||||
## Nullability при переопределении
|
||||
```kotlin
|
||||
open class Base25 {
|
||||
open fun find(id: Int): String? = null
|
||||
}
|
||||
|
||||
class Derived25 : Base25() {
|
||||
override fun find(id: Int): String = "найдено $id" // сузили String? до String
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val b: Base25 = Derived25()
|
||||
println(b.find(1)) // найдено 1
|
||||
}
|
||||
```
|
||||
Результат можно сделать non-null - это сужение. А параметр, наоборот, можно расширить до nullable:
|
||||
```kotlin
|
||||
open class Base26 {
|
||||
open fun handle(x: String) = "строка"
|
||||
}
|
||||
|
||||
class Derived26 : Base26() {
|
||||
override fun handle(x: String) = "подкласс: $x"
|
||||
}
|
||||
|
||||
fun main() { println(Derived26().handle("a")) } // подкласс: a
|
||||
```
|
||||
|
||||
## Переопределение equals, hashCode, toString
|
||||
Члены `Any` открыты всегда:
|
||||
```kotlin
|
||||
class Point4(val x: Int, val y: Int) {
|
||||
override fun equals(other: Any?) = other is Point4 && other.x == x && other.y == y
|
||||
override fun hashCode() = 31 * x + y
|
||||
override fun toString() = "($x, $y)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Point4(1, 2)) // (1, 2)
|
||||
println(Point4(1, 2) == Point4(1, 2)) // true
|
||||
}
|
||||
```
|
||||
Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
|
||||
## Переопределение и расширения
|
||||
Функция-расширение **не** переопределяется - она разрешается статически, по типу ссылки:
|
||||
```kotlin
|
||||
open class Base27
|
||||
class Derived27 : Base27()
|
||||
|
||||
fun Base27.describe() = "база"
|
||||
fun Derived27.describe() = "подкласс"
|
||||
|
||||
fun main() {
|
||||
val d: Base27 = Derived27()
|
||||
println(d.describe()) // база — по типу переменной!
|
||||
println(Derived27().describe()) // подкласс
|
||||
}
|
||||
```
|
||||
Это принципиальное отличие от методов. Если нужен полиморфизм - нужен метод класса. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
Расширение не может скрыть метод с той же сигнатурой: метод всегда побеждает.
|
||||
```kotlin
|
||||
class Service13 {
|
||||
fun run2() = "метод"
|
||||
}
|
||||
|
||||
fun Service13.run2() = "расширение" // никогда не вызовется
|
||||
|
||||
fun main() { println(Service13().run2()) } // метод
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `@Override` опциональна | `override` обязательно |
|
||||
> | метод виртуален, `final` закрывает | нужен `open`, иначе финален |
|
||||
> | переопределённый метод остаётся виртуальным | то же, закрывается `final override` |
|
||||
> | `super.method()` | то же |
|
||||
> | `A.super.method()` при конфликте | `super<A>.method()` |
|
||||
> | поля скрываются | свойства переопределяются |
|
||||
> | ковариантный результат (Java 5+) | то же |
|
||||
>
|
||||
> **Обязательный `override`** - главное практическое улучшение. В Java `@Override` необязательна, поэтому опечатка в имени тихо создаёт новый метод, который никогда не вызовется.
|
||||
>
|
||||
> **Конфликт реализаций интерфейсов** решается похоже, но синтаксис другой: `A.super.f()` в Java против `super<A>.f()` в Kotlin. Требование явно разрешать конфликт одинаково строгое в обоих языках.
|
||||
>
|
||||
> **Сокрытие полей** - проблема Java, которой в Kotlin нет. Поле подкласса с тем же именем скрывает поле базового класса, и какое из них увидит код, зависит от типа ссылки:
|
||||
> ```java
|
||||
> class Base { int value = 1; }
|
||||
> class Derived extends Base { int value = 2; }
|
||||
> Base b = new Derived();
|
||||
> // b.value == 1, ((Derived) b).value == 2
|
||||
> ```
|
||||
> В Kotlin свойства - это методы доступа, поэтому `override` работает полиморфно.
|
||||
>
|
||||
> **Статические методы в Java не переопределяются, а скрываются.** В Kotlin статики нет; члены `companion object` тоже не переопределяются, но путаницы меньше, потому что компаньон не наследуется.
|
||||
>
|
||||
> Сравнить: [Переопределение в Java](../../Программирование%20на%20Java/ООП/Наследование/Переопределение.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Расширение не переопределяется** - выбор идёт по статическому типу. Это самая частая ошибка ожиданий:
|
||||
> ```kotlin
|
||||
> open class Base28
|
||||
> class Derived28 : Base28()
|
||||
>
|
||||
> fun Base28.name() = "база"
|
||||
> fun Derived28.name() = "подкласс"
|
||||
>
|
||||
> fun main() {
|
||||
> val list: List<Base28> = listOf(Derived28())
|
||||
> println(list[0].name()) // база — не "подкласс"
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Перегрузка вместо переопределения компилируется молча:**
|
||||
> ```kotlin
|
||||
> open class Base29 { open fun f(x: Any) = "any" }
|
||||
> class Derived29 : Base29() {
|
||||
> override fun f(x: Any) = "override"
|
||||
> fun f(x: Int) = "overload"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val d = Derived29()
|
||||
> println(d.f(1)) // overload — точнее по типу
|
||||
> println((d as Base29).f(1)) // override — по типу ссылки
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **`super` в лямбде внутри метода работает, а в объекте-выражении - относится к нему:**
|
||||
> ```kotlin
|
||||
> open class Base30 { open fun f() = "база" }
|
||||
>
|
||||
> class Derived30 : Base30() {
|
||||
> override fun f() = "подкласс"
|
||||
> fun test(): String {
|
||||
> val fromLambda = listOf(1).map { super.f() }.first() // база
|
||||
> return fromLambda
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Derived30().test()) } // база
|
||||
> ```
|
||||
>
|
||||
> **Переопределение `equals` без `hashCode` ломает хеш-коллекции.** Компилятор не предупреждает.
|
||||
>
|
||||
> **Сужение видимости при переопределении запрещено**, а расширение - разрешено и легко делается случайно: `public override` у `protected`-метода публикует внутреннюю деталь.
|
||||
>
|
||||
> **Делегат свойства не наследуется.** Переопределяя `val x by lazy { }`, подкласс должен задать своё хранилище - делегат базового класса не переиспользуется:
|
||||
> ```kotlin
|
||||
> open class Base31 {
|
||||
> open val value: String by lazy { "база" }
|
||||
> }
|
||||
>
|
||||
> class Derived31 : Base31() {
|
||||
> override val value: String by lazy { "подкласс" } // свой делегат
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Derived31().value) } // подкласс
|
||||
> ```
|
||||
|
||||
Дополнительно: [Полиморфизм](Полиморфизм.md)
|
||||
Далее: [Абстрактные классы](Абстрактные%20классы.md)
|
||||
@@ -1,325 +0,0 @@
|
||||
---
|
||||
title: Полиморфизм
|
||||
description: Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Полиморфизм - возможность работать с разными типами через один интерфейс. В Kotlin четыре формы:
|
||||
|
||||
| Форма | Механизм | Когда выбирается реализация |
|
||||
|---|---|---|
|
||||
| подтипов | `override` | в рантайме, по фактическому типу |
|
||||
| ad-hoc (перегрузка) | одно имя, разные параметры | при компиляции |
|
||||
| параметрический | дженерики | при компиляции (типы стираются) |
|
||||
| через `when` по sealed | сопоставление типов | в рантайме, но проверяется компилятором |
|
||||
|
||||
## Полиморфизм подтипов
|
||||
```kotlin
|
||||
interface Shape5 {
|
||||
fun area(): Double
|
||||
fun name(): String
|
||||
}
|
||||
|
||||
class Circle5(val r: Double) : Shape5 {
|
||||
override fun area() = Math.PI * r * r
|
||||
override fun name() = "круг"
|
||||
}
|
||||
|
||||
class Square4(val side: Double) : Shape5 {
|
||||
override fun area() = side * side
|
||||
override fun name() = "квадрат"
|
||||
}
|
||||
|
||||
fun describe(s: Shape5) = "${s.name()}: %.2f".format(java.util.Locale.US, s.area())
|
||||
|
||||
fun main() {
|
||||
val shapes: List<Shape5> = listOf(Circle5(1.0), Square4(2.0))
|
||||
shapes.forEach { println(describe(it)) }
|
||||
// круг: 3.14
|
||||
// квадрат: 4.00
|
||||
println(shapes.sumOf { it.area() } > 7) // true
|
||||
}
|
||||
```
|
||||
Функция `describe` написана один раз и работает с любым `Shape5`, включая те, которые появятся позже. Выбор реализации происходит в рантайме по фактическому типу объекта - это динамическая диспетчеризация.
|
||||
|
||||
## Перегрузка
|
||||
Одно имя, разные параметры. Выбор - при компиляции, по статическим типам аргументов:
|
||||
```kotlin
|
||||
fun format2(value: Int) = "целое: $value"
|
||||
fun format2(value: Double) = "дробное: $value"
|
||||
fun format2(value: String) = "строка: '$value'"
|
||||
fun format2(value: Any?) = "что-то: $value"
|
||||
|
||||
fun main() {
|
||||
println(format2(1)) // целое: 1
|
||||
println(format2(1.0)) // дробное: 1.0
|
||||
println(format2("текст")) // строка: 'текст'
|
||||
println(format2(listOf(1))) // что-то: [1]
|
||||
|
||||
val x: Any = 1 // статический тип Any!
|
||||
println(format2(x)) // что-то: 1 — не "целое: 1"
|
||||
}
|
||||
```
|
||||
Последняя строка показывает суть: компилятор видит тип переменной, а не объекта. Это основное отличие от полиморфизма подтипов.
|
||||
|
||||
В Kotlin перегрузка нужна реже - её заменяют значения по умолчанию:
|
||||
```kotlin
|
||||
fun connect(host: String, port: Int = 8080, secure: Boolean = false) =
|
||||
"$host:$port secure=$secure"
|
||||
|
||||
fun main() {
|
||||
println(connect("localhost")) // localhost:8080 secure=false
|
||||
println(connect("localhost", secure = true)) // localhost:8080 secure=true
|
||||
}
|
||||
```
|
||||
Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
|
||||
|
||||
## Параметрический полиморфизм
|
||||
Один код для любого типа:
|
||||
```kotlin
|
||||
fun <T> firstOrDefault(items: List<T>, default: T): T =
|
||||
if (items.isEmpty()) default else items[0]
|
||||
|
||||
class Box4<T>(private val value: T) {
|
||||
fun get(): T = value
|
||||
fun <R> map(f: (T) -> R): Box4<R> = Box4(f(value))
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(firstOrDefault(listOf(1, 2), 0)) // 1
|
||||
println(firstOrDefault(emptyList(), "нет")) // нет
|
||||
println(Box4(5).map { it * 2 }.get()) // 10
|
||||
}
|
||||
```
|
||||
Ограничение параметра типа позволяет вызывать методы:
|
||||
```kotlin
|
||||
fun <T : Comparable<T>> maxOf2(a: T, b: T): T = if (a > b) a else b
|
||||
|
||||
fun main() {
|
||||
println(maxOf2(3, 7)) // 7
|
||||
println(maxOf2("abc", "abd")) // abd
|
||||
}
|
||||
```
|
||||
Подробнее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
## Полиморфизм через sealed и when
|
||||
```kotlin
|
||||
sealed interface Payment {
|
||||
data class Card(val number: String) : Payment
|
||||
data class Cash(val amount: Long) : Payment
|
||||
data object Invoice : Payment
|
||||
}
|
||||
|
||||
fun process2(p: Payment): String = when (p) {
|
||||
is Payment.Card -> "карта ****${p.number.takeLast(4)}"
|
||||
is Payment.Cash -> "наличные ${p.amount}"
|
||||
Payment.Invoice -> "счёт"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(process2(Payment.Card("1234567890123456"))) // карта ****3456
|
||||
println(process2(Payment.Cash(500))) // наличные 500
|
||||
println(process2(Payment.Invoice)) // счёт
|
||||
}
|
||||
```
|
||||
Это альтернатива полиморфизму подтипов с обратным распределением ответственности:
|
||||
|
||||
| | метод в классе | `when` по sealed |
|
||||
|---|---|---|
|
||||
| добавить новый тип | правок в одном месте | компилятор найдёт все `when` |
|
||||
| добавить новую операцию | правка во всех классах | один новый `when` |
|
||||
| логика операции | размазана по классам | собрана в одном месте |
|
||||
|
||||
Выбор зависит от того, что меняется чаще. Набор типов стабилен, а операций много - `sealed` + `when`. Операции стабильны, а типы добавляются - методы в классах.
|
||||
|
||||
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Что разрешается статически
|
||||
Это главный источник ошибок ожиданий. Статически (по типу ссылки, а не объекта) выбираются:
|
||||
|
||||
**Функции-расширения:**
|
||||
```kotlin
|
||||
open class Base37
|
||||
class Derived33 : Base37()
|
||||
|
||||
fun Base37.describe() = "база"
|
||||
fun Derived33.describe() = "подкласс"
|
||||
|
||||
fun main() {
|
||||
val d: Base37 = Derived33()
|
||||
println(d.describe()) // база — по типу переменной!
|
||||
println(Derived33().describe()) // подкласс
|
||||
}
|
||||
```
|
||||
|
||||
**Перегруженные функции:**
|
||||
```kotlin
|
||||
open class Animal4
|
||||
class Dog3 : Animal4()
|
||||
|
||||
fun feed(a: Animal4) = "корм для животного"
|
||||
fun feed(d: Dog3) = "корм для собаки"
|
||||
|
||||
fun main() {
|
||||
val a: Animal4 = Dog3()
|
||||
println(feed(a)) // корм для животного
|
||||
println(feed(Dog3())) // корм для собаки
|
||||
}
|
||||
```
|
||||
|
||||
**Члены `companion object`** - они не переопределяются:
|
||||
```kotlin
|
||||
open class Base38 {
|
||||
companion object { fun create() = "из Base" }
|
||||
}
|
||||
|
||||
class Derived34 : Base38() {
|
||||
companion object { fun create() = "из Derived" }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Base38.create()) // из Base
|
||||
println(Derived34.create()) // из Derived — выбор по имени класса
|
||||
}
|
||||
```
|
||||
|
||||
Динамически выбираются только **члены класса**, помеченные `open`/`abstract` и переопределённые через `override`.
|
||||
|
||||
## Приведение и проверка типа
|
||||
```kotlin
|
||||
fun handle(x: Any): String = when (x) {
|
||||
is String -> "строка длины ${x.length}" // умное приведение
|
||||
is Int -> "число ${x + 1}"
|
||||
is List<*> -> "список из ${x.size}"
|
||||
else -> "неизвестно: ${x::class.simpleName}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle("abc")) // строка длины 3
|
||||
println(handle(5)) // число 6
|
||||
println(handle(listOf(1, 2))) // список из 2
|
||||
println(handle(1.0)) // неизвестно: Double
|
||||
}
|
||||
```
|
||||
Длинная цепочка `is` часто означает, что иерархия просит `sealed`-типа или полиморфного метода. Подробнее: [Приведение типов](../Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md)
|
||||
|
||||
## Вариантность
|
||||
Полиморфизм распространяется на дженерики через `out` и `in`:
|
||||
```kotlin
|
||||
open class Animal5(val name: String)
|
||||
class Dog4(name: String) : Animal5(name)
|
||||
|
||||
fun printAll(animals: List<Animal5>) = animals.joinToString { it.name }
|
||||
|
||||
fun main() {
|
||||
val dogs: List<Dog4> = listOf(Dog4("Рекс"), Dog4("Бобик"))
|
||||
println(printAll(dogs)) // Рекс, Бобик — List<Dog4> подходит как List<Animal5>
|
||||
}
|
||||
```
|
||||
Это работает, потому что `List<out E>` ковариантен. С `MutableList` так нельзя - он инвариантен. Подробнее: [Вариантность](../Дженерики/Вариантность.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | метод виртуален по умолчанию | нужен `open` |
|
||||
> | `@Override` опциональна | `override` обязательно |
|
||||
> | перегрузка для опциональных параметров | значения по умолчанию |
|
||||
> | `instanceof` + каст | `is` + умное приведение |
|
||||
> | `switch` с pattern matching (Java 21) | `when` с `is` |
|
||||
> | Visitor для операций над иерархией | `sealed` + `when` |
|
||||
> | статические методы скрываются | члены компаньона не наследуются |
|
||||
>
|
||||
> **Механика полиморфизма подтипов идентична** - это та же виртуальная диспетчеризация JVM. Отличается значение по умолчанию: в Kotlin метод финален, пока не помечен `open`.
|
||||
>
|
||||
> **Visitor почти не нужен.** В Java для добавления операций к закрытой иерархии без правки классов применяют паттерн Visitor: интерфейс с методом на каждый тип, метод `accept` в каждом классе. `sealed` + `when` даёт ту же проверку полноты компилятором, но без дополнительного кода:
|
||||
> ```kotlin
|
||||
> sealed interface Expr2 {
|
||||
> data class Num2(val value: Int) : Expr2
|
||||
> data class Add2(val l: Expr2, val r: Expr2) : Expr2
|
||||
> }
|
||||
>
|
||||
> fun eval(e: Expr2): Int = when (e) {
|
||||
> is Expr2.Num2 -> e.value
|
||||
> is Expr2.Add2 -> eval(e.l) + eval(e.r)
|
||||
> }
|
||||
>
|
||||
> fun show(e: Expr2): String = when (e) { // новая операция — один when
|
||||
> is Expr2.Num2 -> "${e.value}"
|
||||
> is Expr2.Add2 -> "(${show(e.l)} + ${show(e.r)})"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val expr = Expr2.Add2(Expr2.Num2(1), Expr2.Num2(2))
|
||||
> println("${show(expr)} = ${eval(expr)}") // (1 + 2) = 3
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Умное приведение убирает двойную работу.** В Java после `instanceof` нужен явный каст (до версии 16, где появился pattern matching).
|
||||
>
|
||||
> Сравнить: [Полиморфизм в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Расширения не полиморфны** - выбор по статическому типу. Это самая частая ошибка:
|
||||
> ```kotlin
|
||||
> open class Base39
|
||||
> class Derived35 : Base39()
|
||||
>
|
||||
> fun Base39.name() = "база"
|
||||
> fun Derived35.name() = "подкласс"
|
||||
>
|
||||
> fun main() {
|
||||
> println(listOf<Base39>(Derived35()).first().name()) // база
|
||||
> }
|
||||
> ```
|
||||
> Нужен полиморфизм - нужен член класса.
|
||||
>
|
||||
> **Перегрузка выбирается по типу ссылки, а не объекта.** Код ведёт себя по-разному в зависимости от того, как объявлена переменная.
|
||||
>
|
||||
> **Смешение перегрузки и переопределения даёт неочевидный результат:**
|
||||
> ```kotlin
|
||||
> open class Base40 { open fun f(x: Any) = "any" }
|
||||
> class Derived36 : Base40() {
|
||||
> override fun f(x: Any) = "override"
|
||||
> fun f(x: Int) = "overload"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val d = Derived36()
|
||||
> println(d.f(1)) // overload
|
||||
> println((d as Base40).f(1)) // override
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Стирание типов ломает перегрузку по дженерикам:**
|
||||
> ```kotlin
|
||||
> // fun show(items: List<String>) = "строки"
|
||||
> // fun show(items: List<Int>) = "числа" // ошибка: та же JVM-сигнатура
|
||||
>
|
||||
> @JvmName("showStrings")
|
||||
> fun show2(items: List<String>) = "строки: ${items.size}"
|
||||
> fun show2(items: List<Int>) = "числа: ${items.size}"
|
||||
>
|
||||
> fun main() { println(show2(listOf("a"))) } // строки: 1
|
||||
> ```
|
||||
> Обойти можно через `@JvmName`, но обычно это признак, что нужен один дженерик-метод.
|
||||
>
|
||||
> **Длинная цепочка `is` вместо полиморфизма.** Если `when (x) { is A -> ...; is B -> ... }` повторяется в нескольких местах, логику стоит перенести в методы типов или закрыть иерархию через `sealed`, чтобы компилятор контролировал полноту.
|
||||
>
|
||||
> **Члены `companion object` не переопределяются.** Попытка «переопределить» фабрику в подклассе создаёт независимый компаньон; вызов `Derived.create()` работает только потому, что имя разрешается статически.
|
||||
>
|
||||
> **Массивы инвариантны, а `List` ковариантен** - в отличие от Java, где массивы ковариантны и могут бросить `ArrayStoreException`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings: List<String> = listOf("a")
|
||||
> val anys: List<Any> = strings // ok
|
||||
> println(anys.size) // 1
|
||||
> // val arr: Array<Any> = arrayOf("a") // ошибка компиляции
|
||||
> }
|
||||
> ```
|
||||
|
||||
Дополнительно: [Переопределение](Переопределение.md)
|
||||
Далее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
@@ -1,433 +0,0 @@
|
||||
---
|
||||
title: Свойства
|
||||
description: Геттеры и сеттеры, backing field, вычисляемые свойства, lateinit, lazy и делегаты
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Свойство - это поле плюс методы доступа. Снаружи выглядит как поле, но обращение всегда идёт через геттер и сеттер.
|
||||
|
||||
```kotlin
|
||||
class User {
|
||||
val id: Int = 1 // только чтение: геттер
|
||||
var name: String = "Иван" // чтение и запись: геттер + сеттер
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User()
|
||||
println(u.id) // вызов геттера
|
||||
u.name = "Пётр" // вызов сеттера
|
||||
println(u.name) // Пётр
|
||||
}
|
||||
```
|
||||
|
||||
Полная форма объявления:
|
||||
```kotlin
|
||||
class Rect {
|
||||
var width: Int = 0
|
||||
get() = field
|
||||
set(value) { field = value }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val r = Rect()
|
||||
r.width = 5
|
||||
println(r.width) // 5
|
||||
}
|
||||
```
|
||||
Такой код писать не нужно - это то, что компилятор генерирует сам. Писать вручную стоит только нестандартное поведение.
|
||||
|
||||
## field - backing field
|
||||
Внутри геттера и сеттера доступен `field` - само хранилище. Прямое обращение к свойству по имени привело бы к бесконечной рекурсии.
|
||||
```kotlin
|
||||
class Counter {
|
||||
var value: Int = 0
|
||||
set(newValue) {
|
||||
println("было $field, стало $newValue")
|
||||
field = newValue // так
|
||||
// value = newValue // бесконечная рекурсия!
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = Counter()
|
||||
c.value = 5 // было 0, стало 5
|
||||
c.value = 7 // было 5, стало 7
|
||||
println(c.value) // 7
|
||||
}
|
||||
```
|
||||
|
||||
`field` появляется только если он реально нужен. У вычисляемого свойства хранилища нет:
|
||||
```kotlin
|
||||
class Rect2(val width: Int, val height: Int) {
|
||||
val area: Int get() = width * height // поля нет, считается каждый раз
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Rect2(2, 3).area) // 6
|
||||
}
|
||||
```
|
||||
Это важно для памяти: свойство-геттер не занимает места в объекте.
|
||||
|
||||
## Валидация в сеттере
|
||||
```kotlin
|
||||
class Account {
|
||||
var balance: Long = 0
|
||||
set(value) {
|
||||
require(value >= 0) { "баланс не может быть отрицательным: $value" }
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = Account()
|
||||
a.balance = 100
|
||||
println(a.balance) // 100
|
||||
try {
|
||||
a.balance = -1
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // баланс не может быть отрицательным: -1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Видимость сеттера
|
||||
Частый приём - читать можно всем, менять только внутри:
|
||||
```kotlin
|
||||
class Session {
|
||||
var token: String = ""
|
||||
private set // геттер public, сеттер private
|
||||
|
||||
fun login(t: String) { token = t }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Session()
|
||||
s.login("abc")
|
||||
println(s.token) // abc
|
||||
// s.token = "x" // ошибка: сеттер приватный
|
||||
}
|
||||
```
|
||||
Это аккуратнее, чем приватное поле плюс отдельный публичный геттер.
|
||||
|
||||
## Вычисляемые свойства или функции
|
||||
```kotlin
|
||||
class Order3(val items: List<Int>) {
|
||||
val total: Int get() = items.sum() // свойство
|
||||
fun computeTotal() = items.sum() // функция
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val o = Order3(listOf(1, 2, 3))
|
||||
println("${o.total} ${o.computeTotal()}") // 6 6
|
||||
}
|
||||
```
|
||||
Соглашение: свойство, если это **характеристика** объекта, вычисляется дешёво и не бросает исключений. Функция - если выполняется работа: запрос, вычисление сложнее O(1), обращение к сети.
|
||||
|
||||
```kotlin
|
||||
class Data2(val rows: List<String>) {
|
||||
val size: Int get() = rows.size // дешёво — свойство
|
||||
fun parse(): List<Int> = rows.map { it.length } // работа — функция
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val d = Data2(listOf("ab", "cde"))
|
||||
println("${d.size} ${d.parse()}") // 2 [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
## Инициализация и порядок
|
||||
Свойство должно быть инициализировано при создании объекта:
|
||||
```kotlin
|
||||
class Service5 {
|
||||
val a: Int = 1 // сразу
|
||||
val b: Int // в init
|
||||
init { b = a + 1 }
|
||||
val c: Int get() = a + b // геттер, инициализации не требует
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service5()
|
||||
println("${s.a} ${s.b} ${s.c}") // 1 2 3
|
||||
}
|
||||
```
|
||||
Подробнее: [Инициализация](Инициализация.md)
|
||||
|
||||
## lateinit
|
||||
Когда значение появится позже, но хочется non-null тип:
|
||||
```kotlin
|
||||
class Service6 {
|
||||
lateinit var client: String
|
||||
|
||||
fun setup() { client = "готов" }
|
||||
fun use() = if (::client.isInitialized) client else "не настроен"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service6()
|
||||
println(s.use()) // не настроен
|
||||
s.setup()
|
||||
println(s.use()) // готов
|
||||
println(s.client) // готов
|
||||
}
|
||||
```
|
||||
Ограничения `lateinit`:
|
||||
- только `var`
|
||||
- только non-null **ссылочный** тип: для `Int`, `Double` и других примитивов нельзя
|
||||
- нельзя задать свой геттер или сеттер
|
||||
- проверка `::x.isInitialized` доступна только внутри класса
|
||||
|
||||
Обращение до инициализации даёт понятную ошибку:
|
||||
```kotlin
|
||||
class Service7 { lateinit var name: String }
|
||||
|
||||
fun main() {
|
||||
try {
|
||||
println(Service7().name)
|
||||
} catch (e: UninitializedPropertyAccessException) {
|
||||
println(e.message) // lateinit property name has not been initialized
|
||||
}
|
||||
}
|
||||
```
|
||||
`lateinit` нужен для внедрения зависимостей и жизненных циклов фреймворков (Android, Spring). В остальных случаях лучше nullable-тип или `lazy`.
|
||||
|
||||
## lazy
|
||||
Вычисляется один раз при первом обращении:
|
||||
```kotlin
|
||||
class Config5 {
|
||||
val heavy: String by lazy {
|
||||
println("вычисляем...")
|
||||
"результат"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = Config5()
|
||||
println("создан") // создан
|
||||
println(c.heavy) // вычисляем... затем результат
|
||||
println(c.heavy) // результат — повторно не считается
|
||||
}
|
||||
```
|
||||
`lazy` работает только с `val`. По умолчанию потокобезопасен (синхронизация при первом доступе); если объект гарантированно в одном потоке, можно ослабить:
|
||||
```kotlin
|
||||
val value: Int by lazy(LazyThreadSafetyMode.NONE) { 42 }
|
||||
|
||||
fun main() { println(value) } // 42
|
||||
```
|
||||
|
||||
| | `lateinit var` | `by lazy` |
|
||||
|---|---|---|
|
||||
| изменяемость | `var` | `val` |
|
||||
| кто задаёт значение | внешний код | сама лямбда |
|
||||
| примитивы | нельзя | можно |
|
||||
| потокобезопасность | нет | есть по умолчанию |
|
||||
| доступ до инициализации | исключение | вычислит на месте |
|
||||
|
||||
## observable и vetoable
|
||||
Готовые делегаты для реакции на изменение:
|
||||
```kotlin
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class Form {
|
||||
var name: String by Delegates.observable("") { _, old, new ->
|
||||
println("name: '$old' -> '$new'")
|
||||
}
|
||||
|
||||
var age: Int by Delegates.vetoable(0) { _, _, new ->
|
||||
new in 0..150 // false отменяет присваивание
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val f = Form()
|
||||
f.name = "Иван" // name: '' -> 'Иван'
|
||||
f.age = 20
|
||||
f.age = 500 // отклонено
|
||||
println(f.age) // 20
|
||||
}
|
||||
```
|
||||
|
||||
`Delegates.notNull()` - альтернатива `lateinit` для примитивов:
|
||||
```kotlin
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class Service8 {
|
||||
var port: Int by Delegates.notNull()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val s = Service8()
|
||||
s.port = 8080
|
||||
println(s.port) // 8080
|
||||
}
|
||||
```
|
||||
|
||||
## Свойства в Map
|
||||
Делегирование в `Map` - удобно для разбора динамических данных:
|
||||
```kotlin
|
||||
class User2(map: Map<String, Any?>) {
|
||||
val name: String by map
|
||||
val age: Int by map
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val u = User2(mapOf("name" to "Иван", "age" to 20))
|
||||
println("${u.name} ${u.age}") // Иван 20
|
||||
}
|
||||
```
|
||||
|
||||
## Свой делегат
|
||||
```kotlin
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class Trimmed {
|
||||
private var value = ""
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
|
||||
value = newValue.trim()
|
||||
}
|
||||
}
|
||||
|
||||
class Form2 {
|
||||
var login: String by Trimmed()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val f = Form2()
|
||||
f.login = " иван "
|
||||
println("'${f.login}'") // 'иван'
|
||||
}
|
||||
```
|
||||
Подробнее: [Делегирование](Делегирование.md)
|
||||
|
||||
## Свойства верхнего уровня и расширения
|
||||
```kotlin
|
||||
val appName = "MyApp" // свойство файла
|
||||
const val VERSION = "1.0" // константа компиляции
|
||||
|
||||
val String.firstWord: String // свойство-расширение
|
||||
get() = substringBefore(" ")
|
||||
|
||||
fun main() {
|
||||
println("$appName $VERSION") // MyApp 1.0
|
||||
println("привет мир".firstWord) // привет
|
||||
}
|
||||
```
|
||||
У свойства-расширения не может быть `field` - только геттер. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | поле + `getX()` + `setX()` | `var x` |
|
||||
> | `obj.getName()` | `obj.name` |
|
||||
> | `obj.setName("x")` | `obj.name = "x"` |
|
||||
> | `private final` поле | `val` |
|
||||
> | геттер с вычислением | `val x get() = ...` |
|
||||
> | Lombok `@Getter`/`@Setter` | не нужны |
|
||||
> | `Supplier` + кеш для ленивости | `by lazy` |
|
||||
> | `Optional<T>` поле | `T?` |
|
||||
>
|
||||
> **Свойства - не синтаксический сахар над геттерами, а единица абстракции.** Переход от хранимого свойства к вычисляемому не меняет код клиентов:
|
||||
> ```kotlin
|
||||
> class Rect3(val w: Int, val h: Int) {
|
||||
> val area: Int = w * h // было: хранится
|
||||
> }
|
||||
>
|
||||
> class Rect4(val w: Int, val h: Int) {
|
||||
> val area: Int get() = w * h // стало: считается. Клиенты не изменились
|
||||
> }
|
||||
>
|
||||
> fun main() { println("${Rect3(2,3).area} ${Rect4(2,3).area}") } // 6 6
|
||||
> ```
|
||||
> В Java переход от `obj.field` к `obj.getField()` - ломающее изменение, поэтому там поля сразу прячут за геттерами «на всякий случай». В Kotlin эта защита встроена.
|
||||
>
|
||||
> **Java-геттеры видны как свойства.** Метод `getName()` в Java-классе доступен из Kotlin как `obj.name`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val sb = StringBuilder("abc")
|
||||
> println(sb.length) // вместо sb.length() — синтетическое свойство
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`by lazy` заменяет двойную проверку блокировки.** Классический ленивый синглтон в Java требует `volatile` и синхронизации; в Kotlin это одна строка.
|
||||
>
|
||||
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Обращение к свойству внутри его сеттера - бесконечная рекурсия:**
|
||||
> ```kotlin
|
||||
> class Bad2 {
|
||||
> var x: Int = 0
|
||||
> set(value) { field = value } // верно
|
||||
> // set(value) { x = value } // StackOverflowError
|
||||
> }
|
||||
>
|
||||
> fun main() { Bad2().x = 1; println("ok") }
|
||||
> ```
|
||||
>
|
||||
> **Вычисляемое свойство выполняется при каждом чтении:**
|
||||
> ```kotlin
|
||||
> class Slow(val items: List<Int>) {
|
||||
> val max: Int get() = items.also { println("считаем") }.max()
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val s = Slow(listOf(1, 2))
|
||||
> println(s.max)
|
||||
> println(s.max) // "считаем" выведется дважды
|
||||
> }
|
||||
> ```
|
||||
> Если вычисление дорогое, нужен `by lazy` или хранимое свойство.
|
||||
>
|
||||
> **`val` не означает неизменяемость объекта** - только то, что ссылку нельзя переприсвоить:
|
||||
> ```kotlin
|
||||
> class Box2 { val items = mutableListOf(1) }
|
||||
>
|
||||
> fun main() {
|
||||
> val b = Box2()
|
||||
> b.items.add(2) // разрешено
|
||||
> println(b.items) // [1, 2]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`val` с геттером может возвращать разное:**
|
||||
> ```kotlin
|
||||
> class Clock { val now: Long get() = System.nanoTime() }
|
||||
>
|
||||
> fun main() {
|
||||
> val c = Clock()
|
||||
> println(c.now != c.now) // true — два разных значения
|
||||
> }
|
||||
> ```
|
||||
> Поэтому умное приведение для таких свойств не работает. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
|
||||
>
|
||||
> **`lateinit` не работает с примитивами и с `val`.** Для `Int` используйте `Delegates.notNull()` или значение-маркер.
|
||||
>
|
||||
> **`lateinit` скрывает незаданную зависимость до рантайма.** Чем раньше объект получает всё необходимое (лучше - в конструкторе), тем меньше шансов на `UninitializedPropertyAccessException`.
|
||||
>
|
||||
> **`by lazy` с исключением внутри повторит попытку:**
|
||||
> ```kotlin
|
||||
> class Risky {
|
||||
> var attempts = 0
|
||||
> val value: String by lazy {
|
||||
> attempts++
|
||||
> if (attempts < 2) error("первая попытка падает")
|
||||
> "успех на попытке $attempts"
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val r = Risky()
|
||||
> runCatching { r.value }
|
||||
> println(r.value) // успех на попытке 2
|
||||
> }
|
||||
> ```
|
||||
> Результат не кешируется, пока лямбда не завершится успешно.
|
||||
>
|
||||
> **Свойство-расширение не имеет поля** - состояние к чужому классу добавить нельзя, только вычисление.
|
||||
>
|
||||
> **Публичный `var` в API позволяет внешнему коду менять состояние в любой момент.** Часто правильнее `var` с `private set` и явным методом изменения.
|
||||
|
||||
Дополнительно: [Делегирование](Делегирование.md)
|
||||
Далее: [Инициализация](Инициализация.md)
|
||||
@@ -1,653 +0,0 @@
|
||||
---
|
||||
title: Аннотации
|
||||
description: Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа
|
||||
tags:
|
||||
- kotlin
|
||||
- annotations
|
||||
- reflection
|
||||
---
|
||||
|
||||
Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор.
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Table(val name: String, val schema: String = "public")
|
||||
|
||||
@Table("users")
|
||||
class User(val id: Int, val name: String)
|
||||
|
||||
fun main() {
|
||||
val table = User::class.findAnnotation<Table>()
|
||||
println("таблица=${table?.name}, схема=${table?.schema}")
|
||||
}
|
||||
```
|
||||
```
|
||||
таблица=users, схема=public
|
||||
```
|
||||
|
||||
## Объявление
|
||||
```kotlin
|
||||
annotation class Simple
|
||||
|
||||
annotation class WithParams(
|
||||
val name: String,
|
||||
val priority: Int = 0,
|
||||
val tags: Array<String> = [],
|
||||
)
|
||||
|
||||
fun main() {
|
||||
println("объявлены")
|
||||
}
|
||||
```
|
||||
```
|
||||
объявлены
|
||||
```
|
||||
Правила для аннотаций жёстче, чем для обычных классов:
|
||||
- нет тела, нет функций и состояния - только параметры конструктора;
|
||||
- все параметры - `val`;
|
||||
- типы параметров ограничены: примитивы, `String`, `KClass`, `enum`, другая аннотация, массивы перечисленного;
|
||||
- нельзя `null` в качестве значения по умолчанию - сам тип не может быть nullable.
|
||||
|
||||
## Цели - @Target
|
||||
```kotlin
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
annotation class Marker
|
||||
|
||||
@Marker
|
||||
class Annotated(@Marker val prop: Int) {
|
||||
@Marker
|
||||
fun method() = Unit
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println("применено к четырём местам")
|
||||
}
|
||||
```
|
||||
```
|
||||
применено к четырём местам
|
||||
```
|
||||
| Цель | Куда |
|
||||
|---|---|
|
||||
| `CLASS` | класс, интерфейс, `object`, аннотация |
|
||||
| `FUNCTION` | функция, не конструктор |
|
||||
| `PROPERTY` | свойство (только Kotlin-код) |
|
||||
| `FIELD` | backing field свойства |
|
||||
| `VALUE_PARAMETER` | параметр функции или конструктора |
|
||||
| `CONSTRUCTOR` | конструктор |
|
||||
| `PROPERTY_GETTER`, `PROPERTY_SETTER` | аксессоры |
|
||||
| `LOCAL_VARIABLE` | локальная переменная |
|
||||
| `TYPE` | использование типа: `List<@Marker String>` |
|
||||
| `TYPE_PARAMETER` | параметр типа |
|
||||
| `EXPRESSION` | выражение (требует `SOURCE`) |
|
||||
| `FILE` | файл, через `@file:` |
|
||||
| `ANNOTATION_CLASS` | мета-аннотация |
|
||||
|
||||
Без `@Target` аннотация применима почти везде, кроме `TYPE`, `EXPRESSION`, `FILE` и `TYPE_PARAMETER` - их нужно указывать явно.
|
||||
|
||||
## Хранение - @Retention
|
||||
```kotlin
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Src
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Bin
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Run
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class NoRetention
|
||||
|
||||
@Src @Bin @Run
|
||||
class All
|
||||
|
||||
@NoRetention
|
||||
class Default
|
||||
|
||||
fun main() {
|
||||
println("All=${All::class.annotations.map { it.annotationClass.simpleName }}")
|
||||
println("Default=${Default::class.annotations.map { it.annotationClass.simpleName }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
All=[Run]
|
||||
Default=[NoRetention]
|
||||
```
|
||||
| Retention | В байт-коде | Видна рефлексии |
|
||||
|---|---|---|
|
||||
| `SOURCE` | нет | нет |
|
||||
| `BINARY` | да | нет |
|
||||
| `RUNTIME` (по умолчанию) | да | да |
|
||||
|
||||
Ключевое отличие от Java: **по умолчанию `RUNTIME`**, а не `CLASS`. Поэтому `@NoRetention` без явного указания видна через рефлексию - в Java аналогичная аннотация была бы невидима. Для аннотаций, которые читает только KSP или линтер, стоит явно ставить `SOURCE`: это убирает их из байт-кода.
|
||||
|
||||
## Use-site targets
|
||||
Свойство в Kotlin компилируется в несколько Java-элементов: поле, геттер, иногда сеттер, а для параметра конструктора ещё и параметр. Аннотация должна попасть на конкретный из них - для этого есть префиксы.
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.javaSetter
|
||||
|
||||
@Target(
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Mark(val where: String)
|
||||
|
||||
class Targets(
|
||||
@Mark("без префикса") val implicit: Int,
|
||||
@field:Mark("field") val onField: Int,
|
||||
@get:Mark("getter") val onGetter: Int,
|
||||
@property:Mark("property") val onProperty: Int,
|
||||
) {
|
||||
@set:Mark("setter")
|
||||
var mutable: Int = 0
|
||||
}
|
||||
|
||||
fun main() {
|
||||
for (p in Targets::class.memberProperties.sortedBy { it.name }) {
|
||||
val prop = p.annotations.filterIsInstance<Mark>().map { it.where }
|
||||
val field = p.javaField?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
|
||||
val getter = p.javaGetter?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
|
||||
println("${p.name}: property=$prop field=$field getter=$getter")
|
||||
}
|
||||
val ctor = Targets::class.primaryConstructor!!.javaConstructor!!
|
||||
ctor.parameterAnnotations.forEachIndexed { i, anns ->
|
||||
println("параметр $i: ${anns.filterIsInstance<Mark>().map { it.where }}")
|
||||
}
|
||||
val mutable = Targets::class.memberProperties.first { it.name == "mutable" }
|
||||
val setter = (mutable as KMutableProperty1<*, *>).javaSetter
|
||||
println("setter=${setter?.annotations?.filterIsInstance<Mark>()?.map { it.where }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
implicit: property=[без префикса] field=[] getter=[]
|
||||
mutable: property=[] field=[] getter=[]
|
||||
onField: property=[] field=[field] getter=[]
|
||||
onGetter: property=[] field=[] getter=[getter]
|
||||
onProperty: property=[property] field=[] getter=[]
|
||||
параметр 0: [без префикса]
|
||||
параметр 1: []
|
||||
параметр 2: []
|
||||
параметр 3: []
|
||||
setter=[setter]
|
||||
```
|
||||
| Префикс | Куда попадёт |
|
||||
|---|---|
|
||||
| `@property:` | свойство (видно только Kotlin-рефлексии) |
|
||||
| `@field:` | backing field |
|
||||
| `@get:` | геттер |
|
||||
| `@set:` | сеттер |
|
||||
| `@param:` | параметр конструктора |
|
||||
| `@setparam:` | параметр сеттера |
|
||||
| `@receiver:` | получатель функции-расширения |
|
||||
| `@delegate:` | поле делегата |
|
||||
| `@file:` | файл, до объявления `package` |
|
||||
| `@all:` | все применимые цели сразу |
|
||||
|
||||
### Правило выбора по умолчанию
|
||||
Без префикса аннотация применяется к **первой подходящей** цели из списка, в порядке: `param` → `property` → `field`. Это проверяется легко:
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Multi
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class PropOrField
|
||||
|
||||
@Target(AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FieldOnly
|
||||
|
||||
class C(
|
||||
@Multi val a: Int,
|
||||
@PropOrField val b: Int,
|
||||
@FieldOnly val c: Int,
|
||||
)
|
||||
|
||||
fun report(name: String) {
|
||||
val p = C::class.memberProperties.first { it.name == name }
|
||||
val onProperty = p.annotations.map { it.annotationClass.simpleName }
|
||||
val onField = p.javaField?.annotations?.map { it.annotationClass.simpleName }.orEmpty()
|
||||
val index = C::class.primaryConstructor!!.parameters.indexOfFirst { it.name == name }
|
||||
val onParam = C::class.primaryConstructor!!.javaConstructor!!
|
||||
.parameterAnnotations[index].map { it.annotationClass.simpleName }
|
||||
println("$name: property=$onProperty field=$onField param=$onParam")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
report("a")
|
||||
report("b")
|
||||
report("c")
|
||||
}
|
||||
```
|
||||
```
|
||||
a: property=[Multi] field=[] param=[Multi]
|
||||
b: property=[PropOrField] field=[] param=[]
|
||||
c: property=[] field=[FieldOnly] param=[]
|
||||
```
|
||||
Обратите внимание на `a`: аннотация, применимая и к параметру, и к свойству, попала **на оба** - на параметр конструктора и на Kotlin-свойство (но не на поле). Именно здесь возникают проблемы с Java-библиотеками: Jackson или Hibernate читают поля через Java Reflection, а там аннотации нет - нужен явный `@field:`.
|
||||
|
||||
### @all
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
|
||||
@Target(
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Mark
|
||||
|
||||
class WithAll(@all:Mark val x: Int)
|
||||
|
||||
fun main() {
|
||||
val p = WithAll::class.memberProperties.first()
|
||||
println("property=${p.annotations.size}")
|
||||
println("field=${p.javaField?.annotations?.size}")
|
||||
println("getter=${p.javaGetter?.annotations?.size}")
|
||||
val params = WithAll::class.primaryConstructor!!.javaConstructor!!.parameterAnnotations
|
||||
println("param=${params[0].size}")
|
||||
}
|
||||
```
|
||||
```
|
||||
property=1
|
||||
field=1
|
||||
getter=1
|
||||
param=1
|
||||
```
|
||||
`@all:` - решение давней проблемы: раньше приходилось писать `@field:Mark @get:Mark @param:Mark` подряд. В Kotlin 2.4 работает без дополнительных флагов компилятора.
|
||||
|
||||
## Параметры аннотаций
|
||||
```kotlin
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
interface Validator
|
||||
class NotBlank : Validator
|
||||
class MaxLength : Validator
|
||||
|
||||
enum class Mode { FAST, SAFE }
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Validators(val value: Array<KClass<out Validator>>)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Levels(val value: IntArray, val names: Array<String>)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Nested(val inner: Levels)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class WithEnum(val mode: Mode)
|
||||
|
||||
@Validators([NotBlank::class, MaxLength::class])
|
||||
@Levels(value = [1, 2, 3], names = ["low", "mid", "high"])
|
||||
@Nested(Levels([9], ["x"]))
|
||||
@WithEnum(Mode.SAFE)
|
||||
class Configured
|
||||
|
||||
fun main() {
|
||||
val kc = Configured::class
|
||||
println("классы=${kc.findAnnotation<Validators>()!!.value.map { it.simpleName }}")
|
||||
val levels = kc.findAnnotation<Levels>()!!
|
||||
println("IntArray=${levels.value.toList()} names=${levels.names.toList()}")
|
||||
println("вложенная=${kc.findAnnotation<Nested>()!!.inner.value.toList()}")
|
||||
println("enum=${kc.findAnnotation<WithEnum>()!!.mode}")
|
||||
}
|
||||
```
|
||||
```
|
||||
классы=[NotBlank, MaxLength]
|
||||
IntArray=[1, 2, 3] names=[low, mid, high]
|
||||
вложенная=[9]
|
||||
enum=SAFE
|
||||
```
|
||||
Массивы задаются литералом `[a, b]` - старый синтаксис `arrayOf(a, b)` тоже работает, но литерал короче. Классы передаются как `KClass`, то есть `Foo::class`, а не `Foo::class.java`.
|
||||
|
||||
## Значения по умолчанию и equals
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class HasDefaults(val a: Int = 1, val b: String = "x", val c: IntArray = [1, 2])
|
||||
|
||||
@HasDefaults
|
||||
class WithDefaults
|
||||
|
||||
@HasDefaults(a = 5)
|
||||
class Partial
|
||||
|
||||
fun main() {
|
||||
val d = WithDefaults::class.findAnnotation<HasDefaults>()!!
|
||||
println("дефолты a=${d.a} b=${d.b} c=${d.c.toList()}")
|
||||
val p = Partial::class.findAnnotation<HasDefaults>()!!
|
||||
println("частично a=${p.a} b=${p.b}")
|
||||
println("toString=$p")
|
||||
println("равенство=${d == WithDefaults::class.findAnnotation<HasDefaults>()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
дефолты a=1 b=x c=[1, 2]
|
||||
частично a=5 b=x
|
||||
toString=@HasDefaults(a=5, b="x", c={1, 2})
|
||||
равенство=true
|
||||
```
|
||||
Экземпляры аннотаций сравниваются по значениям параметров, а `toString` даёт читаемое представление с подставленными дефолтами - удобно для отладки.
|
||||
|
||||
## Повторяемые аннотации
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
|
||||
@Repeatable
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Tag(val value: String)
|
||||
|
||||
@Tag("a")
|
||||
@Tag("b")
|
||||
class Tagged
|
||||
|
||||
fun main() {
|
||||
println("все теги=${Tagged::class.findAnnotations<Tag>().map { it.value }}")
|
||||
println("через annotations=${Tagged::class.annotations.filterIsInstance<Tag>().map { it.value }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
все теги=[a, b]
|
||||
через annotations=[a, b]
|
||||
```
|
||||
В Java для повторяемых аннотаций нужен явный контейнер (`@Repeatable(Tags.class)` плюс класс `Tags` с массивом). В Kotlin достаточно `@Repeatable`, а для чтения есть `findAnnotations<T>()`, возвращающий список.
|
||||
|
||||
## Чтение через рефлексию
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Table(val name: String)
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Column(val name: String, val nullable: Boolean = false)
|
||||
|
||||
@Table("users")
|
||||
class User(
|
||||
@Column("user_id") val id: Int,
|
||||
@Column("full_name", nullable = true) val name: String?,
|
||||
)
|
||||
|
||||
fun main() {
|
||||
val kc = User::class
|
||||
println("hasAnnotation=${kc.hasAnnotation<Table>()}")
|
||||
println("findAnnotation=${kc.findAnnotation<Table>()?.name}")
|
||||
for (p in kc.memberProperties.sortedBy { it.name }) {
|
||||
val column = p.findAnnotation<Column>()
|
||||
println("${p.name}: column=${column?.name} nullable=${column?.nullable}")
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
hasAnnotation=true
|
||||
findAnnotation=users
|
||||
id: column=user_id nullable=false
|
||||
name: column=full_name nullable=true
|
||||
```
|
||||
| Функция | Что возвращает |
|
||||
|---|---|
|
||||
| `annotations` | список всех `RUNTIME`-аннотаций элемента |
|
||||
| `findAnnotation<T>()` | первую аннотацию типа `T` или `null` |
|
||||
| `findAnnotations<T>()` | все аннотации типа `T` (для `@Repeatable`) |
|
||||
| `hasAnnotation<T>()` | есть ли аннотация |
|
||||
| `annotationClass` | `KClass` самой аннотации |
|
||||
|
||||
Эти функции есть у `KClass`, `KProperty`, `KFunction` и `KParameter` - интерфейс единый. Подробнее: [Рефлексия](Рефлексия.md)
|
||||
|
||||
## Практический пример: валидация
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class NotBlank(val message: String = "не должно быть пустым")
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Range(val min: Int, val max: Int)
|
||||
|
||||
data class Form(
|
||||
@NotBlank val name: String,
|
||||
@NotBlank("email обязателен") val email: String,
|
||||
@Range(18, 120) val age: Int,
|
||||
)
|
||||
|
||||
fun validate(obj: Any): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
for (p in obj::class.memberProperties) {
|
||||
p.isAccessible = true
|
||||
val value = p.call(obj)
|
||||
p.findAnnotation<NotBlank>()?.let { rule ->
|
||||
if ((value as? String).isNullOrBlank()) errors += "${p.name}: ${rule.message}"
|
||||
}
|
||||
p.findAnnotation<Range>()?.let { rule ->
|
||||
val number = value as? Int ?: return@let
|
||||
if (number !in rule.min..rule.max) {
|
||||
errors += "${p.name}: должно быть от ${rule.min} до ${rule.max}"
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.sorted()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(validate(Form("Иван", "ivan@example.com", 30)))
|
||||
println(validate(Form("", "", 5)))
|
||||
}
|
||||
```
|
||||
```
|
||||
[]
|
||||
[age: должно быть от 18 до 120, email: email обязателен, name: не должно быть пустым]
|
||||
```
|
||||
Это рабочая схема, но она платит рефлексией в рантайме: разбор метаданных при первом обращении и потеря проверок на этапе компиляции. Тот же результат даёт KSP-процессор, генерирующий код валидации при сборке. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
|
||||
## Аннотации компилятора
|
||||
Часть аннотаций обрабатывает сам компилятор - они меняют генерируемый код или проверки.
|
||||
|
||||
| Аннотация | Что делает |
|
||||
|---|---|
|
||||
| `@Deprecated` | предупреждение или ошибка при использовании |
|
||||
| `@Suppress("NAME")` | подавляет предупреждение в области применения |
|
||||
| `@JvmName` | имя в байт-коде |
|
||||
| `@JvmStatic` | статический метод для членов `companion object` |
|
||||
| `@JvmOverloads` | перегрузки для параметров со значениями по умолчанию |
|
||||
| `@JvmField` | публичное поле вместо геттера |
|
||||
| `@JvmInline` | обязательна для `value class` |
|
||||
| `@Throws` | добавляет `throws` в сигнатуру для Java |
|
||||
| `@OptIn`, `@RequiresOptIn` | механизм согласия на нестабильный API |
|
||||
| `@PublishedApi` | доступ к `internal` из `public inline` |
|
||||
| `@Volatile`, `@Synchronized` | то же, что в Java |
|
||||
| `@Transient` | исключение из сериализации |
|
||||
| `@DslMarker` | ограничение области в DSL |
|
||||
| `@BuilderInference` | вывод типа в билдерах |
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Deprecated("используйте new()", ReplaceWith("new()"), DeprecationLevel.WARNING)
|
||||
fun old() = 1
|
||||
|
||||
fun new() = 2
|
||||
|
||||
fun main() {
|
||||
val d = ::old.findAnnotation<Deprecated>()
|
||||
println("message=${d?.message}")
|
||||
println("level=${d?.level}")
|
||||
println("replaceWith=${d?.replaceWith?.expression}")
|
||||
}
|
||||
```
|
||||
```
|
||||
message=используйте new()
|
||||
level=WARNING
|
||||
replaceWith=new()
|
||||
```
|
||||
`ReplaceWith` понимает IDE: она предлагает автозамену вызова. `DeprecationLevel.ERROR` делает использование ошибкой компиляции, `HIDDEN` убирает объявление из автодополнения, сохраняя бинарную совместимость.
|
||||
|
||||
Аннотации интеропа меняют байт-код заметно:
|
||||
```kotlin
|
||||
@file:JvmName("InteropUtils")
|
||||
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
class Interop {
|
||||
@Throws(IOException::class)
|
||||
fun mayThrow() = Unit
|
||||
|
||||
@JvmField
|
||||
val field = 1
|
||||
|
||||
@get:JvmName("fetchValue")
|
||||
val value = 2
|
||||
|
||||
@JvmOverloads
|
||||
fun withDefaults(a: Int, b: Int = 1, c: Int = 2) = a + b + c
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun staticFn() = 3
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = Interop::class.java
|
||||
println("методы=${c.declaredMethods.map { it.name }.sorted()}")
|
||||
println("поля=${c.declaredFields.map { it.name }.sorted()}")
|
||||
println("throws=${c.getDeclaredMethod("mayThrow").exceptionTypes.map { it.simpleName }}")
|
||||
println("статические=${c.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.map { it.name }.sorted()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
методы=[fetchValue, mayThrow, staticFn, withDefaults, withDefaults, withDefaults, withDefaults$default]
|
||||
поля=[Companion, field, value]
|
||||
throws=[IOException]
|
||||
статические=[staticFn, withDefaults$default]
|
||||
```
|
||||
`@JvmOverloads` дал три перегрузки `withDefaults`, `@get:JvmName` переименовал геттер в `fetchValue`, `@JvmField` превратил свойство в поле без геттера. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
## Аннотации для KSP
|
||||
Аннотация, предназначенная процессору, не нужна в рантайме:
|
||||
```kotlin
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class GenerateBuilder
|
||||
|
||||
@GenerateBuilder
|
||||
data class Person(val name: String, val age: Int)
|
||||
|
||||
fun main() {
|
||||
println("в рантайме=${Person::class.annotations.size}")
|
||||
}
|
||||
```
|
||||
```
|
||||
в рантайме=0
|
||||
```
|
||||
`SOURCE` убирает аннотацию из байт-кода полностью - KSP читает исходники, а не классы. Это уменьшает размер артефакта и исключает случайное чтение рефлексией. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `@interface Foo { String value(); }` | `annotation class Foo(val value: String)` |
|
||||
> | `String value() default "x"` | `val value: String = "x"` |
|
||||
> | retention по умолчанию `CLASS` | по умолчанию `RUNTIME` |
|
||||
> | `@Target(ElementType.FIELD)` | `@Target(AnnotationTarget.FIELD)` |
|
||||
> | нет понятия свойства | `@property:`, `@field:`, `@get:`, `@set:` |
|
||||
> | `@Repeatable(Container.class)` + контейнер | просто `@Repeatable` |
|
||||
> | `Foo.class` в параметре | `Foo::class` |
|
||||
> | `{1, 2}` для массива | `[1, 2]` |
|
||||
> | `getAnnotation(Foo.class)` | `findAnnotation<Foo>()` |
|
||||
> | аннотации на файл невозможны | `@file:JvmName(...)` |
|
||||
>
|
||||
> **Use-site targets существуют потому, что свойство - не поле.** В Java `@Column private String name` однозначна: цель одна. В Kotlin `val name: String` порождает поле, геттер и Kotlin-свойство как отдельную сущность в метаданных, а в конструкторе ещё и параметр - аннотация должна выбрать одну цель. По умолчанию выбирается первая применимая в порядке `param` → `property` → `field`, и это главная причина, по которой Java-библиотеки «не видят» аннотацию: Jackson читает поле, а аннотация оказалась на параметре. Префикс `@field:` или новый `@all:` решают проблему. Подробнее: [Свойства](../ООП/Свойства.md)
|
||||
>
|
||||
> **Retention по умолчанию - `RUNTIME`, а не `CLASS`.** Java по умолчанию не включает аннотацию в рантайм, поэтому каждая библиотека обязана ставить `@Retention(RUNTIME)` явно. Kotlin выбрал обратный дефолт - аннотации чаще читают рефлексией, чем процессором. Обратная сторона: аннотация для линтера без явного `SOURCE` попадёт и в байт-код, и в рантайм, увеличивая артефакт.
|
||||
>
|
||||
> **Повторяемость не требует класса-контейнера.** В Java `@Repeatable` обязывает объявить отдельную аннотацию-контейнер с массивом - лишний код, который приходится писать для каждой повторяемой аннотации. Kotlin генерирует контейнер сам; `findAnnotations<T>()` возвращает список, и раскрывать контейнер вручную не нужно.
|
||||
>
|
||||
> **Аннотации на файл - новая цель, которой в Java нет.** `@file:JvmName("Utils")` меняет имя класса-контейнера для функций верхнего уровня, `@file:Suppress` подавляет предупреждения во всём файле. В Java единица объявления - всегда класс, поэтому и цели `FILE` быть не может. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
|
||||
>
|
||||
> **Генерация кода вместо рефлексии - предпочтительный путь.** В Java-экосистеме аннотации почти всегда читаются рефлексией: Spring, Jackson, Hibernate строят модель при старте приложения. Kotlin подталкивает к другому: `kotlinx.serialization` и KSP обрабатывают аннотации при компиляции и генерируют код, поэтому `kotlin-reflect` не нужен, старт быстрее, а ошибки видны при сборке. Для Kotlin/Native и Kotlin/JS это единственный работающий вариант. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
>
|
||||
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Аннотация без префикса может не попасть туда, куда нужно Java-библиотеке.** Проверено: аннотация с целями `VALUE_PARAMETER` и `PROPERTY` на `val` в конструкторе попадает на параметр и Kotlin-свойство, но **не на поле** - Jackson, Hibernate и JPA её не увидят. Нужен `@field:` или `@all:`.
|
||||
>
|
||||
> **`@property:` видна только Kotlin-рефлексии.** В Java-представлении такой аннотации нет вообще: она хранится в метаданных Kotlin. Библиотека на Java Reflection её не найдёт никогда.
|
||||
>
|
||||
> **Retention по умолчанию `RUNTIME` раздувает байт-код.** Аннотации для KSP, линтеров и внутренних проверок стоит помечать `@Retention(AnnotationRetention.SOURCE)` - иначе они остаются в классах и видны рефлексии без необходимости.
|
||||
>
|
||||
> **`BINARY` не видна рефлексии, хотя лежит в байт-коде.** Проверено: из `@Src @Bin @Run` рефлексия вернёт только `Run`. Это частая причина «аннотация есть в коде, а `findAnnotation` даёт `null`».
|
||||
>
|
||||
> **`null` в качестве значения аннотации невозможен.** Типы параметров не могут быть nullable, поэтому «отсутствие значения» кодируют пустой строкой или сигнальной константой. Это ограничение формата аннотаций в JVM, а не Kotlin.
|
||||
>
|
||||
> **Параметры аннотаций ограничены по типам.** `data class`, коллекции, `Duration` и функции использовать нельзя - только примитивы, `String`, `KClass`, `enum`, другая аннотация и массивы из них. Попытка передать `List<String>` вместо `Array<String>` не скомпилируется.
|
||||
>
|
||||
> **`KClass` в параметре, а не `Class`.** `@Validators([NotBlank::class])` - правильно, `NotBlank::class.java` в аннотации не примут. При чтении обратно получается `KClass`, и для Java-API нужен `.java`.
|
||||
>
|
||||
> **`@JvmStatic` работает только в `companion object` и `object`.** На обычном методе класса даёт ошибку компиляции - статических методов у Kotlin-классов не бывает. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
>
|
||||
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `f(a, b = 1, c = 2)` появятся `f(a)`, `f(a, b)`, `f(a, b, c)` - вызвать `f(a, c)` из Java не получится. Проверено на реальном байт-коде.
|
||||
>
|
||||
> **`@Throws` обязательна для Java-вызывающих.** Kotlin не имеет проверяемых исключений, поэтому без `@Throws` Java-код не сможет написать `catch (IOException e)` - компилятор Java сообщит, что исключение не бросается. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
>
|
||||
> **Аннотации на типах требуют явной цели `TYPE`.** `List<@Marker String>` не скомпилируется, если в `@Target` нет `AnnotationTarget.TYPE` - по умолчанию эта цель не включена.
|
||||
>
|
||||
> **`@Target(AnnotationTarget.EXPRESSION)` работает только с `SOURCE`.** Выражения не существуют в байт-коде, поэтому `RUNTIME` с такой целью даст ошибку компиляции.
|
||||
>
|
||||
> **`@all:` - новая возможность и ломает сборку на старых версиях Kotlin.** В 2.4 работает без флагов, в более ранних версиях требует `-Xannotation-target-all` или недоступна вовсе. Для библиотек с широкой совместимостью пока надёжнее перечислять цели вручную.
|
||||
>
|
||||
> **Чтение аннотаций рефлексией стоит дорого при первом обращении.** Это тот же разбор метаданных, что и у остальной рефлексии: ~150 мс на класс впервые. Кешируйте результат, а лучше переносите обработку в KSP. Подробнее: [Рефлексия](Рефлексия.md)
|
||||
>
|
||||
> **Обфускация ломает аннотации с именами в строках.** `@Column("user_id")` переживёт R8, а вот поиск свойства по имени из аннотации - нет: само свойство переименуется. Нужны правила `-keep`.
|
||||
|
||||
Далее: [Kotlin](../Kotlin.md)
|
||||
@@ -1,505 +0,0 @@
|
||||
---
|
||||
title: Рефлексия
|
||||
description: KClass, KProperty, KFunction, kotlin-reflect, ссылки на члены, интероп с Java Reflection
|
||||
tags:
|
||||
- kotlin
|
||||
- reflection
|
||||
---
|
||||
|
||||
Рефлексия - доступ к структуре программы во время выполнения: какие у класса свойства, какие параметры у функции, как вызвать то, что не известно на этапе компиляции. Kotlin даёт собственный API (`kotlin.reflect`) поверх Java Reflection, потому что в байт-коде не видно ни свойств, ни `suspend`, ни значений по умолчанию.
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
data class User(val id: Int, var name: String)
|
||||
|
||||
fun main() {
|
||||
val user = User(1, "Иван")
|
||||
User::class.memberProperties
|
||||
.sortedBy { it.name }
|
||||
.forEach { println("${it.name} = ${it.call(user)}") }
|
||||
}
|
||||
```
|
||||
```
|
||||
id = 1
|
||||
name = Иван
|
||||
```
|
||||
|
||||
## Два уровня API
|
||||
Kotlin-рефлексия разделена на две части, и это важно практически.
|
||||
|
||||
```kotlin
|
||||
// Доступно всегда, только kotlin-stdlib
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
enum class Status { OK }
|
||||
data class P(val x: Int)
|
||||
|
||||
fun main() {
|
||||
println(Status::class.simpleName)
|
||||
println(P::class.qualifiedName)
|
||||
println(P::class.java.name)
|
||||
println(P::class.isInstance(P(1)))
|
||||
}
|
||||
```
|
||||
```
|
||||
Status
|
||||
P
|
||||
P
|
||||
true
|
||||
```
|
||||
Без `kotlin-reflect` в classpath работают только `simpleName`, `qualifiedName`, `isInstance`, `java` и несколько похожих свойств. Всё остальное - `memberProperties`, `primaryConstructor`, `sealedSubclasses` - бросает `KotlinReflectionNotSupportedError`:
|
||||
```
|
||||
Exception in thread "main" kotlin.jvm.KotlinReflectionNotSupportedError:
|
||||
Kotlin reflection implementation is not found at runtime.
|
||||
Make sure you have kotlin-reflect.jar in the classpath
|
||||
```
|
||||
|
||||
Подключается отдельно:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation(kotlin("reflect"))
|
||||
}
|
||||
```
|
||||
`kotlin-reflect` весит около 3 МБ и тянет собственный парсер метаданных - для Android и serverless это значимо. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
## KClass
|
||||
```kotlin
|
||||
import kotlin.reflect.full.companionObject
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
data class User(val id: Int, var name: String) {
|
||||
val display: String get() = "#$id $name"
|
||||
fun greet(greeting: String): String = "$greeting, $name"
|
||||
companion object { const val TABLE = "users" }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val kc = User::class
|
||||
println("simpleName=${kc.simpleName}")
|
||||
println("isData=${kc.isData}")
|
||||
println("isFinal=${kc.isFinal}")
|
||||
println("visibility=${kc.visibility}")
|
||||
println("primary params=${kc.primaryConstructor!!.parameters.map { it.name }}")
|
||||
println("memberProperties=${kc.memberProperties.map { it.name }.sorted()}")
|
||||
println("функции=${kc.declaredMemberFunctions.map { it.name }.sorted()}")
|
||||
println("companionObject=${kc.companionObject?.simpleName}")
|
||||
}
|
||||
```
|
||||
```
|
||||
simpleName=User
|
||||
isData=true
|
||||
isFinal=true
|
||||
visibility=PUBLIC
|
||||
primary params=[id, name]
|
||||
memberProperties=[display, id, name]
|
||||
функции=[component1, component2, copy, equals, greet, hashCode, toString]
|
||||
companionObject=Companion
|
||||
```
|
||||
Видно, что `data class` генерирует `componentN`, `copy`, `equals`, `hashCode`, `toString` - рефлексия показывает их наравне с написанными руками. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
| Свойство `KClass` | Что даёт |
|
||||
|---|---|
|
||||
| `simpleName`, `qualifiedName` | имена; `null` для локальных и анонимных |
|
||||
| `isData`, `isSealed`, `isInner`, `isCompanion`, `isValue`, `isFun` | характер класса |
|
||||
| `isAbstract`, `isFinal`, `isOpen` | модальность |
|
||||
| `visibility` | `PUBLIC`, `INTERNAL`, `PROTECTED`, `PRIVATE` |
|
||||
| `constructors`, `primaryConstructor` | конструкторы |
|
||||
| `memberProperties`, `declaredMemberProperties` | свойства с унаследованными и без |
|
||||
| `memberFunctions`, `declaredMemberFunctions` | функции |
|
||||
| `supertypes`, `superclasses` | иерархия |
|
||||
| `sealedSubclasses` | подклассы `sealed`-типа |
|
||||
| `nestedClasses` | вложенные классы |
|
||||
| `objectInstance` | экземпляр `object`, иначе `null` |
|
||||
| `companionObject`, `companionObjectInstance` | компаньон |
|
||||
| `typeParameters` | параметры типа |
|
||||
| `java` | мост к `Class<T>` |
|
||||
|
||||
### sealed, enum, object
|
||||
```kotlin
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
|
||||
sealed interface Shape {
|
||||
data class Circle(val r: Double) : Shape
|
||||
data class Square(val a: Double) : Shape
|
||||
}
|
||||
|
||||
enum class Status { OK, FAIL }
|
||||
|
||||
object Single { val x = 1 }
|
||||
|
||||
open class Base
|
||||
class Derived : Base()
|
||||
|
||||
fun main() {
|
||||
println("sealedSubclasses=${Shape::class.sealedSubclasses.map { it.simpleName }}")
|
||||
println("enum values=${Status::class.java.enumConstants.toList()}")
|
||||
println("object instance=${Single::class.objectInstance?.x}")
|
||||
println("supertypes=${Derived::class.supertypes}")
|
||||
println("isSubclassOf=${Derived::class.isSubclassOf(Base::class)}")
|
||||
}
|
||||
```
|
||||
```
|
||||
sealedSubclasses=[Circle, Square]
|
||||
enum values=[OK, FAIL]
|
||||
object instance=1
|
||||
supertypes=[Base]
|
||||
isSubclassOf=true
|
||||
```
|
||||
`sealedSubclasses` даёт полный список вариантов - это основа для сериализаторов и фабрик по типу. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Ссылки на члены
|
||||
```kotlin
|
||||
import kotlin.reflect.KFunction2
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
|
||||
data class User(val id: Int, var name: String) {
|
||||
fun greet(greeting: String): String = "$greeting, $name"
|
||||
}
|
||||
|
||||
fun topLevel(a: Int, b: String = "по умолчанию"): String = "$a-$b"
|
||||
|
||||
fun main() {
|
||||
val user = User(1, "Иван")
|
||||
|
||||
// свойство как объект
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val nameProp = User::class.memberProperties
|
||||
.first { it.name == "name" } as KMutableProperty1<User, String>
|
||||
println("get=${nameProp.get(user)}")
|
||||
nameProp.set(user, "Пётр")
|
||||
println("после set=${user.name}")
|
||||
|
||||
// функция класса
|
||||
val greet = User::class.declaredMemberFunctions.first { it.name == "greet" }
|
||||
println("call=${greet.call(user, "Привет")}")
|
||||
|
||||
// ссылка на функцию верхнего уровня
|
||||
val ref: KFunction2<Int, String, String> = ::topLevel
|
||||
println("name=${ref.name}, параметров=${ref.parameters.size}")
|
||||
println("call=${ref.call(1, "b")}")
|
||||
println("callBy с дефолтом=${ref.callBy(mapOf(ref.parameters[0] to 5))}")
|
||||
}
|
||||
```
|
||||
```
|
||||
get=Иван
|
||||
после set=Пётр
|
||||
call=Привет, Пётр
|
||||
name=topLevel, параметров=2
|
||||
call=1-b
|
||||
callBy с дефолтом=5-по умолчанию
|
||||
```
|
||||
| Тип | Что представляет |
|
||||
|---|---|
|
||||
| `KProperty0<V>` | свойство без получателя: `::topLevelVal`, `obj::prop` |
|
||||
| `KProperty1<T, V>` | свойство класса: `User::name` |
|
||||
| `KProperty2<T, R, V>` | свойство-расширение с двумя получателями |
|
||||
| `KMutableProperty1<T, V>` | то же для `var` - добавляет `set` |
|
||||
| `KFunction<R>` | функция; `KFunction1`, `KFunction2`, ... по арности |
|
||||
| `KParameter` | параметр: `name`, `type`, `isOptional`, `kind` |
|
||||
| `KType` | тип с аргументами и `isMarkedNullable` |
|
||||
|
||||
`call` и `callBy` различаются принципиально: `call` требует все аргументы по порядку, `callBy` принимает `Map<KParameter, Any?>` и **учитывает значения по умолчанию** для отсутствующих. Значения по умолчанию в байт-коде не видны, поэтому через Java Reflection `callBy` не воспроизвести. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
|
||||
|
||||
### Приватные члены
|
||||
```kotlin
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
class Hidden {
|
||||
private val secret = 42
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val prop = Hidden::class.declaredMemberProperties.first { it.name == "secret" }
|
||||
|
||||
val error = runCatching { prop.call(Hidden()) }.exceptionOrNull()
|
||||
println("без isAccessible: ${error!!::class.simpleName}")
|
||||
|
||||
prop.isAccessible = true
|
||||
println("с isAccessible: ${prop.call(Hidden())}")
|
||||
}
|
||||
```
|
||||
```
|
||||
без isAccessible: IllegalCallableAccessException
|
||||
с isAccessible: 42
|
||||
```
|
||||
`isAccessible = true` - это Kotlin-обёртка над `setAccessible` из Java. На JDK 17+ она может упираться в строгую инкапсуляцию модулей для классов стандартной библиотеки; для собственного кода работает всегда.
|
||||
|
||||
## Типы
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
class G {
|
||||
val field: List<Map<String, Int>> = emptyList()
|
||||
}
|
||||
|
||||
fun nullableParam(x: String?) = x
|
||||
|
||||
fun main() {
|
||||
val t = typeOf<List<Map<String, Int>>>()
|
||||
println("typeOf=$t")
|
||||
println("classifier=${t.classifier}")
|
||||
println("аргументы=${t.arguments}")
|
||||
|
||||
val prop = G::class.memberProperties.first()
|
||||
println("тип свойства=${prop.returnType}")
|
||||
println("вложенный аргумент=${prop.returnType.arguments[0].type}")
|
||||
|
||||
val p = ::nullableParam.parameters[0]
|
||||
println("nullable=${p.type.isMarkedNullable}")
|
||||
}
|
||||
```
|
||||
```
|
||||
typeOf=kotlin.collections.List<kotlin.collections.Map<kotlin.String, kotlin.Int>>
|
||||
classifier=class kotlin.collections.List
|
||||
аргументы=[kotlin.collections.Map<kotlin.String, kotlin.Int>]
|
||||
тип свойства=kotlin.collections.List<kotlin.collections.Map<kotlin.String, kotlin.Int>>
|
||||
вложенный аргумент=kotlin.collections.Map<kotlin.String, kotlin.Int>
|
||||
nullable=true
|
||||
```
|
||||
`KType` сохраняет аргументы типов и признак nullable - эта информация берётся из метаданных Kotlin, а не из байт-кода. Но это **статическая** информация: тип объявления, а не фактический тип значения.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list: List<String> = listOf("a")
|
||||
println("класс=${list::class.simpleName}")
|
||||
println("параметры типа=${list::class.typeParameters.map { it.name }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
класс=SingletonList
|
||||
параметры типа=[E]
|
||||
```
|
||||
У значения узнать аргумент типа нельзя - дженерики стираются. `list::class` даёт лишь имя параметра `E`, а не `String`. Для сохранения типа нужен `reified` на этапе компиляции. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## Делегаты
|
||||
```kotlin
|
||||
import kotlin.reflect.KProperty0
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
class Lazily {
|
||||
val value: String by lazy { "вычислено" }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val obj = Lazily()
|
||||
val prop: KProperty0<String> = obj::value
|
||||
prop.isAccessible = true
|
||||
|
||||
println("делегат до чтения=${prop.getDelegate()}")
|
||||
println("значение=${obj.value}")
|
||||
println("делегат после=${prop.getDelegate()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
делегат до чтения=Lazy value not initialized yet.
|
||||
значение=вычислено
|
||||
делегат после=вычислено
|
||||
```
|
||||
`getDelegate()` возвращает сам объект-делегат, а не значение свойства - для `by lazy` это `Lazy<T>`, чей `toString` до инициализации сообщает «not initialized yet». Это единственный способ узнать состояние ленивого свойства, не вычисляя его. Подробнее: [Делегирование](../ООП/Делегирование.md)
|
||||
|
||||
## Интероп с Java Reflection
|
||||
```kotlin
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
class Target {
|
||||
lateinit var late: String
|
||||
val lazyProp: Int by lazy { 7 }
|
||||
}
|
||||
|
||||
fun topLevel(a: Int): Int = a
|
||||
|
||||
fun main() {
|
||||
println("KClass -> Class=${Point::class.java.name}")
|
||||
println("Class -> KClass=${Point::class.java.kotlin.simpleName}")
|
||||
println("javaMethod=${::topLevel.javaMethod?.name}")
|
||||
println("Method -> KFunction=${::topLevel.javaMethod?.kotlinFunction?.name}")
|
||||
println("javaConstructor=${Point::class.primaryConstructor?.javaConstructor}")
|
||||
|
||||
val late = Target::class.memberProperties.first { it.name == "late" }
|
||||
println("lateinit isLateinit=${late.isLateinit}, javaField=${late.javaField?.name}")
|
||||
|
||||
val lz = Target::class.memberProperties.first { it.name == "lazyProp" }
|
||||
println("lazy javaField=${lz.javaField?.name}")
|
||||
}
|
||||
```
|
||||
```
|
||||
KClass -> Class=Point
|
||||
Class -> KClass=Point
|
||||
javaMethod=topLevel
|
||||
Method -> KFunction=topLevel
|
||||
javaConstructor=public Point(int,int)
|
||||
lateinit isLateinit=true, javaField=late
|
||||
lazy javaField=lazyProp$delegate
|
||||
```
|
||||
| Переход | Как |
|
||||
|---|---|
|
||||
| `KClass` → `Class` | `kc.java` |
|
||||
| `Class` → `KClass` | `cls.kotlin` |
|
||||
| `KFunction` → `Method` | `fn.javaMethod` |
|
||||
| `KFunction` → `Constructor` | `fn.javaConstructor` |
|
||||
| `KProperty` → `Field` | `prop.javaField` |
|
||||
| `KProperty` → геттер/сеттер | `prop.javaGetter`, `prop.javaSetter` |
|
||||
| `Method` → `KFunction` | `method.kotlinFunction` |
|
||||
| `Field` → `KProperty` | `field.kotlinProperty` |
|
||||
|
||||
Поле делегированного свойства называется `имя$delegate` - это важно для библиотек, которые ищут поля по имени. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
### Что Java Reflection не видит
|
||||
```kotlin
|
||||
class Visibility {
|
||||
private val hidden = 1
|
||||
internal val internalProp = 2
|
||||
val open = 3
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println("java методы=${Visibility::class.java.declaredMethods.map { it.name }.sorted()}")
|
||||
println("java поля=${Visibility::class.java.declaredFields.map { it.name }.sorted()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
java методы=[getInternalProp$main, getOpen]
|
||||
java поля=[hidden, internalProp, open]
|
||||
```
|
||||
Через Java API видны только геттеры (`private val` вообще не имеет метода), а `internal` превращается в `public` с манглированным именем `getInternalProp$main`. Kotlin-рефлексия показывает исходную картину: три свойства с видимостями `PRIVATE`, `INTERNAL`, `PUBLIC`.
|
||||
|
||||
## Практический пример: маппинг
|
||||
```kotlin
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
data class Config(val host: String, val port: Int, val debug: Boolean)
|
||||
|
||||
fun toMap(obj: Any): Map<String, Any?> =
|
||||
obj::class.memberProperties.associate { it.name to it.call(obj) }
|
||||
|
||||
fun <T : Any> fromMap(kClass: KClass<T>, map: Map<String, Any?>): T {
|
||||
val ctor = requireNonNull(kClass.primaryConstructor)
|
||||
val args = ctor.parameters
|
||||
.filter { map.containsKey(it.name) || !it.isOptional }
|
||||
.associateWith { map[it.name] }
|
||||
return ctor.callBy(args)
|
||||
}
|
||||
|
||||
private fun <T> requireNonNull(value: T?): T =
|
||||
requireNotNull(value) { "у класса нет первичного конструктора" }
|
||||
|
||||
fun main() {
|
||||
val config = Config("localhost", 8080, true)
|
||||
println(toMap(config).toSortedMap())
|
||||
|
||||
val restored = fromMap(
|
||||
Config::class,
|
||||
mapOf("host" to "example.com", "port" to 443, "debug" to false),
|
||||
)
|
||||
println("восстановлено=$restored")
|
||||
}
|
||||
```
|
||||
```
|
||||
{debug=true, host=localhost, port=8080}
|
||||
восстановлено=Config(host=example.com, port=443, debug=false)
|
||||
```
|
||||
Фильтр по `isOptional` позволяет не передавать параметры со значениями по умолчанию - этого достаточно для простого маппера конфигурации. Для сериализации в продакшене лучше взять `kotlinx.serialization`: она генерирует код на этапе компиляции и работает без `kotlin-reflect`.
|
||||
|
||||
## Производительность
|
||||
Измерено на 200 000 обращений к свойству `data class`:
|
||||
```
|
||||
прямой доступ мс=2
|
||||
через рефлексию мс=9
|
||||
замедление раз=3
|
||||
```
|
||||
Само обращение медленнее в несколько раз - для большинства задач приемлемо. Дороже стоит **первое** обращение к метаданным:
|
||||
```
|
||||
первое обращение к memberProperties мс=158
|
||||
второе обращение мкс=16
|
||||
```
|
||||
158 миллисекунд против 16 микросекунд: `kotlin-reflect` при первом запросе разбирает аннотацию `@Metadata` класса и строит модель. Отсюда простое правило: результаты `memberProperties` и `primaryConstructor` нужно кешировать, а не запрашивать в цикле.
|
||||
|
||||
Для многих задач рефлексия вообще не нужна:
|
||||
| Задача | Альтернатива |
|
||||
|---|---|
|
||||
| сериализация | `kotlinx.serialization` - генерация в компиляции |
|
||||
| DI | ручная сборка или KSP-генерация |
|
||||
| валидация по аннотациям | KSP-процессор |
|
||||
| копирование с изменением | `copy()` у `data class` |
|
||||
| вызов по имени | `Map<String, () -> Unit>` |
|
||||
| проверка типа | `is` и `when` по `sealed` |
|
||||
|
||||
Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java Reflection | kotlin.reflect |
|
||||
> |---|---|
|
||||
> | `Class<T>` | `KClass<T>` |
|
||||
> | `Field` + `Method` раздельно | `KProperty` как единое целое |
|
||||
> | `Method` | `KFunction` |
|
||||
> | в JDK, доступна всегда | отдельная зависимость `kotlin-reflect` |
|
||||
> | значения по умолчанию не видны | `callBy` их учитывает |
|
||||
> | nullability не видна | `KType.isMarkedNullable` |
|
||||
> | аргументы типов стёрты | `KType.arguments` из метаданных |
|
||||
> | `setAccessible(true)` | `isAccessible = true` |
|
||||
> | `getSuperclass()` | `supertypes`, `superclasses` |
|
||||
> | подклассы `sealed` не перечислить | `sealedSubclasses` |
|
||||
>
|
||||
> **Свойство - первичная сущность, а не пара «поле + методы».** В Java `private String name` с геттером и сеттером - это `Field` и два `Method`, никак формально не связанные: код, обходящий бин, вынужден угадывать соответствие по именам (`getName` → `name`) и спотыкаться на `isActive` для `boolean`. `KProperty` представляет свойство целиком, с `get`, `set` (у `KMutableProperty`), типом и видимостью; переходы к Java-представлению есть, но нужны редко. Подробнее: [Свойства](../ООП/Свойства.md)
|
||||
>
|
||||
> **Метаданные Kotlin хранят то, чего в байт-коде нет.** Компилятор пишет в класс аннотацию `@Metadata` с описанием исходной структуры: nullability, аргументы типов, `suspend`, значения по умолчанию, `internal`-видимость, имена параметров. Java Reflection этого не видит и показывает результат компиляции - `getInternalProp$main` вместо `internal val internalProp`, `Object f(int, Continuation)` вместо `suspend fun f(x: Int): String`. Цена - 3 МБ `kotlin-reflect` и разбор метаданных при первом обращении. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
>
|
||||
> **Рефлексия не входит в стандартную библиотеку, и это осознанное решение.** В Java рефлексия доступна в любой программе - её используют Spring, Jackson, Hibernate, и платить за неё приходится всем. Kotlin целится и в Android с Kotlin/Native, где 3 МБ и динамический разбор метаданных неприемлемы, поэтому полная рефлексия вынесена в отдельный артефакт и на Native отсутствует вовсе. Это подталкивает к генерации кода: `kotlinx.serialization` и KSP решают те же задачи в компиляции. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
>
|
||||
> **`callBy` возможен только благодаря метаданным.** Значения по умолчанию в Java-байт-коде превращаются в набор перегрузок или в синтетический метод с битовой маской - восстановить «вызови конструктор, остальное по умолчанию» через `Constructor.newInstance` нельзя. `callBy` принимает карту известных параметров и подставляет дефолты сам, что делает рефлективное создание объектов из `Map` практичным.
|
||||
>
|
||||
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Без `kotlin-reflect` в classpath большая часть API падает в рантайме.** `KotlinReflectionNotSupportedError` возникает при обращении к `memberProperties`, `primaryConstructor`, `sealedSubclasses` - компилятор ошибки не покажет, потому что объявления берутся из stdlib. Добавляйте `implementation(kotlin("reflect"))`.
|
||||
>
|
||||
> **Первое обращение к метаданным класса стоит ~150 мс.** Измерено: `memberProperties` впервые - 158 мс, повторно - 16 мкс. В цикле или на горячем пути это катастрофа. Кешируйте `KClass`-производные в `val` на уровне объекта или в `Map`.
|
||||
>
|
||||
> **`memberProperties` включает унаследованные, `declaredMemberProperties` - нет.** Путаница этих двух даёт либо лишние поля родителя при сериализации, либо потерю унаследованных. Выбирайте осознанно.
|
||||
>
|
||||
> **Доступ к `private` без `isAccessible = true` бросает `IllegalCallableAccessException`.** Это отдельный тип, не `IllegalAccessException` из Java - обработчики, ловящие только Java-исключение, его пропустят.
|
||||
>
|
||||
> **`isAccessible = true` может не сработать на JDK 17+.** Строгая инкапсуляция модулей блокирует доступ к внутренностям JDK и закрытых модулей; для своего кода ограничений нет, для чужих библиотек может потребоваться `--add-opens`.
|
||||
>
|
||||
> **У локальных и анонимных классов `qualifiedName` равен `null`.** У анонимного объекта `simpleName` тоже `null` - проверено. Код вида `kc.qualifiedName!!` падает именно на них, причём обычно в тестах, где анонимные объекты используются чаще.
|
||||
>
|
||||
> **Дженерики стираются, и фактический аргумент типа у значения недоступен.** `list::class.typeParameters` даёт `[E]`, а не `[String]`. Сохранить тип можно только через `reified` в момент компиляции или передавая `KType` параметром. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
>
|
||||
> **`KType` отражает объявленный тип, а не фактический.** У свойства типа `Any`, в котором лежит `String`, `returnType` останется `kotlin.Any` - для определения фактического нужен `value::class`.
|
||||
>
|
||||
> **`call` игнорирует значения по умолчанию, `callBy` - нет.** `ctor.call("a")` на конструкторе с тремя параметрами бросит `IllegalArgumentException` даже если два имеют дефолты. Для частичных аргументов нужен `callBy` с `Map<KParameter, Any?>`.
|
||||
>
|
||||
> **`callBy` с `null` для необязательного параметра передаст именно `null`, а не дефолт.** Ключ, присутствующий в карте, считается заданным; чтобы использовать значение по умолчанию, параметр нужно **не включать** в карту вовсе.
|
||||
>
|
||||
> **Поле делегированного свойства называется `имя$delegate`.** `javaField?.name` для `val x by lazy { }` даст `x$delegate`, а не `x` - код, ищущий поля по имени свойства, их не найдёт.
|
||||
>
|
||||
> **`getDelegate()` требует `isAccessible = true` и возвращает делегат, а не значение.** Для `by lazy` это `Lazy<T>`, чей `toString` до инициализации - «Lazy value not initialized yet.»; попытка привести его к типу значения даст `ClassCastException`.
|
||||
>
|
||||
> **`internal` в Java-представлении становится `public` с манглированным именем.** `getInternalProp$main` - проверено; обход через `Class.getDeclaredMethods` покажет именно это имя, а не `getInternalProp`.
|
||||
>
|
||||
> **`objectInstance` равен `null` для обычных классов.** Для проверки «это `object`?» нужен `kc.objectInstance != null`, а не наличие свойства.
|
||||
>
|
||||
> **Рефлексия обходит проверки null-безопасности.** `prop.set(obj, null)` через `KMutableProperty` с типом `String` запишет `null` в непустое свойство: компилятор не участвует, а проверки `Intrinsics` стоят только на входе в функции. Дальше код упадёт NPE в неожиданном месте. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
|
||||
>
|
||||
> **Рефлексия ломается при обфускации.** R8 и ProGuard переименовывают классы и члены; код, ищущий свойство по строковому имени, перестаёт работать в релизной сборке и работает в отладочной. Нужны правила `-keep`.
|
||||
>
|
||||
> **Kotlin/Native и Kotlin/JS не поддерживают полную рефлексию.** `kotlin-reflect` существует только для JVM; в Multiplatform-коде доступны лишь `::class.simpleName` и подобное. Общий код на рефлексии не построить.
|
||||
|
||||
Далее: [Аннотации](Аннотации.md)
|
||||
@@ -1,185 +0,0 @@
|
||||
---
|
||||
title: Null-безопасность
|
||||
description: Разделение типов на nullable и non-null, как компилятор предотвращает NPE
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
---
|
||||
|
||||
Главная особенность системы типов Kotlin: **возможность быть null - часть типа**.
|
||||
|
||||
```kotlin
|
||||
var a: String = "text"
|
||||
// a = null // ошибка компиляции
|
||||
|
||||
var b: String? = "text"
|
||||
b = null // ok
|
||||
```
|
||||
|
||||
`String` и `String?` - разные типы. `String` является подтипом `String?`, но не наоборот.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
SN["String? — допускает null"]
|
||||
S["String — не допускает null"]
|
||||
N["Nothing? — только null"]
|
||||
SN --> S
|
||||
SN --> N
|
||||
```
|
||||
|
||||
## Зачем это нужно
|
||||
`NullPointerException` в Java возникает в рантайме, иногда далеко от места, где null попал в переменную. В Kotlin компилятор отказывается собирать такой код:
|
||||
|
||||
```kotlin
|
||||
fun length(s: String?): Int {
|
||||
// return s.length // ошибка: only safe (?.) or non-null (!!) calls are allowed
|
||||
return s?.length ?: 0
|
||||
}
|
||||
```
|
||||
Проблема переносится с этапа выполнения на этап компиляции. Разыменовать nullable-значение без проверки невозможно.
|
||||
|
||||
## Работа с nullable
|
||||
Пять подходов, от предпочтительного к крайнему:
|
||||
|
||||
### 1. Проверка на null
|
||||
После проверки компилятор сам приводит тип:
|
||||
```kotlin
|
||||
fun describe(s: String?): String {
|
||||
if (s == null) return "нет значения"
|
||||
return "длина ${s.length}" // здесь s уже String, не String?
|
||||
}
|
||||
```
|
||||
Это [умное приведение](Умные%20приведения.md).
|
||||
|
||||
### 2. Безопасный вызов - ?.
|
||||
```kotlin
|
||||
val len: Int? = s?.length // null, если s == null
|
||||
```
|
||||
|
||||
### 3. Elvis - ?:
|
||||
```kotlin
|
||||
val len: Int = s?.length ?: 0 // значение по умолчанию
|
||||
val name = user?.name ?: error("нет пользователя") // или выход
|
||||
```
|
||||
|
||||
### 4. Безопасное приведение - as?
|
||||
```kotlin
|
||||
val n: Int? = value as? Int // null вместо ClassCastException
|
||||
```
|
||||
|
||||
### 5. Утверждение - !!
|
||||
```kotlin
|
||||
val len: Int = s!!.length // NPE, если s == null
|
||||
```
|
||||
`!!` означает «я гарантирую, что здесь не null». Каждое его использование - это место, где вы отключили проверку компилятора.
|
||||
|
||||
Подробнее про операторы: [Операторы null-безопасности](Операторы%20null-безопасности.md)
|
||||
|
||||
## Nullable в коллекциях
|
||||
`?` ставится раздельно для элементов и для самой коллекции:
|
||||
```kotlin
|
||||
val a: List<String> = listOf("x") // список есть, элементы не null
|
||||
val b: List<String?> = listOf(null) // список есть, элементы могут быть null
|
||||
val c: List<String>? = null // списка может не быть
|
||||
val d: List<String?>? = null // и то, и другое
|
||||
```
|
||||
Для отсеивания null есть готовые функции:
|
||||
```kotlin
|
||||
val items: List<String?> = listOf("a", null, "b")
|
||||
println(items.filterNotNull()) // [a, b]
|
||||
```
|
||||
|
||||
## Nullable и дженерики
|
||||
Параметр типа по умолчанию допускает null, его верхняя граница - `Any?`:
|
||||
```kotlin
|
||||
fun <T> process(item: T) { } // T может быть nullable
|
||||
fun <T : Any> strict(item: T) { } // T гарантированно non-null
|
||||
|
||||
strict("text")
|
||||
// strict(null) // ошибка компиляции
|
||||
```
|
||||
Подробнее: [Параметрический полиморфизм](../../Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
## Где NPE всё-таки возможен
|
||||
Kotlin не даёт абсолютной гарантии. Источники NPE:
|
||||
|
||||
1. **Явный `!!`** - вы сами попросили
|
||||
2. **Явный `throw NullPointerException()`**
|
||||
3. **Код на Java** - типы оттуда компилятор проверить не может ([Платформенные типы](Платформенные%20типы.md))
|
||||
4. **`lateinit` до инициализации** - технически `UninitializedPropertyAccessException`
|
||||
5. **Утечка `this` из конструктора** - обращение к ещё не инициализированному свойству
|
||||
|
||||
```kotlin
|
||||
class Broken {
|
||||
val a: String = compute() // вызывается до инициализации b
|
||||
val b: String = "value"
|
||||
|
||||
fun compute(): String = b.uppercase() // NPE: b ещё null
|
||||
}
|
||||
```
|
||||
Последний случай компилятор обычно замечает и выдаёт предупреждение, но через несколько уровней вызовов - может и пропустить.
|
||||
|
||||
## Правило
|
||||
Если у вас много `!!` - типы спроектированы неправильно. Nullable-тип должен появляться там, где отсутствие значения - реальная часть предметной области («у пользователя может не быть отчества»), а не там, где лень разбираться с инициализацией.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | любая ссылка может быть null | null допускают только типы с `?` |
|
||||
> | NPE в рантайме | ошибка компиляции |
|
||||
> | `Optional<T>` как обёртка | `T?` на уровне типов, без аллокации |
|
||||
> | `@Nullable`/`@NotNull` - подсказки для IDE | часть системы типов, проверяется компилятором |
|
||||
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или просто non-null тип |
|
||||
>
|
||||
> `Optional<T>` в Java - это объект-обёртка: аллокация, нет поддержки в полях, не композируется с дженериками. `T?` в Kotlin не создаёт обёртки: в байт-коде это обычная ссылка, а проверки расставляет компилятор.
|
||||
>
|
||||
> Публичные функции Kotlin получают проверки параметров автоматически - вызов из Java с null упадёт сразу на входе, с понятным сообщением, а не глубже по стеку:
|
||||
> ```kotlin
|
||||
> fun greet(name: String) = "Hi, $name"
|
||||
> ```
|
||||
> ```java
|
||||
> GreetKt.greet(null);
|
||||
> // NullPointerException: Parameter specified as non-null is null:
|
||||
> // method GreetKt.greet, parameter name
|
||||
> ```
|
||||
> В сообщении указаны и метод, и имя параметра.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`!!` - не решение, а отложенная проблема.** Строка `user!!.profile!!.name!!` падает без указания, какое из трёх звеньев было null.
|
||||
>
|
||||
> **Умное приведение не работает с `var` из другого потока и с открытыми свойствами:**
|
||||
> ```kotlin
|
||||
> class Holder {
|
||||
> var value: String? = null
|
||||
> fun use() {
|
||||
> if (value != null) {
|
||||
> // println(value.length) // ошибка: smart cast невозможен
|
||||
> println(value!!.length) // или скопировать в локальную val
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Подробнее: [Умные приведения](Умные%20приведения.md)
|
||||
>
|
||||
> **Платформенные типы из Java проверок не имеют:**
|
||||
> ```kotlin
|
||||
> val s: String = javaMethodThatReturnsNull() // скомпилируется, упадёт в рантайме
|
||||
> ```
|
||||
>
|
||||
> **`Boolean?` нельзя использовать в `if` напрямую:**
|
||||
> ```kotlin
|
||||
> val flag: Boolean? = null
|
||||
> // if (flag) { } // ошибка компиляции
|
||||
> if (flag == true) { } // так
|
||||
> if (flag ?: false) { } // или так
|
||||
> ```
|
||||
>
|
||||
> **`null` в строковом шаблоне превращается в текст:**
|
||||
> ```kotlin
|
||||
> val x: String? = null
|
||||
> println("значение: $x") // значение: null
|
||||
> ```
|
||||
|
||||
Далее: [Операторы null-безопасности](Операторы%20null-безопасности.md)
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
title: Операторы null-безопасности
|
||||
description: Безопасный вызов ?., elvis ?:, as?, !! и функции let, requireNotNull
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
---
|
||||
|
||||
Четыре оператора и несколько функций стандартной библиотеки для работы с nullable-типами.
|
||||
|
||||
## Безопасный вызов - ?.
|
||||
Вызывает член, если значение не null, иначе возвращает null:
|
||||
```kotlin
|
||||
val s: String? = null
|
||||
println(s?.length) // null
|
||||
println(s?.uppercase()) // null
|
||||
|
||||
val t: String? = "text"
|
||||
println(t?.length) // 4
|
||||
```
|
||||
Результат `?.` всегда nullable - даже если сам член возвращает non-null тип.
|
||||
|
||||
### Цепочки
|
||||
```kotlin
|
||||
class Address(val city: String?)
|
||||
class User(val address: Address?)
|
||||
|
||||
val user: User? = null
|
||||
val city: String? = user?.address?.city // null, если оборвалось где угодно
|
||||
```
|
||||
Цепочка прерывается на первом null, остальные вызовы не выполняются. Это «безопасная навигация» - в Java пришлось бы писать три вложенных `if`.
|
||||
|
||||
### С присваиванием
|
||||
```kotlin
|
||||
class Box { var value: Int = 0 }
|
||||
val box: Box? = null
|
||||
box?.value = 42 // ничего не произойдёт, если box == null
|
||||
```
|
||||
|
||||
## Elvis - ?:
|
||||
Возвращает левую часть, если она не null, иначе правую:
|
||||
```kotlin
|
||||
val name: String? = null
|
||||
println(name ?: "анонимный") // анонимный
|
||||
|
||||
val len = name?.length ?: 0 // 0
|
||||
```
|
||||
Назван так из-за схожести `?:` с причёской Элвиса Пресли.
|
||||
|
||||
Правая часть вычисляется **только** при null (ленивость):
|
||||
```kotlin
|
||||
val value = cached ?: expensiveCompute() // вызов только при необходимости
|
||||
```
|
||||
|
||||
### Для раннего выхода
|
||||
Справа может стоять `return` или `throw` - они имеют тип [Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому подходят под любой тип:
|
||||
```kotlin
|
||||
fun process(input: String?): Int {
|
||||
val text = input ?: return -1 // выход из функции
|
||||
val n = text.toIntOrNull() ?: error("не число") // исключение
|
||||
return n * 2
|
||||
}
|
||||
```
|
||||
Это идиома: вместо вложенных проверок - плоская последовательность «достали или вышли».
|
||||
|
||||
## Безопасное приведение - as?
|
||||
Возвращает null вместо `ClassCastException`:
|
||||
```kotlin
|
||||
val value: Any = "text"
|
||||
val n: Int? = value as? Int // null
|
||||
val s: String? = value as? String // "text"
|
||||
|
||||
val safe = value as? Int ?: 0 // 0 — частая связка с elvis
|
||||
```
|
||||
Обычный `as` при несовпадении типов падает:
|
||||
```kotlin
|
||||
// val bad = value as Int // ClassCastException
|
||||
```
|
||||
|
||||
## Утверждение non-null - !!
|
||||
Превращает `T?` в `T`, бросая NPE при null:
|
||||
```kotlin
|
||||
val s: String? = "text"
|
||||
println(s!!.length) // 4
|
||||
|
||||
val n: String? = null
|
||||
// println(n!!.length) // NullPointerException
|
||||
```
|
||||
Применять только когда null означает ошибку программиста, и падение предпочтительнее тихого продолжения. В остальных случаях есть `?:`, `?.` или проверка.
|
||||
|
||||
## let для nullable
|
||||
Выполняет блок, только если значение не null:
|
||||
```kotlin
|
||||
val name: String? = "Kotlin"
|
||||
name?.let {
|
||||
println(it.length) // внутри it — уже String, не String?
|
||||
}
|
||||
|
||||
val upper = name?.let { it.uppercase() } ?: "ПУСТО"
|
||||
```
|
||||
Полезно, когда нужно несколько операций и нежелательно повторять `?.`:
|
||||
```kotlin
|
||||
user?.let {
|
||||
log(it.name)
|
||||
send(it.email)
|
||||
it.lastSeen = now()
|
||||
}
|
||||
```
|
||||
Подробнее про `let`, `run`, `also`, `apply`: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
|
||||
|
||||
## Функции проверки
|
||||
```kotlin
|
||||
val value: String? = getValue()
|
||||
|
||||
val a = requireNotNull(value) // IllegalArgumentException
|
||||
val b = requireNotNull(value) { "нет значения" }
|
||||
val c = checkNotNull(value) // IllegalStateException
|
||||
```
|
||||
Обе возвращают non-null значение, то есть заменяют `!!` с внятным сообщением. Разница - в типе исключения: `require*` для проверки аргументов, `check*` для проверки состояния.
|
||||
Подробнее: [Проверки и контракты](../../Исключения/Проверки%20и%20контракты.md)
|
||||
|
||||
## Полезные nullable-функции stdlib
|
||||
```kotlin
|
||||
val s: String? = null
|
||||
println(s.isNullOrEmpty()) // true — работает на nullable-получателе
|
||||
println(s.isNullOrBlank()) // true
|
||||
println(s.orEmpty()) // "" — строка вместо null
|
||||
|
||||
val list: List<Int>? = null
|
||||
println(list.orEmpty()) // []
|
||||
println(list?.size ?: 0) // 0
|
||||
|
||||
println("42".toIntOrNull()) // 42
|
||||
println("abc".toIntOrNull()) // null — вместо исключения
|
||||
```
|
||||
Функции с суффиксом `OrNull` есть у многих операций: `firstOrNull()`, `singleOrNull()`, `toIntOrNull()`, `getOrNull(i)`.
|
||||
|
||||
## Сводка
|
||||
| Оператор | Результат при null | Когда применять |
|
||||
|---|---|---|
|
||||
| `?.` | `null` | навигация по цепочке |
|
||||
| `?:` | правая часть | значение по умолчанию, ранний выход |
|
||||
| `as?` | `null` | неуверенное приведение типа |
|
||||
| `!!` | NPE | null невозможен по инварианту |
|
||||
| `?.let {}` | блок не выполняется | несколько действий со значением |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `if (x != null) x.foo()` | `x?.foo()` |
|
||||
> | `x != null ? x : def` | `x ?: def` |
|
||||
> | `x instanceof T ? (T) x : null` | `x as? T` |
|
||||
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или `x!!` |
|
||||
> | `Optional.ofNullable(x).map(f).orElse(d)` | `x?.let(f) ?: d` |
|
||||
> | три вложенных `if` для цепочки | `a?.b?.c` |
|
||||
>
|
||||
> Java-аналог `Optional` требует оборачивания и разворачивания, работает только как возвращаемое значение и создаёт объект. Операторы Kotlin компилируются в обычные проверки `ifnull` в байт-коде - накладных расходов нет.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`?.` возвращает nullable, цепочка «заражает» весь результат:**
|
||||
> ```kotlin
|
||||
> val list: List<String>? = listOf("a")
|
||||
> val size: Int? = list?.size // Int?, не Int
|
||||
> val sure: Int = list?.size ?: 0 // нужен elvis
|
||||
> ```
|
||||
>
|
||||
> **Приоритет `?:` ниже арифметики** - без скобок получится не то:
|
||||
> ```kotlin
|
||||
> val a: Int? = null
|
||||
> println((a ?: 0) + 1) // 1
|
||||
> // println(a ?: 0 + 1) // это a ?: (0+1), то есть 1 — здесь совпало,
|
||||
> // // но в сложных выражениях расхождение будет
|
||||
> ```
|
||||
>
|
||||
> **`as?` не спасает от null, только от неверного типа:**
|
||||
> ```kotlin
|
||||
> val x: Any? = null
|
||||
> val s: String? = x as? String // null — потому что x был null
|
||||
> ```
|
||||
>
|
||||
> **`!!` в цепочке скрывает, что именно упало.** `a!!.b!!.c!!` даёт NPE без указания звена. Либо проверяйте по шагам, либо используйте `requireNotNull` с сообщением.
|
||||
>
|
||||
> **`?.let` возвращает null, если получатель null** - а не то, что вернул блок:
|
||||
> ```kotlin
|
||||
> val s: String? = null
|
||||
> val r: Int? = s?.let { 42 } // null, блок не выполнялся
|
||||
> ```
|
||||
>
|
||||
> **Безопасный вызов на функции-расширении с nullable-получателем работает иначе.** `s.isNullOrEmpty()` вызывается без `?.`, потому что расширение объявлено для `String?` и само обрабатывает null внутри.
|
||||
|
||||
Дополнительно: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
|
||||
Далее: [Умные приведения](Умные%20приведения.md)
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
title: Платформенные типы
|
||||
description: Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
- interop
|
||||
---
|
||||
|
||||
Платформенный тип (platform type) - тип, пришедший из Java, про nullability которого компилятор Kotlin ничего не знает. Обозначается `T!`, но написать его в коде нельзя - он существует только в сообщениях компилятора и в IDE.
|
||||
|
||||
```java
|
||||
// Java
|
||||
public class Repo {
|
||||
public String findName() { return null; }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// Kotlin
|
||||
val repo = Repo()
|
||||
val a: String = repo.findName() // компилируется! падает в рантайме
|
||||
val b: String? = repo.findName() // тоже компилируется
|
||||
```
|
||||
`findName()` имеет тип `String!` - его можно присвоить и в `String`, и в `String?`. Проверку компилятор не делает.
|
||||
|
||||
## Почему так
|
||||
Альтернативы были хуже:
|
||||
- считать все Java-типы nullable → весь Java-код пришлось бы обвешивать `!!` и `?.`, включая `System.out`
|
||||
- считать все non-null → ложное чувство безопасности без возможности это заметить
|
||||
|
||||
Платформенный тип - компромисс: ответственность за nullability возвращается программисту, как в Java.
|
||||
|
||||
## Где падает
|
||||
NPE возникает в точке присваивания, а не использования - компилятор вставляет проверку на границе:
|
||||
```kotlin
|
||||
val a: String = repo.findName() // NPE здесь же
|
||||
println(a.length) // до этой строки не дойдёт
|
||||
```
|
||||
Это лучше, чем в Java: падение происходит рядом с источником, а не там, куда значение утекло.
|
||||
|
||||
## Как чинить
|
||||
### 1. Указывать nullable-тип явно
|
||||
Любое значение из Java трактуйте как `T?`, пока не убедились в обратном:
|
||||
```kotlin
|
||||
val name: String? = repo.findName()
|
||||
println(name?.length ?: 0)
|
||||
```
|
||||
|
||||
### 2. Аннотации в Java-коде
|
||||
Компилятор Kotlin читает аннотации nullability и начинает проверять типы:
|
||||
```java
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class Repo {
|
||||
@Nullable public String findName() { return null; }
|
||||
@NotNull public String getId() { return "1"; }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
val a: String = repo.findName() // теперь ошибка компиляции
|
||||
val b: String? = repo.findName() // ok
|
||||
val c: String = repo.getId() // ok, проверка не нужна
|
||||
```
|
||||
|
||||
Поддерживаются аннотации из нескольких наборов:
|
||||
|
||||
| Набор | Пакет |
|
||||
|---|---|
|
||||
| JetBrains | `org.jetbrains.annotations` |
|
||||
| JSpecify | `org.jspecify.annotations` |
|
||||
| Android | `androidx.annotation` |
|
||||
| JSR-305 | `javax.annotation` |
|
||||
| Eclipse | `org.eclipse.jdt.annotation` |
|
||||
| Lombok | `lombok.NonNull` |
|
||||
|
||||
### 3. Обёртка на границе
|
||||
Для внешней библиотеки без аннотаций - свой слой, который один раз разбирается с nullability:
|
||||
```kotlin
|
||||
// вся работа с «грязными» типами собрана здесь
|
||||
fun Repo.nameOrNull(): String? = findName()
|
||||
fun Repo.requireName(): String = findName() ?: error("name отсутствует")
|
||||
```
|
||||
Дальше код работает с честными типами Kotlin.
|
||||
|
||||
### 4. Строгий режим компилятора
|
||||
Флаг `-Xjsr305=strict` заставляет компилятор относиться к JSR-305-аннотациям как к обязательным:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xjsr305=strict")
|
||||
}
|
||||
}
|
||||
```
|
||||
Значения: `ignore`, `warn` (по умолчанию), `strict`.
|
||||
|
||||
## Коллекции из Java
|
||||
Платформенность распространяется на параметры типа:
|
||||
```java
|
||||
public List<String> getItems() { return null; }
|
||||
```
|
||||
Тип результата - `(Mutable)List<String!>!`. Неизвестно ни то, может ли быть null сам список, ни его элементы:
|
||||
```kotlin
|
||||
val items: List<String?>? = repo.items // самое честное предположение
|
||||
items?.filterNotNull()?.forEach(::println)
|
||||
```
|
||||
|
||||
Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список:
|
||||
```kotlin
|
||||
val list: List<String> = repo.items // на самом деле ArrayList
|
||||
// list.add("x") // так нельзя, тип List
|
||||
(list as MutableList).add("x") // а так — да, и это сработает
|
||||
```
|
||||
Подробнее: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
|
||||
|
||||
## Обратное направление
|
||||
Код на Kotlin для Java-вызывающего выглядит аннотированным: компилятор добавляет `@NotNull`/`@Nullable` в байт-код автоматически. Плюс в публичные функции вставляются проверки параметров:
|
||||
```kotlin
|
||||
fun greet(name: String) = "Hi, $name"
|
||||
```
|
||||
```java
|
||||
GreetKt.greet(null);
|
||||
// NullPointerException: Parameter specified as non-null is null:
|
||||
// method GreetKt.greet, parameter name
|
||||
```
|
||||
Падение происходит на входе в функцию, с указанием метода и имени параметра.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java платформенных типов нет - там все ссылки одинаково могут быть null, и никакой проверки не существует.
|
||||
>
|
||||
> Платформенные типы - это механизм совместимости, а не фича языка. Их задача - позволить Kotlin вызывать существующие библиотеки без переписывания. Чем больше кода в проекте на Kotlin, тем реже они встречаются.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Самый опасный случай - значение «просачивается» в non-null тип:**
|
||||
> ```kotlin
|
||||
> data class User(val name: String)
|
||||
> val user = User(repo.findName()) // NPE в конструкторе, а причина — в Java-методе
|
||||
> ```
|
||||
>
|
||||
> **IDE подсказывает тип `String!`, но в коде так написать нельзя.** Если увидели `!` в подсказке - значит, компилятор вас не защищает.
|
||||
>
|
||||
> **Аннотации на уровне пакета легко пропустить.** `@NonNullApi` или `package-info.java` с `@ParametersAreNonnullByDefault` меняют поведение для всего пакета. Spring, например, аннотирует свои пакеты - поэтому его API в Kotlin выглядит типизированным.
|
||||
>
|
||||
> **`-Xjsr305=strict` может сломать сборку при обновлении библиотеки** - новые аннотации превратятся в ошибки компиляции. Включать осознанно.
|
||||
>
|
||||
> **Дженерик-методы Java особенно коварны:**
|
||||
> ```java
|
||||
> public <T> T getOrNull(String key) { return null; }
|
||||
> ```
|
||||
> ```kotlin
|
||||
> val x: String = repo.getOrNull("k") // T выведется в String, NPE в рантайме
|
||||
> ```
|
||||
>
|
||||
> **Массивы из Java тоже платформенные:** `String[]` → `Array<(out) String!>!`. Элементы могут быть null даже если сам массив не null.
|
||||
|
||||
Дополнительно: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
|
||||
Далее: [Строки](../Работа%20с%20данными/Строки.md)
|
||||
@@ -1,241 +0,0 @@
|
||||
---
|
||||
title: Умные приведения
|
||||
description: Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
- types
|
||||
---
|
||||
|
||||
Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён.
|
||||
|
||||
```kotlin
|
||||
fun describe(value: Any): String {
|
||||
if (value is String) {
|
||||
return "строка длины ${value.length}" // value уже String
|
||||
}
|
||||
return "не строка"
|
||||
}
|
||||
```
|
||||
Явного `(String) value` писать не нужно - в отличие от Java (до версии 16).
|
||||
|
||||
## После проверки типа - is
|
||||
```kotlin
|
||||
fun size(x: Any): Int = when (x) {
|
||||
is String -> x.length // String
|
||||
is List<*> -> x.size // List<*>
|
||||
is Map<*, *> -> x.size // Map<*, *>
|
||||
else -> 0
|
||||
}
|
||||
```
|
||||
`!is` работает так же, с инверсией:
|
||||
```kotlin
|
||||
fun check(x: Any) {
|
||||
if (x !is String) return
|
||||
println(x.length) // после return здесь только String
|
||||
}
|
||||
```
|
||||
|
||||
## После проверки на null
|
||||
```kotlin
|
||||
fun length(s: String?): Int {
|
||||
if (s == null) return 0
|
||||
return s.length // s: String
|
||||
}
|
||||
|
||||
fun length2(s: String?): Int {
|
||||
if (s != null) {
|
||||
return s.length // s: String
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## В логических выражениях
|
||||
Приведение работает внутри `&&` и `||`, так как они ленивые:
|
||||
```kotlin
|
||||
fun check(x: Any?): Boolean {
|
||||
return x is String && x.length > 3 // x приведён во второй части
|
||||
}
|
||||
|
||||
fun check2(x: Any?): Boolean {
|
||||
return x !is String || x.length > 3 // приведён в правой части ||
|
||||
}
|
||||
```
|
||||
|
||||
## В elvis и безопасных вызовах
|
||||
```kotlin
|
||||
fun process(s: String?) {
|
||||
val text = s ?: return // после этой строки s... нет, но text: String
|
||||
println(text.length)
|
||||
}
|
||||
```
|
||||
|
||||
## Где приведение НЕ работает
|
||||
Главное условие: компилятор должен гарантировать, что **значение не изменится между проверкой и использованием**.
|
||||
|
||||
### Изменяемое свойство класса - var
|
||||
```kotlin
|
||||
class Holder {
|
||||
var value: String? = null
|
||||
|
||||
fun broken() {
|
||||
if (value != null) {
|
||||
// println(value.length) // ошибка: smart cast to 'String' is impossible
|
||||
}
|
||||
}
|
||||
|
||||
fun fixed() {
|
||||
val local = value // копия в локальную val
|
||||
if (local != null) {
|
||||
println(local.length) // работает
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Почему: между проверкой и обращением другой поток (или вызванная функция) может присвоить `value = null`.
|
||||
|
||||
### Свойство с кастомным геттером
|
||||
```kotlin
|
||||
class Random2 {
|
||||
val value: String?
|
||||
get() = if (Math.random() > 0.5) "x" else null
|
||||
|
||||
fun broken() {
|
||||
if (value != null) {
|
||||
// println(value.length) // ошибка: геттер вернёт разное
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### open-свойство
|
||||
```kotlin
|
||||
open class Base {
|
||||
open val value: String? = "x"
|
||||
}
|
||||
|
||||
fun check(b: Base) {
|
||||
if (b.value != null) {
|
||||
// println(b.value.length) // ошибка: наследник мог переопределить геттер
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Свойство из другого модуля
|
||||
Даже `val` из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера.
|
||||
|
||||
### Локальная var, захваченная лямбдой
|
||||
```kotlin
|
||||
fun broken() {
|
||||
var s: String? = "text"
|
||||
val modify = { s = null }
|
||||
|
||||
if (s != null) {
|
||||
// println(s.length) // ошибка: лямбда может изменить s
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Сводка условий
|
||||
| Что проверяем | Умное приведение |
|
||||
|---|---|
|
||||
| локальная `val` | ✅ работает |
|
||||
| локальная `var` без захвата лямбдой | ✅ работает |
|
||||
| локальная `var`, захваченная лямбдой | ❌ |
|
||||
| `val`-свойство в том же модуле, без геттера | ✅ работает |
|
||||
| `var`-свойство | ❌ |
|
||||
| `open val` | ❌ |
|
||||
| `val` с кастомным геттером | ❌ |
|
||||
| `val` из другого модуля | ❌ |
|
||||
| параметр функции (всегда `val`) | ✅ работает |
|
||||
|
||||
Универсальное решение во всех ❌-случаях: скопировать значение в локальную `val`.
|
||||
|
||||
## Явное приведение - as
|
||||
Когда умное не применимо:
|
||||
```kotlin
|
||||
val x: Any = "text"
|
||||
val s = x as String // ClassCastException при несовпадении
|
||||
val n = x as? Int // null при несовпадении
|
||||
```
|
||||
Подробнее: [Приведение типов](../Работа%20с%20данными/Приведение%20типов.md)
|
||||
|
||||
## Умное приведение в Kotlin 2.x
|
||||
Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения:
|
||||
```kotlin
|
||||
fun test(x: Any?) {
|
||||
val isString = x is String
|
||||
if (isString) {
|
||||
println(x.length) // K2 понимает связь через переменную-флаг
|
||||
}
|
||||
}
|
||||
|
||||
fun orElse(x: Any?) {
|
||||
if (x !is String) return
|
||||
println(x.length) // работало и раньше
|
||||
}
|
||||
```
|
||||
Подробнее: [Компилятор K2](../../Инструментарий/Компилятор%20K2.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `if (x instanceof String) { String s = (String) x; ... }` | `if (x is String) { x.length }` |
|
||||
> | pattern matching `x instanceof String s` (Java 16+) | есть с первой версии, без объявления переменной |
|
||||
> | приведение только по типу | ещё и по null |
|
||||
> | компилятор не отслеживает изменяемость | отказывается приводить, если значение может измениться |
|
||||
>
|
||||
> Java 16 добавила pattern matching: `if (x instanceof String s)`. Это ближе к Kotlin, но Java всё равно требует явно назвать переменную, и на nullability это не распространяется.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Сообщение компилятора объясняет причину** - его стоит читать целиком:
|
||||
> ```
|
||||
> smart cast to 'String' is impossible, because 'value' is a mutable property
|
||||
> that could be mutated concurrently
|
||||
>
|
||||
> smart cast to 'String' is impossible, because 'value' is a property
|
||||
> that has an open or custom getter
|
||||
>
|
||||
> smart cast to 'String' is impossible, because 's' is a local variable
|
||||
> that is mutated in a capturing closure
|
||||
> ```
|
||||
> Там прямо сказано и что мешает, и почему.
|
||||
>
|
||||
> **`!!` вместо локальной `val` - плохой обмен.** Оба компилируются, но второй вариант предсказуем:
|
||||
> ```kotlin
|
||||
> // println(value!!.length) // падение при гонке
|
||||
> val v = value ?: return // гонки нет
|
||||
> ```
|
||||
>
|
||||
> **Приведение дженериков неполное из-за стирания типов:**
|
||||
> ```kotlin
|
||||
> fun check(x: Any) {
|
||||
> if (x is List<*>) {
|
||||
> // элементы остаются Any?, тип параметра неизвестен
|
||||
> val first = x.firstOrNull() // Any?
|
||||
> }
|
||||
> // if (x is List<String>) // ошибка: cannot check for instance of erased type
|
||||
> }
|
||||
> ```
|
||||
> Обход - [reified](../../Дженерики/Reified%20и%20inline.md).
|
||||
>
|
||||
> **Приведение теряется после вызова функции, меняющей состояние** - если это свойство, а не локальная переменная:
|
||||
> ```kotlin
|
||||
> class C {
|
||||
> var s: String? = "x"
|
||||
> fun reset() { s = null }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **В `when` без аргумента приведение работает только для проверенной переменной:**
|
||||
> ```kotlin
|
||||
> fun combine(a: Any?, b: Any?): Int = when {
|
||||
> a is String && b is Int -> a.length + b // оба приведены
|
||||
> else -> 0
|
||||
> }
|
||||
> ```
|
||||
|
||||
Далее: [Платформенные типы](Платформенные%20типы.md)
|
||||
@@ -1,274 +0,0 @@
|
||||
---
|
||||
title: Any, Unit, Nothing
|
||||
description: Корень иерархии Any, тип-единица Unit и нижний тип Nothing
|
||||
tags:
|
||||
- kotlin
|
||||
- types
|
||||
---
|
||||
|
||||
Три особых типа образуют каркас системы типов Kotlin.
|
||||
|
||||
| Тип | Сколько значений | Роль |
|
||||
|---|---|---|
|
||||
| `Any` | все объекты | супертип всего |
|
||||
| `Unit` | одно (`Unit`) | «результата нет» |
|
||||
| `Nothing` | ни одного | «возврата не будет» |
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Any?"] --> B["Any"]
|
||||
A --> N1["Nothing?"]
|
||||
B --> C["String"]
|
||||
B --> D["Int"]
|
||||
B --> E["Unit"]
|
||||
C --> N["Nothing"]
|
||||
D --> N
|
||||
E --> N
|
||||
N1 --> N
|
||||
```
|
||||
`Any` сверху, `Nothing` снизу: он подтип любого типа.
|
||||
|
||||
## Any
|
||||
Супертип всех non-null типов. Объявляет три метода:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = 42
|
||||
println(x.toString()) // 42
|
||||
println(x.hashCode()) // 42
|
||||
println(x == 42) // true — вызов equals
|
||||
}
|
||||
```
|
||||
Больше у `Any` ничего нет: ни `wait`, ни `notify`, ни `getClass`.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val items: List<Any> = listOf(1, "два", 3.0, true)
|
||||
println(items.joinToString { it::class.simpleName ?: "?" })
|
||||
// Int, String, Double, Boolean
|
||||
}
|
||||
```
|
||||
|
||||
`Any` не принимает `null` - для этого есть `Any?`, самый широкий тип в языке:
|
||||
```kotlin
|
||||
fun main() {
|
||||
// val a: Any = null // ошибка компиляции
|
||||
val b: Any? = null // ok
|
||||
println(b) // null
|
||||
}
|
||||
```
|
||||
|
||||
Расширения `Any?` можно звать на `null`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: String? = null
|
||||
println(x.toString()) // "null" — расширение Any?.toString()
|
||||
println(x.hashCode()) // 0
|
||||
}
|
||||
```
|
||||
Это не методы `Any`, а функции-расширения stdlib, объявленные для `Any?`.
|
||||
|
||||
## Unit
|
||||
Тип функций, которые ничего не возвращают. Это **настоящий объект**, а не отсутствие значения:
|
||||
```kotlin
|
||||
fun log(msg: String): Unit = println(msg)
|
||||
|
||||
fun main() {
|
||||
val result = log("привет")
|
||||
println(result) // kotlin.Unit
|
||||
println(result == Unit) // true
|
||||
}
|
||||
```
|
||||
`Unit` - `object`, у него ровно одно значение. Тип результата `Unit` можно опускать, и обычно так и делают:
|
||||
```kotlin
|
||||
fun log(msg: String) = println(msg) // тип выведен: Unit
|
||||
fun log2(msg: String) { println(msg) } // то же самое
|
||||
```
|
||||
|
||||
Поскольку `Unit` - полноценный тип, он подставляется в дженерики, где `void` в Java не работает:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val actions: List<() -> Unit> = listOf({ print("a") }, { print("b") })
|
||||
actions.forEach { it() } // ab
|
||||
println()
|
||||
|
||||
val map = mutableMapOf<String, Unit>()
|
||||
map["ключ"] = Unit // Unit как значение — законно
|
||||
println(map) // {ключ=kotlin.Unit}
|
||||
}
|
||||
```
|
||||
|
||||
В лямбде с типом результата `Unit` последнее выражение игнорируется - возвращать ничего не нужно:
|
||||
```kotlin
|
||||
fun run2(block: () -> Unit) = block()
|
||||
|
||||
fun main() {
|
||||
run2 { 42 } // 42 отбрасывается, лямбда подходит под () -> Unit
|
||||
println("ok")
|
||||
}
|
||||
```
|
||||
|
||||
## Nothing
|
||||
Тип без значений. Функция с результатом `Nothing` **не может завершиться нормально** - только исключением или бесконечным циклом:
|
||||
```kotlin
|
||||
fun fail(message: String): Nothing = throw IllegalStateException(message)
|
||||
|
||||
fun main() {
|
||||
val x: String = if (true) "ok" else fail("нет значения")
|
||||
println(x) // ok
|
||||
}
|
||||
```
|
||||
|
||||
Зачем это нужно: `Nothing` - подтип любого типа, поэтому выражение с типом `Nothing` подходит в любую позицию. Компилятор знает, что дальше кода не будет.
|
||||
|
||||
```kotlin
|
||||
fun fail(msg: String): Nothing = throw IllegalArgumentException(msg)
|
||||
|
||||
fun parse(s: String): Int = s.toIntOrNull() ?: fail("не число: $s")
|
||||
|
||||
fun main() {
|
||||
println(parse("42")) // 42
|
||||
// parse("abc") // IllegalArgumentException: не число: abc
|
||||
}
|
||||
```
|
||||
Если бы `fail` возвращала `Unit`, этот код не скомпилировался бы: `Int` и `Unit` несовместимы.
|
||||
|
||||
`throw` сам по себе - выражение типа `Nothing`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Int = try {
|
||||
"abc".toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalStateException("плохие данные", e) // Nothing подходит
|
||||
}
|
||||
println(x)
|
||||
}
|
||||
```
|
||||
|
||||
### Анализ недостижимого кода
|
||||
```kotlin
|
||||
fun fail(): Nothing = throw RuntimeException()
|
||||
|
||||
fun main() {
|
||||
val x = 1
|
||||
if (x > 0) {
|
||||
println("да")
|
||||
} else {
|
||||
fail()
|
||||
// println("недостижимо") // предупреждение компилятора
|
||||
}
|
||||
}
|
||||
```
|
||||
Компилятор использует `Nothing`, чтобы доказать: ветка не продолжается. Поэтому `TODO()` работает где угодно:
|
||||
```kotlin
|
||||
fun calculate(x: Int): String = TODO("ещё не реализовано")
|
||||
|
||||
fun main() {
|
||||
// calculate(1) // NotImplementedError
|
||||
println("компилируется")
|
||||
}
|
||||
```
|
||||
`TODO()` объявлена как `fun TODO(): Nothing`, поэтому подходит под любой тип результата.
|
||||
|
||||
### Nothing? и null
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x = null
|
||||
println(x) // null
|
||||
// тип x — Nothing?, единственное значение которого null
|
||||
val list = listOf(null, null) // List<Nothing?>
|
||||
println(list.size) // 2
|
||||
}
|
||||
```
|
||||
Пустые коллекции тоже используют `Nothing`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val empty: List<Nothing> = emptyList()
|
||||
val strings: List<String> = empty // подходит — List ковариантен
|
||||
println(strings.size) // 0
|
||||
}
|
||||
```
|
||||
Один объект `EmptyList` подходит под `List<T>` для любого `T` именно потому, что `List<Nothing>` - подтип всех `List<T>`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Object` | `Any` (но без `wait`/`notify`/`getClass`) |
|
||||
> | `void` | `Unit` |
|
||||
> | `Void` | `Unit` (или `Nothing?`) |
|
||||
> | нет аналога | `Nothing` |
|
||||
> | `Object o = null;` | `Any? = null`, `Any` не принимает null |
|
||||
>
|
||||
> **`Any` беднее `Object`.** Методы `wait()`, `notify()`, `getClass()` в `Any` не объявлены: первые два относятся к устаревшей модели синхронизации, третий заменён на `::class`. На JVM они доступны после приведения к `Object`, но из Kotlin их намеренно не видно.
|
||||
>
|
||||
> **`Unit` против `void`** - не косметика. `void` не тип, поэтому в Java нельзя написать `List<void>` или `Function<String, void>`; приходится использовать `Void` (у которого единственное значение - `null`) или `Consumer`. В Kotlin `() -> Unit` - обычный тип.
|
||||
>
|
||||
> **`Nothing` в Java нет вообще.** Из-за этого Java-компилятор не знает, что метод никогда не вернёт управление:
|
||||
> ```java
|
||||
> int parse(String s) {
|
||||
> if (s == null) fail("null"); // fail объявлен void
|
||||
> return Integer.parseInt(s); // компилятор требует этот return
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `fail(): Nothing` сообщает компилятору, что ветка обрывается, и анализ потока это учитывает.
|
||||
>
|
||||
> Сравнить: [Класс Object в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Any` - не то же самое, что `Any?`.** Функция, принимающая `Any`, не примет `null`:
|
||||
> ```kotlin
|
||||
> fun show(x: Any) = x.toString()
|
||||
>
|
||||
> fun main() {
|
||||
> val n: String? = null
|
||||
> // println(show(n)) // ошибка компиляции
|
||||
> println(show(n ?: "пусто")) // пусто
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Лямбда с результатом `Unit` молча глотает значение:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val items = mutableListOf<Int>()
|
||||
> listOf(1, 2).forEach { items.add(it) } // add возвращает Boolean,
|
||||
> println(items) // но тип () -> Unit его отбрасывает
|
||||
> }
|
||||
> ```
|
||||
> Это обычно удобно, но если по ошибке вместо `map` написать `forEach`, результат исчезнет без предупреждения.
|
||||
>
|
||||
> **`return` с типом `Unit` писать не нужно**, а `return Unit` - признак непонимания:
|
||||
> ```kotlin
|
||||
> fun f(): Unit {
|
||||
> return Unit // компилируется, но так не пишут
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Функция с типом `Nothing` не может иметь тело-блок с `return`:**
|
||||
> ```kotlin
|
||||
> fun fail(): Nothing {
|
||||
> // return // ошибка: ничего нельзя вернуть
|
||||
> throw RuntimeException() // только выход через исключение
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`val x = null` имеет тип `Nothing?`, а не `Any?`.** Присвоить такой переменной что-либо другое нельзя:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> var x = null
|
||||
> // x = "строка" // ошибка: ожидался Nothing?
|
||||
> val y: String? = null // так
|
||||
> println("$x $y")
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`Nothing` в дженерике означает «значений не будет»:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val producer: () -> Nothing = { throw RuntimeException("всегда падает") }
|
||||
> // producer() — вызов гарантированно бросит исключение
|
||||
> println("тип ${producer::class.simpleName} объявлен, но вызов невозможен")
|
||||
> }
|
||||
> ```
|
||||
> Если такой тип получился случайно (например, `emptyList()` без указания параметра), в коллекцию ничего нельзя добавить.
|
||||
|
||||
Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md)
|
||||
Далее: [Data class](Data%20class.md)
|
||||
@@ -1,276 +0,0 @@
|
||||
---
|
||||
title: Data class
|
||||
description: Автоматические equals, hashCode, toString, copy и componentN, их границы и ловушки
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
`data class` - класс, для которого компилятор генерирует стандартный набор методов.
|
||||
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String)
|
||||
|
||||
fun main() {
|
||||
val u = User(1, "Иван")
|
||||
println(u) // User(id=1, name=Иван)
|
||||
println(u == User(1, "Иван")) // true
|
||||
println(u.copy(name = "Пётр")) // User(id=1, name=Пётр)
|
||||
val (id, name) = u
|
||||
println("$id $name") // 1 Иван
|
||||
}
|
||||
```
|
||||
|
||||
Генерируется:
|
||||
|
||||
| Метод | Что делает |
|
||||
|---|---|
|
||||
| `equals` | сравнивает все свойства основного конструктора |
|
||||
| `hashCode` | считает по тем же свойствам |
|
||||
| `toString` | `Имя(свойство=значение, ...)` |
|
||||
| `componentN` | для [деструктуризации](../Работа%20с%20данными/Деструктуризация.md) |
|
||||
| `copy` | копия с заменой части свойств |
|
||||
|
||||
## Требования
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
```
|
||||
- основной конструктор с **хотя бы одним** параметром
|
||||
- все параметры конструктора - `val` или `var`
|
||||
- класс не может быть `abstract`, `open`, `sealed` или `inner`
|
||||
|
||||
```kotlin
|
||||
// data class Empty // ошибка: нет параметров
|
||||
// data class Bad(x: Int) // ошибка: x не val/var
|
||||
```
|
||||
|
||||
## Только свойства конструктора
|
||||
Это главное, что нужно понимать про `data class`. Свойства, объявленные в теле, **не участвуют** в генерации:
|
||||
```kotlin
|
||||
data class User(val id: Int) {
|
||||
var nickname: String = "" // в теле — игнорируется
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = User(1).apply { nickname = "иван" }
|
||||
val b = User(1).apply { nickname = "пётр" }
|
||||
println(a == b) // true! nickname не сравнивается
|
||||
println(a) // User(id=1) — nickname не печатается
|
||||
println(a.copy()) // User(id=1) — nickname потерян, станет ""
|
||||
}
|
||||
```
|
||||
Если свойство важно для равенства - оно должно быть в конструкторе.
|
||||
|
||||
## copy
|
||||
Создаёт новый объект, меняя указанные свойства:
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String, val active: Boolean = true)
|
||||
|
||||
fun main() {
|
||||
val u = User(1, "Иван")
|
||||
println(u.copy()) // User(id=1, name=Иван, active=true)
|
||||
println(u.copy(name = "Пётр")) // User(id=1, name=Пётр, active=true)
|
||||
println(u.copy(id = 2, active = false))
|
||||
println(u) // исходный не изменился
|
||||
}
|
||||
```
|
||||
`copy` - основной способ «изменить» неизменяемый объект. Аргументы почти всегда именованные.
|
||||
|
||||
## Вложенные data class
|
||||
```kotlin
|
||||
data class Address(val city: String, val street: String)
|
||||
data class Person(val name: String, val address: Address)
|
||||
|
||||
fun main() {
|
||||
val p = Person("Иван", Address("Москва", "Арбат"))
|
||||
println(p == Person("Иван", Address("Москва", "Арбат"))) // true
|
||||
|
||||
// замена вложенного свойства — copy внутри copy
|
||||
val moved = p.copy(address = p.address.copy(city = "Казань"))
|
||||
println(moved) // Person(name=Иван, address=Address(city=Казань, street=Арбат))
|
||||
}
|
||||
```
|
||||
Для глубоких структур такие цепочки становятся громоздкими - это известная цена иммутабельности.
|
||||
|
||||
## Переопределение сгенерированного
|
||||
Любой из методов можно написать руками - свой вариант побеждает:
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String) {
|
||||
override fun toString() = "#$id $name"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(User(1, "Иван")) // #1 Иван
|
||||
println(User(1, "Иван") == User(1, "Иван")) // true — equals сгенерирован
|
||||
}
|
||||
```
|
||||
|
||||
Частый случай - равенство только по идентификатору:
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String) {
|
||||
override fun equals(other: Any?) = other is User && other.id == id
|
||||
override fun hashCode() = id
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(User(1, "Иван") == User(1, "Пётр")) // true — сравниваем id
|
||||
}
|
||||
```
|
||||
Но если равенство нестандартное, `data class` уже почти не нужен - остаются только `copy` и `toString`.
|
||||
|
||||
## В коллекциях
|
||||
Корректный `equals`/`hashCode` делает `data class` пригодным для `Set` и ключей `Map`:
|
||||
```kotlin
|
||||
data class Key(val a: Int, val b: String)
|
||||
|
||||
fun main() {
|
||||
val map = mapOf(Key(1, "x") to "значение")
|
||||
println(map[Key(1, "x")]) // значение — новый объект находит запись
|
||||
println(setOf(Key(1, "x"), Key(1, "x")).size) // 1 — дубликат убран
|
||||
println(listOf(Key(1, "x")).contains(Key(1, "x"))) // true
|
||||
}
|
||||
```
|
||||
|
||||
## Наследование
|
||||
`data class` не может быть `open`, но может реализовывать интерфейсы и наследоваться от обычного класса:
|
||||
```kotlin
|
||||
interface Named { val name: String }
|
||||
|
||||
data class User(override val name: String, val id: Int) : Named
|
||||
|
||||
fun main() {
|
||||
val n: Named = User("Иван", 1)
|
||||
println(n.name) // Иван
|
||||
}
|
||||
```
|
||||
Часто `data class` реализуют варианты [sealed-иерархии](Sealed%20class%20и%20interface.md):
|
||||
```kotlin
|
||||
sealed interface Event
|
||||
data class Click(val x: Int, val y: Int) : Event
|
||||
data class KeyPress(val code: Int) : Event
|
||||
data object Close : Event
|
||||
|
||||
fun handle(e: Event): String = when (e) {
|
||||
is Click -> "клик ${e.x},${e.y}"
|
||||
is KeyPress -> "клавиша ${e.code}"
|
||||
Close -> "закрытие"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle(Click(1, 2))) // клик 1,2
|
||||
println(handle(Close)) // закрытие
|
||||
}
|
||||
```
|
||||
|
||||
## data object
|
||||
Для варианта без данных есть `data object` (Kotlin 1.9+) - он отличается от обычного `object` осмысленным `toString`:
|
||||
```kotlin
|
||||
object Plain
|
||||
data object Loading
|
||||
|
||||
fun main() {
|
||||
println(Plain) // Plain@1b6d3586
|
||||
println(Loading) // Loading
|
||||
}
|
||||
```
|
||||
Подробнее: [Object и companion object](Object%20и%20companion%20object.md)
|
||||
|
||||
## Когда не нужен
|
||||
- **Сущность с идентичностью** (entity в БД): равенство по всем полям неверно, нужно по `id`
|
||||
- **Класс с поведением**: если методов больше, чем свойств, это не носитель данных
|
||||
- **Публичный API, который будет развиваться**: порядок свойств в конструкторе становится контрактом из-за `componentN`
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> Ближайший аналог - `record` из Java 16:
|
||||
> ```java
|
||||
> record User(int id, String name) {}
|
||||
> ```
|
||||
>
|
||||
> | | Java record | Kotlin data class |
|
||||
> |---|---|---|
|
||||
> | `equals`/`hashCode`/`toString` | да | да |
|
||||
> | копия с изменением | нет | `copy()` |
|
||||
> | деструктуризация | только в pattern matching | везде |
|
||||
> | изменяемые поля | нет, всегда final | `var` разрешён |
|
||||
> | наследование от класса | нет | можно |
|
||||
> | значения по умолчанию | нет | есть |
|
||||
> | своё тело | можно | можно |
|
||||
>
|
||||
> Главное практическое различие - `copy()`. В Java для изменения одного поля record приходится вызывать конструктор со всеми аргументами:
|
||||
> ```java
|
||||
> var moved = new User(u.id(), "Пётр");
|
||||
> ```
|
||||
>
|
||||
> Второе - `record` всегда неизменяем, `data class` допускает `var`. Это гибкость, которой лучше не пользоваться.
|
||||
>
|
||||
> До появления `record` в Java такие классы писали руками или генерировали Lombok-ом (`@Data`, `@Value`). Lombok генерировал ещё и `@Builder`, аналога которому в Kotlin нет - его заменяют значения по умолчанию и именованные аргументы.
|
||||
>
|
||||
> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Массив в `data class` ломает равенство:**
|
||||
> ```kotlin
|
||||
> data class Packet(val data: ByteArray)
|
||||
>
|
||||
> fun main() {
|
||||
> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // false!
|
||||
> }
|
||||
> ```
|
||||
> Сгенерированный `equals` вызывает `data.equals(other.data)`, а у массива это сравнение ссылок. Нужно писать руками:
|
||||
> ```kotlin
|
||||
> class Packet(val data: ByteArray) {
|
||||
> override fun equals(other: Any?) =
|
||||
> other is Packet && data.contentEquals(other.data)
|
||||
> override fun hashCode() = data.contentHashCode()
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // true
|
||||
> }
|
||||
> ```
|
||||
> Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md)
|
||||
>
|
||||
> **`var` в `data class` + использование как ключа `Map` = потерянная запись:**
|
||||
> ```kotlin
|
||||
> data class Key(var id: Int)
|
||||
>
|
||||
> fun main() {
|
||||
> val key = Key(1)
|
||||
> val map = hashMapOf(key to "значение")
|
||||
> key.id = 2 // hashCode изменился
|
||||
> println(map[key]) // null — запись потеряна
|
||||
> println(map[Key(1)]) // null — старый бакет, другой equals
|
||||
> println(map.size) // 1 — но она там есть
|
||||
> }
|
||||
> ```
|
||||
> Это худшая ошибка с `data class`. Поэтому свойства делают `val`.
|
||||
>
|
||||
> **`copy` копирует поверхностно.** Вложенный изменяемый объект остаётся общим:
|
||||
> ```kotlin
|
||||
> data class Box(val items: MutableList<Int>)
|
||||
>
|
||||
> fun main() {
|
||||
> val a = Box(mutableListOf(1))
|
||||
> val b = a.copy()
|
||||
> b.items.add(2)
|
||||
> println(a.items) // [1, 2] — изменился и оригинал!
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Свойства в теле класса не входят ни в `equals`, ни в `copy`, ни в `toString`** - значение теряется при копировании.
|
||||
>
|
||||
> **Порядок свойств - часть API** из-за `componentN`. Перестановка свойств одинакового типа не вызовет ошибки компиляции у клиентов, но сломает их деструктуризацию. Подробнее: [Деструктуризация](../Работа%20с%20данными/Деструктуризация.md)
|
||||
>
|
||||
> **`copy` до Kotlin 2.0 игнорировал видимость конструктора.** Приватный конструктор не защищал от создания невалидного объекта через `copy`. С Kotlin 2.1 `copy` получает ту же видимость, что конструктор, если включён `-Xconsistent-data-class-copy-visibility`; это станет поведением по умолчанию.
|
||||
>
|
||||
> **`toString` печатает всё, включая пароли и токены.** Логирование `data class` с секретами - реальная утечка:
|
||||
> ```kotlin
|
||||
> data class Credentials(val login: String, val password: String) {
|
||||
> override fun toString() = "Credentials(login=$login, password=***)"
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Credentials("ivan", "secret")) }
|
||||
> ```
|
||||
|
||||
Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md)
|
||||
Далее: [Enum class](Enum%20class.md)
|
||||
@@ -1,298 +0,0 @@
|
||||
---
|
||||
title: Enum class
|
||||
description: Перечисления со свойствами и методами, entries, when по enum и его границы
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
`enum class` - класс с фиксированным набором экземпляров.
|
||||
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED, DELETED }
|
||||
|
||||
fun main() {
|
||||
val s = Status.ACTIVE
|
||||
println(s) // ACTIVE
|
||||
println(s.name) // ACTIVE — строка
|
||||
println(s.ordinal) // 0 — позиция в объявлении
|
||||
}
|
||||
```
|
||||
|
||||
## Свойства и конструктор
|
||||
Каждая константа - вызов конструктора:
|
||||
```kotlin
|
||||
enum class Planet(val radiusKm: Int, val hasRings: Boolean) {
|
||||
EARTH(6371, false),
|
||||
SATURN(58232, true),
|
||||
MARS(3390, false);
|
||||
|
||||
val radiusM: Int get() = radiusKm * 1000
|
||||
fun describe() = "$name: радиус $radiusKm км, кольца=$hasRings"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Planet.SATURN.describe()) // SATURN: радиус 58232 км, кольца=true
|
||||
println(Planet.EARTH.radiusM) // 6371000
|
||||
}
|
||||
```
|
||||
Точка с запятой после последней константы обязательна, если дальше идёт тело класса.
|
||||
|
||||
## Методы в константах
|
||||
Константа может переопределить метод - это заменяет `when` по самой себе:
|
||||
```kotlin
|
||||
enum class Operation {
|
||||
PLUS { override fun apply(a: Int, b: Int) = a + b },
|
||||
MINUS { override fun apply(a: Int, b: Int) = a - b },
|
||||
TIMES { override fun apply(a: Int, b: Int) = a * b };
|
||||
|
||||
abstract fun apply(a: Int, b: Int): Int
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Operation.PLUS.apply(2, 3)) // 5
|
||||
println(Operation.TIMES.apply(2, 3)) // 6
|
||||
println(Operation.entries.joinToString { "${it.name}=${it.apply(6, 3)}" })
|
||||
// PLUS=9, MINUS=3, TIMES=18
|
||||
}
|
||||
```
|
||||
Каждая такая константа компилируется в анонимный подкласс.
|
||||
|
||||
## Реализация интерфейса
|
||||
```kotlin
|
||||
interface Describable { fun describe(): String }
|
||||
|
||||
enum class Level(val weight: Int) : Describable {
|
||||
LOW(1), HIGH(10);
|
||||
|
||||
override fun describe() = "$name (вес $weight)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val items: List<Describable> = Level.entries
|
||||
println(items.joinToString { it.describe() }) // LOW (вес 1), HIGH (вес 10)
|
||||
}
|
||||
```
|
||||
|
||||
## entries, values, valueOf
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED }
|
||||
|
||||
fun main() {
|
||||
println(Status.entries) // [ACTIVE, BLOCKED] — список
|
||||
println(Status.entries.size) // 2
|
||||
println(Status.valueOf("ACTIVE")) // ACTIVE
|
||||
println(Status.entries.first().name) // ACTIVE
|
||||
// println(Status.valueOf("НЕТ")) // IllegalArgumentException
|
||||
}
|
||||
```
|
||||
|
||||
`entries` (Kotlin 1.9+) заменяет `values()`. Разница практическая:
|
||||
|
||||
| | `values()` | `entries` |
|
||||
|---|---|---|
|
||||
| тип | `Array<Status>` | `EnumEntries<Status>` (это `List`) |
|
||||
| аллокация | **новый массив при каждом вызове** | один неизменяемый список |
|
||||
| изменяемость | элементы можно подменить | нельзя |
|
||||
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED }
|
||||
|
||||
fun main() {
|
||||
@Suppress("DEPRECATION")
|
||||
println(Status.values().contentToString()) // [ACTIVE, BLOCKED]
|
||||
println(Status.entries) // [ACTIVE, BLOCKED]
|
||||
}
|
||||
```
|
||||
В новом коде всегда `entries`.
|
||||
|
||||
Безопасный разбор строки:
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED }
|
||||
|
||||
fun parseStatus(s: String): Status? =
|
||||
Status.entries.find { it.name == s }
|
||||
|
||||
fun main() {
|
||||
println(parseStatus("ACTIVE")) // ACTIVE
|
||||
println(parseStatus("нет")) // null — без исключения
|
||||
}
|
||||
```
|
||||
Через `enumValueOf` то же самое, но с исключением. Для внешних данных (JSON, БД, параметры запроса) нужна версия с `null`.
|
||||
|
||||
## when по enum
|
||||
Главное применение. Компилятор проверяет полноту, и `else` не нужен:
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED, DELETED }
|
||||
|
||||
fun message(s: Status): String = when (s) {
|
||||
Status.ACTIVE -> "работает"
|
||||
Status.BLOCKED -> "заблокирован"
|
||||
Status.DELETED -> "удалён"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(message(Status.BLOCKED)) // заблокирован
|
||||
}
|
||||
```
|
||||
Если добавить константу `ARCHIVED`, этот `when` перестанет компилироваться - компилятор найдёт все места, которые нужно обновить. Поэтому `else` в `when` по enum - вредная привычка: он скрывает именно те ошибки, от которых enum защищает.
|
||||
|
||||
Импорт констант сокращает запись:
|
||||
```kotlin
|
||||
enum class Status { ACTIVE, BLOCKED }
|
||||
|
||||
fun message(s: Status): String {
|
||||
return when (s) {
|
||||
Status.ACTIVE -> "работает"
|
||||
Status.BLOCKED -> "заблокирован"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = println(message(Status.ACTIVE))
|
||||
```
|
||||
|
||||
## Сравнение и порядок
|
||||
```kotlin
|
||||
enum class Level { LOW, MEDIUM, HIGH }
|
||||
|
||||
fun main() {
|
||||
println(Level.LOW == Level.LOW) // true
|
||||
println(Level.LOW < Level.HIGH) // true — по ordinal
|
||||
println(Level.entries.sorted()) // [LOW, MEDIUM, HIGH]
|
||||
println(Level.HIGH.ordinal) // 2
|
||||
println(Level.entries.maxOf { it.ordinal }) // 2
|
||||
}
|
||||
```
|
||||
`enum` реализует `Comparable` по `ordinal`. Сравнивать через `==` безопасно: экземпляр единственный, поэтому `==` и `===` дают одно и то же.
|
||||
|
||||
## EnumMap и EnumSet
|
||||
Специализированные коллекции для enum - массив вместо хеш-таблицы:
|
||||
```kotlin
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
|
||||
enum class Day { MON, TUE, WED }
|
||||
|
||||
fun main() {
|
||||
val hours = EnumMap<Day, Int>(Day::class.java)
|
||||
hours[Day.MON] = 8
|
||||
hours[Day.TUE] = 6
|
||||
println(hours) // {MON=8, TUE=6}
|
||||
|
||||
val workdays = EnumSet.of(Day.MON, Day.TUE)
|
||||
println(workdays.contains(Day.WED)) // false
|
||||
println(EnumSet.complementOf(workdays)) // [WED]
|
||||
}
|
||||
```
|
||||
Это классы JDK; в stdlib Kotlin аналогов нет. Для небольших enum разница с `HashMap` несущественна.
|
||||
|
||||
## companion object
|
||||
```kotlin
|
||||
enum class Status(val code: Int) {
|
||||
ACTIVE(1), BLOCKED(2);
|
||||
|
||||
companion object {
|
||||
private val byCode = entries.associateBy { it.code }
|
||||
fun fromCode(code: Int): Status? = byCode[code]
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Status.fromCode(2)) // BLOCKED
|
||||
println(Status.fromCode(99)) // null
|
||||
}
|
||||
```
|
||||
Поиск по коду через заранее построенную `Map` - стандартный приём. Подробнее: [Object и companion object](Object%20и%20companion%20object.md)
|
||||
|
||||
## Когда enum, а когда sealed
|
||||
```kotlin
|
||||
enum class Status { OK, ERROR } // варианты без данных
|
||||
|
||||
sealed interface Result2
|
||||
data class Ok(val value: Int) : Result2 // варианты с разными данными
|
||||
data class Error(val message: String) : Result2
|
||||
```
|
||||
`enum` - фиксированный набор **значений**, у всех одинаковая структура. `sealed` - фиксированный набор **типов**, у каждого свои свойства. Подробнее: [Sealed class и interface](Sealed%20class%20и%20interface.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `enum Status { ACTIVE }` | `enum class Status { ACTIVE }` |
|
||||
> | `Status.values()` | `Status.entries` |
|
||||
> | `Status.valueOf(s)` | `Status.valueOf(s)` или `entries.find` |
|
||||
> | `switch` без проверки полноты до Java 21 | `when` проверяет всегда |
|
||||
> | `EnumMap`/`EnumSet` | то же (из JDK) |
|
||||
>
|
||||
> Устройство одинаковое - на JVM это тот же `java.lang.Enum`. Два практических различия:
|
||||
>
|
||||
> **`entries` вместо `values()`.** Java-метод `values()` возвращает новый массив при каждом вызове, потому что массив изменяем и вернуть общий нельзя. `entries` возвращает неизменяемый список, поэтому экземпляр один. В цикле это заметная разница.
|
||||
>
|
||||
> **Проверка полноты.** В Java `switch` по enum без `default` долго не проверялся: добавление константы молча менялo поведение. Kotlin проверяет `when`-выражение с момента появления языка, а с Kotlin 2.0 - и `when`-инструкцию.
|
||||
>
|
||||
> Сравнить: [Enum в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`ordinal` нельзя сохранять в БД или в файл.** Он меняется при вставке константы в середину:
|
||||
> ```kotlin
|
||||
> enum class Status { ACTIVE, BLOCKED } // BLOCKED.ordinal == 1
|
||||
> // добавили NEW в середину:
|
||||
> // enum class Status { ACTIVE, NEW, BLOCKED } // BLOCKED.ordinal == 2
|
||||
> ```
|
||||
> Все сохранённые единицы станут означать `NEW`. Для сериализации используйте `name` или свой стабильный `code`.
|
||||
>
|
||||
> **`name` тоже хрупок при переименовании** - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство:
|
||||
> ```kotlin
|
||||
> enum class Status(val code: String) {
|
||||
> ACTIVE("active"), BLOCKED("blocked")
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Status.ACTIVE.code) } // active
|
||||
> ```
|
||||
>
|
||||
> **`valueOf` бросает исключение на незнакомой строке**, а не возвращает `null`. Для внешних данных нужна своя обёртка через `entries.find`.
|
||||
>
|
||||
> **`values()` аллоцирует массив на каждый вызов:**
|
||||
> ```kotlin
|
||||
> enum class Status { ACTIVE }
|
||||
>
|
||||
> fun main() {
|
||||
> @Suppress("DEPRECATION")
|
||||
> repeat(3) { Status.values() } // три разных массива
|
||||
> repeat(3) { Status.entries } // один и тот же список
|
||||
> println("ok")
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`else` в `when` по enum отключает проверку полноты.** Самая частая ошибка: добавили константу, компилятор промолчал, в рантайме сработала ветка `else`.
|
||||
>
|
||||
> **Enum нельзя наследовать и расширять.** Если набор вариантов должен быть расширяемым (например, плагинами), enum не подходит - нужен `sealed interface` или обычная иерархия.
|
||||
>
|
||||
> **Enum с изменяемым состоянием - глобальная переменная:**
|
||||
> ```kotlin
|
||||
> enum class Counter {
|
||||
> INSTANCE;
|
||||
> var count = 0 // общее состояние на всю программу
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> Counter.INSTANCE.count++
|
||||
> println(Counter.INSTANCE.count) // 1 — и так во всех потоках
|
||||
> }
|
||||
> ```
|
||||
> Константы enum - синглтоны, поэтому `var` в них небезопасен при многопоточности.
|
||||
>
|
||||
> **`when` по nullable enum всё равно требует `else` или ветки `null`:**
|
||||
> ```kotlin
|
||||
> enum class Status { ACTIVE, BLOCKED }
|
||||
>
|
||||
> fun message(s: Status?): String = when (s) {
|
||||
> Status.ACTIVE -> "работает"
|
||||
> Status.BLOCKED -> "заблокирован"
|
||||
> null -> "не задан"
|
||||
> }
|
||||
>
|
||||
> fun main() { println(message(null)) } // не задан
|
||||
> ```
|
||||
|
||||
Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md)
|
||||
Далее: [Sealed class и interface](Sealed%20class%20и%20interface.md)
|
||||
@@ -1,347 +0,0 @@
|
||||
---
|
||||
title: Object и companion object
|
||||
description: Синглтон через object, объект-выражение, companion object вместо static и фабрики
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Слово `object` в Kotlin означает три разные вещи: объявление синглтона, выражение-анонимный объект и компаньон класса.
|
||||
|
||||
## object - синглтон
|
||||
```kotlin
|
||||
object Config {
|
||||
val appName = "MyApp"
|
||||
var debug = false
|
||||
|
||||
fun describe() = "$appName debug=$debug"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config.appName) // MyApp
|
||||
Config.debug = true
|
||||
println(Config.describe()) // MyApp debug=true
|
||||
}
|
||||
```
|
||||
Экземпляр создаётся **один** и **лениво** - при первом обращении. Потокобезопасность обеспечивает JVM через механизм инициализации классов.
|
||||
|
||||
```kotlin
|
||||
object Lazy2 {
|
||||
init { println("инициализация") }
|
||||
val value = 42
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println("до обращения") // до обращения
|
||||
println(Lazy2.value) // инициализация, затем 42
|
||||
println(Lazy2.value) // 42 — повторной инициализации нет
|
||||
}
|
||||
```
|
||||
|
||||
Объект может наследоваться и реализовывать интерфейсы:
|
||||
```kotlin
|
||||
interface Logger { fun log(msg: String) }
|
||||
|
||||
object ConsoleLogger : Logger {
|
||||
override fun log(msg: String) = println("[LOG] $msg")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val l: Logger = ConsoleLogger
|
||||
l.log("привет") // [LOG] привет
|
||||
}
|
||||
```
|
||||
Это главное отличие от набора статических методов: синглтон - полноценный объект, его можно передать туда, где ожидается интерфейс.
|
||||
|
||||
У `object` не может быть конструктора - передать ему параметры при создании невозможно.
|
||||
|
||||
## data object
|
||||
Для случаев, где нужен осмысленный `toString` (обычно в [sealed-иерархиях](Sealed%20class%20и%20interface.md)):
|
||||
```kotlin
|
||||
object Plain
|
||||
data object Loading
|
||||
|
||||
fun main() {
|
||||
println(Plain) // Plain@1b6d3586
|
||||
println(Loading) // Loading
|
||||
println(Loading == Loading) // true
|
||||
println(Loading.hashCode() == Loading.hashCode()) // true
|
||||
}
|
||||
```
|
||||
`data object` генерирует `toString`, `equals` и `hashCode`. Для синглтона `equals` и так работал по ссылке - полезен именно `toString`.
|
||||
|
||||
## Объект-выражение
|
||||
Анонимный объект создаётся на месте:
|
||||
```kotlin
|
||||
interface ClickListener {
|
||||
fun onClick(x: Int, y: Int)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val listener = object : ClickListener {
|
||||
override fun onClick(x: Int, y: Int) = println("клик $x,$y")
|
||||
}
|
||||
listener.onClick(1, 2) // клик 1,2
|
||||
}
|
||||
```
|
||||
В отличие от объявления `object`, выражение создаёт **новый** экземпляр при каждом вычислении:
|
||||
```kotlin
|
||||
private fun makeCounter() = object {
|
||||
var count = 0
|
||||
fun inc() = ++count
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = makeCounter()
|
||||
val b = makeCounter()
|
||||
a.inc(); a.inc()
|
||||
println("${a.inc()} ${b.inc()}") // 3 1 — независимые объекты
|
||||
}
|
||||
```
|
||||
|
||||
Можно реализовать несколько интерфейсов сразу и даже ни одного:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val point = object {
|
||||
val x = 1
|
||||
val y = 2
|
||||
override fun toString() = "($x, $y)"
|
||||
}
|
||||
println(point) // (1, 2)
|
||||
println(point.x) // 1
|
||||
}
|
||||
```
|
||||
Тип такого объекта анонимен, поэтому за пределы локальной области он не выходит:
|
||||
```kotlin
|
||||
class Holder {
|
||||
private fun make() = object { val x = 1 } // private — тип виден
|
||||
fun use() = make().x
|
||||
|
||||
// fun bad() = object { val x = 1 } // public: тип станет Any,
|
||||
} // и x будет недоступен
|
||||
|
||||
fun main() { println(Holder().use()) } // 1
|
||||
```
|
||||
|
||||
Объект-выражение видит и может менять локальные переменные:
|
||||
```kotlin
|
||||
fun main() {
|
||||
var clicks = 0
|
||||
val listener = object : Runnable {
|
||||
override fun run() { clicks++ }
|
||||
}
|
||||
listener.run(); listener.run()
|
||||
println(clicks) // 2
|
||||
}
|
||||
```
|
||||
|
||||
## companion object
|
||||
Члены, привязанные к классу, а не к экземпляру:
|
||||
```kotlin
|
||||
class User private constructor(val name: String) {
|
||||
companion object {
|
||||
const val MAX_NAME_LENGTH = 50
|
||||
|
||||
fun create(name: String): User? =
|
||||
if (name.length <= MAX_NAME_LENGTH) User(name) else null
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(User.MAX_NAME_LENGTH) // 50
|
||||
println(User.create("Иван")?.name) // Иван
|
||||
println(User.create("x".repeat(99))) // null
|
||||
}
|
||||
```
|
||||
Обращение идёт через имя класса - как к статическим членам. Приватный конструктор + фабрика в компаньоне - стандартный способ контролировать создание объектов.
|
||||
|
||||
Компаньон может быть только один, и у него может быть имя:
|
||||
```kotlin
|
||||
class User {
|
||||
companion object Factory {
|
||||
fun create() = User()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(User.create()::class.simpleName) // User
|
||||
println(User.Factory.create()::class.simpleName) // User — через имя
|
||||
}
|
||||
```
|
||||
Имя нужно редко - в основном для вызова из Java.
|
||||
|
||||
### Компаньон - это объект
|
||||
Поэтому он может реализовывать интерфейсы:
|
||||
```kotlin
|
||||
interface Parser<T> {
|
||||
fun parse(s: String): T?
|
||||
}
|
||||
|
||||
class Color2(val rgb: Int) {
|
||||
companion object : Parser<Color2> {
|
||||
override fun parse(s: String): Color2? =
|
||||
s.removePrefix("#").toIntOrNull(16)?.let { Color2(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Color2.parse("#ff0000")?.rgb) // 16711680
|
||||
val p: Parser<Color2> = Color2 // компаньон как значение
|
||||
println(p.parse("#00ff00")?.rgb) // 65280
|
||||
}
|
||||
```
|
||||
Статические методы в Java так передать нельзя - это реальное преимущество.
|
||||
|
||||
### Расширения компаньона
|
||||
```kotlin
|
||||
class User(val name: String) {
|
||||
companion object
|
||||
}
|
||||
|
||||
fun User.Companion.anonymous() = User("анонимный")
|
||||
|
||||
fun main() {
|
||||
println(User.anonymous().name) // анонимный
|
||||
}
|
||||
```
|
||||
Пустой `companion object` объявляют именно для этого - чтобы к классу можно было добавить «статическую» фабрику извне.
|
||||
|
||||
## const и @JvmStatic
|
||||
```kotlin
|
||||
class Config {
|
||||
companion object {
|
||||
const val VERSION = "1.0" // константа компиляции
|
||||
val runtime = System.nanoTime() // вычисляется при инициализации
|
||||
|
||||
@JvmStatic
|
||||
fun helper() = "вызывается из Java как Config.helper()"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Config.VERSION)
|
||||
println(Config.helper())
|
||||
}
|
||||
```
|
||||
`const` работает только для примитивов и `String`, и только на верхнем уровне, в `object` или в `companion object`. Значение подставляется в место использования при компиляции.
|
||||
|
||||
`@JvmStatic` делает член настоящим статическим в байт-коде. Без неё Java-код обращается через `Config.Companion.helper()`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
## object или функции верхнего уровня
|
||||
```kotlin
|
||||
// вариант 1: функции файла
|
||||
fun formatDate(d: Long) = "дата $d"
|
||||
|
||||
// вариант 2: object
|
||||
object DateUtils {
|
||||
fun format(d: Long) = "дата $d"
|
||||
}
|
||||
```
|
||||
Если состояния нет и интерфейс реализовывать не нужно - функции верхнего уровня проще: не нужно писать префикс, и в байт-коде нет лишнего экземпляра. `object` оправдан, когда есть состояние или нужна реализация интерфейса.
|
||||
|
||||
## Три формы рядом
|
||||
| Форма | Экземпляров | Когда |
|
||||
|---|---|---|
|
||||
| `object Name { }` | один, ленивый | синглтон: конфиг, реестр, реализация интерфейса |
|
||||
| `object : Type { }` | новый каждый раз | колбэк, одноразовая реализация |
|
||||
| `companion object` | один на класс | фабрики, константы, «статика» |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `static` поля и методы | `companion object` или функции файла |
|
||||
> | `static final` константа | `const val` |
|
||||
> | паттерн Singleton (enum/holder) | `object` |
|
||||
> | анонимный класс `new Iface() {}` | `object : Iface {}` |
|
||||
> | `static` блок инициализации | `init` в `object`/`companion object` |
|
||||
> | статику нельзя передать как объект | компаньон реализует интерфейсы |
|
||||
> | лямбда только для SAM | `object` для любого числа методов |
|
||||
>
|
||||
> **Ключевое: в Kotlin нет `static`.** Это сделано намеренно - статика не участвует в полиморфизме и не реализует интерфейсы. Вместо неё либо функции верхнего уровня (для чистых утилит), либо `companion object` (когда член логически принадлежит классу).
|
||||
>
|
||||
> **Синглтон через `object` решает классическую задачу Java.** Корректный синглтон в Java требует либо `enum` с единственной константой, либо holder-класса с двойной проверкой блокировки:
|
||||
> ```java
|
||||
> class Config {
|
||||
> private Config() {}
|
||||
> private static class Holder { static final Config INSTANCE = new Config(); }
|
||||
> static Config getInstance() { return Holder.INSTANCE; }
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin это `object Config { }` - с той же ленивостью и потокобезопасностью.
|
||||
>
|
||||
> **Анонимный класс Java не может реализовать два интерфейса**, объект-выражение Kotlin - может.
|
||||
>
|
||||
> Сравнить: [Singleton в Java](../../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Вложенные классы в Java](../../../Программирование%20на%20Java/ООП/Вложенные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`object` - глобальное изменяемое состояние, если в нём есть `var`:**
|
||||
> ```kotlin
|
||||
> object Counter {
|
||||
> var count = 0 // общий на всю программу, не потокобезопасен
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> Counter.count++
|
||||
> println(Counter.count) // 1
|
||||
> }
|
||||
> ```
|
||||
> Инкремент из нескольких потоков потеряет обновления. Нужен `AtomicInteger` или синхронизация. Подробнее: [Разделяемое состояние](../../Корутины/Разделяемое%20состояние.md)
|
||||
>
|
||||
> **Синглтон затрудняет тестирование.** Состояние `object` переживает тесты, и порядок их выполнения начинает влиять на результат. Для подменяемых зависимостей лучше внедрение через конструктор.
|
||||
>
|
||||
> **Инициализация `object` ленивая - и порядок может удивить:**
|
||||
> ```kotlin
|
||||
> object A {
|
||||
> val value = B.value + 1
|
||||
> }
|
||||
> object B {
|
||||
> val value = 10
|
||||
> }
|
||||
>
|
||||
> fun main() { println(A.value) } // 11
|
||||
> ```
|
||||
> При круговой зависимости между объектами одно из значений окажется `null` или `0`, без всякой ошибки.
|
||||
>
|
||||
> **`const val` нельзя применить к сложному типу:**
|
||||
> ```kotlin
|
||||
> object Config {
|
||||
> const val NAME = "app" // ok
|
||||
> // const val LIST = listOf(1) // ошибка: только примитивы и String
|
||||
> val LIST = listOf(1) // так
|
||||
> }
|
||||
>
|
||||
> fun main() { println("${Config.NAME} ${Config.LIST}") }
|
||||
> ```
|
||||
>
|
||||
> **Компаньон не наследуется.** У подкласса нет доступа к компаньону родителя как к своему:
|
||||
> ```kotlin
|
||||
> open class Base {
|
||||
> companion object { fun create() = "из Base" }
|
||||
> }
|
||||
> class Child : Base()
|
||||
>
|
||||
> fun main() {
|
||||
> println(Base.create()) // из Base
|
||||
> // println(Child.create()) // ошибка: unresolved reference
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Тип объекта-выражения анонимен.** Если функция публичная, её тип результата схлопнется до супертипа (или `Any`), и дополнительные члены станут недоступны.
|
||||
>
|
||||
> **Из Java члены компаньона видны через `Companion`:**
|
||||
> ```kotlin
|
||||
> class Config {
|
||||
> companion object {
|
||||
> fun helper() = "без JvmStatic"
|
||||
> @JvmStatic fun helper2() = "с JvmStatic"
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> fun main() { println("${Config.helper()} / ${Config.helper2()}") }
|
||||
> ```
|
||||
> Java-код вызывает первую как `Config.Companion.helper()`, вторую - как `Config.helper2()`.
|
||||
>
|
||||
> **`object` внутри класса - не то же, что `companion object`.** Вложенный `object` требует обращения через своё имя: `Outer.Inner.member`.
|
||||
|
||||
Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md)
|
||||
Далее: [Typealias](Typealias.md)
|
||||
@@ -1,294 +0,0 @@
|
||||
---
|
||||
title: Sealed class и interface
|
||||
description: Закрытые иерархии, исчерпывающий when, выбор между sealed class и interface
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
`sealed` означает: все наследники известны компилятору. Новый наследник может появиться только в том же модуле и пакете.
|
||||
|
||||
```kotlin
|
||||
sealed interface Result2
|
||||
data class Success(val data: String) : Result2
|
||||
data class Failure(val error: Throwable) : Result2
|
||||
data object Loading : Result2
|
||||
|
||||
fun render(r: Result2): String = when (r) {
|
||||
is Success -> "данные: ${r.data}"
|
||||
is Failure -> "ошибка: ${r.error.message}"
|
||||
Loading -> "загрузка"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(render(Success("ok"))) // данные: ok
|
||||
println(render(Failure(RuntimeException("сбой")))) // ошибка: сбой
|
||||
println(render(Loading)) // загрузка
|
||||
}
|
||||
```
|
||||
`else` не нужен: компилятор видит все три варианта. Добавление четвёртого сломает компиляцию во всех `when` - это и есть цель.
|
||||
|
||||
## Зачем это нужно
|
||||
`sealed` - способ выразить «одно из» в системе типов. Без него альтернативой был бы класс со всеми полями сразу и флагом:
|
||||
```kotlin
|
||||
// плохо: половина полей всегда null
|
||||
class BadResult(val data: String?, val error: Throwable?, val loading: Boolean)
|
||||
```
|
||||
Такой класс позволяет создать бессмысленные состояния: данные и ошибка одновременно. `sealed` их запрещает на уровне типов.
|
||||
|
||||
## sealed class или sealed interface
|
||||
```kotlin
|
||||
sealed class Shape {
|
||||
abstract val area: Double // общее состояние и поведение
|
||||
}
|
||||
class Circle(val r: Double) : Shape() {
|
||||
override val area get() = Math.PI * r * r
|
||||
}
|
||||
class Square(val side: Double) : Shape() {
|
||||
override val area get() = side * side
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val shapes: List<Shape> = listOf(Circle(1.0), Square(2.0))
|
||||
println(shapes.sumOf { it.area }) // 7.141592653589793
|
||||
}
|
||||
```
|
||||
|
||||
| | `sealed class` | `sealed interface` |
|
||||
|---|---|---|
|
||||
| состояние (свойства с полем) | можно | нет |
|
||||
| конструктор | есть | нет |
|
||||
| наследник может входить в другую иерархию | нет (один супертип-класс) | да |
|
||||
| `data class` как наследник | да | да |
|
||||
|
||||
Правило: берите `sealed interface`, если общего состояния нет. Он гибче - один тип может реализовать несколько sealed-интерфейсов:
|
||||
```kotlin
|
||||
sealed interface Error2
|
||||
sealed interface Retryable
|
||||
|
||||
data class Timeout(val seconds: Int) : Error2, Retryable
|
||||
data class NotFound(val id: Int) : Error2
|
||||
|
||||
fun describe(e: Error2) = when (e) {
|
||||
is Timeout -> "таймаут, можно повторить: ${e is Retryable}"
|
||||
is NotFound -> "не найдено ${e.id}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe(Timeout(30))) // таймаут, можно повторить: true
|
||||
println(describe(NotFound(1))) // не найдено 1
|
||||
}
|
||||
```
|
||||
|
||||
## Где можно объявлять наследников
|
||||
С Kotlin 1.5 наследники должны быть в том же **модуле** и **пакете**, но не обязательно в том же файле:
|
||||
```kotlin
|
||||
// файл Result.kt
|
||||
sealed interface Result2
|
||||
// файл Success.kt — тот же пакет, тот же модуль
|
||||
// data class Success(val data: String) : Result2
|
||||
```
|
||||
Раньше требовался один файл. Сейчас обычно всё равно держат в одном - иерархию видно целиком.
|
||||
|
||||
Вложенное объявление - распространённый стиль:
|
||||
```kotlin
|
||||
sealed interface NetworkResult {
|
||||
data class Ok(val body: String) : NetworkResult
|
||||
data class HttpError(val code: Int) : NetworkResult
|
||||
data object Offline : NetworkResult
|
||||
}
|
||||
|
||||
fun handle(r: NetworkResult) = when (r) {
|
||||
is NetworkResult.Ok -> r.body
|
||||
is NetworkResult.HttpError -> "HTTP ${r.code}"
|
||||
NetworkResult.Offline -> "нет сети"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle(NetworkResult.HttpError(404))) // HTTP 404
|
||||
}
|
||||
```
|
||||
Плюс: имена не загрязняют пакет и читаются как `NetworkResult.Offline`.
|
||||
|
||||
## Исчерпывающий when
|
||||
Проверка работает и для `when`-инструкции (без результата) - с Kotlin 2.0:
|
||||
```kotlin
|
||||
sealed interface Event
|
||||
data object Start : Event
|
||||
data object Stop : Event
|
||||
|
||||
fun log(e: Event) {
|
||||
when (e) { // инструкция, не выражение
|
||||
Start -> println("старт")
|
||||
Stop -> println("стоп")
|
||||
} // else не нужен, полнота проверяется
|
||||
}
|
||||
|
||||
fun main() { log(Start) } // старт
|
||||
```
|
||||
|
||||
Для nullable-типа нужна ветка `null`:
|
||||
```kotlin
|
||||
sealed interface Event
|
||||
data object Start : Event
|
||||
|
||||
fun log(e: Event?) = when (e) {
|
||||
Start -> "старт"
|
||||
null -> "ничего"
|
||||
}
|
||||
|
||||
fun main() { println(log(null)) } // ничего
|
||||
```
|
||||
|
||||
## Иерархия глубже одного уровня
|
||||
Промежуточный узел тоже может быть `sealed`:
|
||||
```kotlin
|
||||
sealed interface Expr
|
||||
data class Num(val value: Int) : Expr
|
||||
sealed interface BinOp : Expr {
|
||||
val left: Expr
|
||||
val right: Expr
|
||||
}
|
||||
data class Add(override val left: Expr, override val right: Expr) : BinOp
|
||||
data class Mul(override val left: Expr, override val right: Expr) : BinOp
|
||||
|
||||
fun eval(e: Expr): Int = when (e) {
|
||||
is Num -> e.value
|
||||
is Add -> eval(e.left) + eval(e.right)
|
||||
is Mul -> eval(e.left) * eval(e.right)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(eval(Add(Num(2), Mul(Num(3), Num(4))))) // 14
|
||||
}
|
||||
```
|
||||
Можно сопоставлять и на промежуточном уровне:
|
||||
```kotlin
|
||||
sealed interface Expr
|
||||
data class Num(val value: Int) : Expr
|
||||
sealed interface BinOp : Expr
|
||||
data class Add(val l: Expr, val r: Expr) : BinOp
|
||||
data class Mul(val l: Expr, val r: Expr) : BinOp
|
||||
|
||||
fun kind(e: Expr) = when (e) {
|
||||
is Num -> "литерал"
|
||||
is BinOp -> "операция" // покрывает Add и Mul
|
||||
}
|
||||
|
||||
fun main() { println(kind(Add(Num(1), Num(2)))) } // операция
|
||||
```
|
||||
|
||||
## Перебор наследников
|
||||
```kotlin
|
||||
sealed interface Status {
|
||||
data object Active : Status
|
||||
data object Blocked : Status
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Status::class.sealedSubclasses.map { it.simpleName })
|
||||
// [Active, Blocked]
|
||||
}
|
||||
```
|
||||
Работает через рефлексию и требует `kotlin-reflect`. Для фиксированных объектов проще держать список руками. Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md)
|
||||
|
||||
## Типичные применения
|
||||
**Результат операции** вместо исключений:
|
||||
```kotlin
|
||||
sealed interface Parsed<out T> {
|
||||
data class Ok<T>(val value: T) : Parsed<T>
|
||||
data class Err(val message: String) : Parsed<Nothing>
|
||||
}
|
||||
|
||||
fun parse(s: String): Parsed<Int> =
|
||||
s.toIntOrNull()?.let { Parsed.Ok(it) } ?: Parsed.Err("не число: $s")
|
||||
|
||||
fun main() {
|
||||
println(when (val r = parse("abc")) {
|
||||
is Parsed.Ok -> "значение ${r.value}"
|
||||
is Parsed.Err -> r.message
|
||||
}) // не число: abc
|
||||
}
|
||||
```
|
||||
Обратите внимание на `Parsed<Nothing>` для варианта без данных - это работает благодаря `out T` и тому, что `Nothing` подтип всего. Подробнее: [Any, Unit, Nothing](Any,%20Unit,%20Nothing.md), [Вариантность](../../Дженерики/Вариантность.md)
|
||||
|
||||
**Состояние UI**, **события**, **команды**, **узлы AST** - везде, где набор вариантов фиксирован и у каждого свои данные.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java аналог появился в 17 - `sealed` с явным `permits`:
|
||||
> ```java
|
||||
> sealed interface Result permits Success, Failure {}
|
||||
> record Success(String data) implements Result {}
|
||||
> record Failure(Throwable error) implements Result {}
|
||||
> ```
|
||||
>
|
||||
> | Java 17+ | Kotlin |
|
||||
> |---|---|
|
||||
> | `sealed interface X permits A, B` | `sealed interface X` (список выводится) |
|
||||
> | наследники в том же пакете/модуле | то же |
|
||||
> | `switch` с pattern matching (Java 21) | `when` с `is` |
|
||||
> | проверка полноты в `switch` (Java 21) | есть всегда |
|
||||
> | `non-sealed` для открытия ветки | нет аналога |
|
||||
>
|
||||
> Kotlin не требует перечислять наследников: компилятор находит их сам в пределах модуля. Это удобнее при добавлении варианта - править объявление не нужно.
|
||||
>
|
||||
> До Java 17 то же выражали абстрактным классом с приватным конструктором или паттерном Visitor. Visitor даёт проверку полноты (интерфейс с методом на каждый тип), но требует много кода и меняет все реализации при добавлении варианта.
|
||||
>
|
||||
> Сравнить: [Абстрактные классы в Java](../../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md), [Полиморфизм подтипов](../../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`else` в `when` убивает проверку полноты.** Главная ошибка:
|
||||
> ```kotlin
|
||||
> sealed interface Event
|
||||
> data object Start : Event
|
||||
> data object Stop : Event
|
||||
>
|
||||
> fun log(e: Event) = when (e) {
|
||||
> Start -> "старт"
|
||||
> else -> "прочее" // добавление варианта пройдёт незамеченным
|
||||
> }
|
||||
>
|
||||
> fun main() { println(log(Stop)) } // прочее
|
||||
> ```
|
||||
> Если ветвей много и большинство обрабатывается одинаково, лучше перечислить их через запятую, чем писать `else`.
|
||||
>
|
||||
> **Проверка полноты не работает из Java.** Java-код, читающий sealed-иерархию Kotlin, обязан иметь `default` в `switch`.
|
||||
>
|
||||
> **`when` по `is` без исчерпывающего контекста не проверяется.** Полнота требуется только там, где `when` - выражение или где компилятор знает, что иерархия закрыта. Для не-sealed типа проверки нет вообще.
|
||||
>
|
||||
> **`sealed class` требует вызова конструктора в наследнике:**
|
||||
> ```kotlin
|
||||
> sealed class Base(val id: Int)
|
||||
> class Child(id: Int) : Base(id) // скобки обязательны
|
||||
>
|
||||
> fun main() { println(Child(1).id) } // 1
|
||||
> ```
|
||||
> У `sealed interface` конструктора нет - на одну причину для ошибки меньше.
|
||||
>
|
||||
> **Наследник в другом модуле не скомпилируется**, даже если пакет совпадает. Это ограничивает расширяемость библиотечного API: если вариантов должно быть больше, `sealed` не подходит.
|
||||
>
|
||||
> **`sealed` класс сам абстрактен** - создать его нельзя:
|
||||
> ```kotlin
|
||||
> sealed class Shape
|
||||
> // val s = Shape() // ошибка: sealed-класс абстрактный
|
||||
> class Circle : Shape()
|
||||
>
|
||||
> fun main() { println(Circle()::class.simpleName) } // Circle
|
||||
> ```
|
||||
>
|
||||
> **Для варианта без данных нужен `data object`, а не `class`:**
|
||||
> ```kotlin
|
||||
> sealed interface State
|
||||
> class Loading1 : State // каждый экземпляр разный
|
||||
> data object Loading2 : State // единственный экземпляр
|
||||
>
|
||||
> fun main() {
|
||||
> println(Loading1() == Loading1()) // false — ловушка
|
||||
> println(Loading2 == Loading2) // true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Дженерик-наследник sealed-типа требует внимания к вариантности.** Без `out` вариант без данных нельзя объявить как `Err : Parsed<Nothing>`.
|
||||
|
||||
Дополнительно: [Enum class](Enum%20class.md)
|
||||
Далее: [Value class](Value%20class.md)
|
||||
@@ -1,228 +0,0 @@
|
||||
---
|
||||
title: Typealias
|
||||
description: Псевдоним типа для сокращения записи, его отличие от value class и границы применения
|
||||
tags:
|
||||
- kotlin
|
||||
- types
|
||||
---
|
||||
|
||||
`typealias` даёт типу другое имя. **Нового типа не создаётся** - это сокращение для чтения.
|
||||
|
||||
```kotlin
|
||||
typealias Name = String
|
||||
|
||||
fun greet(name: Name) = "Привет, $name"
|
||||
|
||||
fun main() {
|
||||
println(greet("Иван")) // Привет, Иван — обычная строка подходит
|
||||
val n: Name = "Пётр"
|
||||
val s: String = n // взаимозаменяемы
|
||||
println(s)
|
||||
}
|
||||
```
|
||||
|
||||
## Где полезно
|
||||
**Длинные дженерик-типы:**
|
||||
```kotlin
|
||||
typealias UsersByCity = Map<String, List<Pair<Int, String>>>
|
||||
|
||||
fun process(data: UsersByCity) = data.keys.sorted()
|
||||
|
||||
fun main() {
|
||||
val data: UsersByCity = mapOf("Москва" to listOf(1 to "Иван"))
|
||||
println(process(data)) // [Москва]
|
||||
}
|
||||
```
|
||||
|
||||
**Функциональные типы:**
|
||||
```kotlin
|
||||
typealias ClickHandler = (x: Int, y: Int) -> Unit
|
||||
typealias Validator<T> = (T) -> String?
|
||||
|
||||
fun onClick(handler: ClickHandler) = handler(1, 2)
|
||||
|
||||
fun main() {
|
||||
onClick { x, y -> println("клик $x,$y") } // клик 1,2
|
||||
|
||||
val notEmpty: Validator<String> =
|
||||
{ if (it.isEmpty()) "пусто" else null }
|
||||
println(notEmpty("")) // пусто
|
||||
println(notEmpty("ok")) // null
|
||||
}
|
||||
```
|
||||
Имена параметров в псевдониме функционального типа - документация: `(x: Int, y: Int) -> Unit` понятнее, чем `(Int, Int) -> Unit`.
|
||||
|
||||
**Разрешение конфликта имён при импорте:**
|
||||
```kotlin
|
||||
import java.util.Date
|
||||
typealias SqlDate = java.sql.Date
|
||||
|
||||
fun main() {
|
||||
val a: Date = Date()
|
||||
val b: SqlDate = SqlDate(0)
|
||||
println("${a::class.simpleName} ${b::class.simpleName}") // Date Date
|
||||
}
|
||||
```
|
||||
Два класса с одинаковым простым именем нельзя импортировать оба - псевдоним решает это без полных имён в коде.
|
||||
|
||||
**Сокращение вложенных имён:**
|
||||
```kotlin
|
||||
class Outer {
|
||||
class Inner {
|
||||
class Deep { fun hello() = "привет" }
|
||||
}
|
||||
}
|
||||
|
||||
typealias Deep = Outer.Inner.Deep
|
||||
|
||||
fun main() {
|
||||
println(Deep().hello()) // привет
|
||||
}
|
||||
```
|
||||
|
||||
## Дженерики
|
||||
Псевдоним может иметь свои параметры типа:
|
||||
```kotlin
|
||||
typealias Handler<T> = (T) -> Unit
|
||||
typealias Dict<V> = Map<String, V>
|
||||
typealias IntDict = Dict<Int> // частичная подстановка
|
||||
|
||||
fun main() {
|
||||
val h: Handler<String> = { println(it.uppercase()) }
|
||||
h("привет") // ПРИВЕТ
|
||||
|
||||
val d: IntDict = mapOf("a" to 1)
|
||||
println(d["a"]) // 1
|
||||
}
|
||||
```
|
||||
|
||||
## Только верхний уровень
|
||||
```kotlin
|
||||
typealias Ok = String // так
|
||||
|
||||
class Wrong {
|
||||
// typealias Bad = String // ошибка: псевдоним только на верхнем уровне
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val x: Ok = "ok"
|
||||
println(x)
|
||||
}
|
||||
```
|
||||
Внутри класса, функции или `object` объявить псевдоним нельзя.
|
||||
|
||||
## typealias и value class
|
||||
Это часто путают, а задачи у них противоположные:
|
||||
|
||||
```kotlin
|
||||
typealias UserId = Int
|
||||
@JvmInline value class AccountId(val value: Int)
|
||||
|
||||
fun loadUser(id: UserId) = "user $id"
|
||||
fun loadAccount(id: AccountId) = "account ${id.value}"
|
||||
|
||||
fun main() {
|
||||
println(loadUser(42)) // 42 — любое Int подойдёт
|
||||
println(loadAccount(AccountId(42))) // нужен именно AccountId
|
||||
// println(loadAccount(42)) // не скомпилируется
|
||||
}
|
||||
```
|
||||
|
||||
| | `typealias` | `value class` |
|
||||
|---|---|---|
|
||||
| новый тип | нет | да |
|
||||
| ошибку в аргументах ловит | нет | да |
|
||||
| накладные расходы | нет | обычно нет |
|
||||
| нужна распаковка (`.value`) | нет | да |
|
||||
| можно добавить методы | нет | да |
|
||||
| видно в байт-коде | нет | mangling имён |
|
||||
|
||||
Правило: если нужна **читаемость** - `typealias`. Если нужна **защита от перепутанных аргументов** - `value class`. Подробнее: [Value class](Value%20class.md)
|
||||
|
||||
## Псевдоним не скрывает реализацию
|
||||
```kotlin
|
||||
typealias Cache = HashMap<String, Int>
|
||||
|
||||
fun main() {
|
||||
val c: Cache = HashMap()
|
||||
println(c is HashMap<*, *>) // true — это и есть HashMap
|
||||
println(c::class.simpleName) // HashMap
|
||||
}
|
||||
```
|
||||
`typealias` нельзя использовать как абстракцию: клиент видит настоящий тип, полагается на его методы, и заменить реализацию не получится. Для этого нужен интерфейс.
|
||||
|
||||
## Когда не стоит
|
||||
```kotlin
|
||||
typealias S = String // экономия букв без смысла
|
||||
typealias MyList = List<Int> // List<Int> и так понятен
|
||||
```
|
||||
Псевдоним добавляет уровень косвенности: читатель должен найти объявление, чтобы понять настоящий тип. Он оправдан, когда сокращает реально длинную запись или даёт имя смысловой роли (`Validator`, `ClickHandler`), а не когда просто короче.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java псевдонимов типов нет вообще. Длинные дженерик-типы приходится либо выписывать целиком, либо прятать за интерфейсом или наследованием:
|
||||
> ```java
|
||||
> // единственный способ "сократить" — создать настоящий тип
|
||||
> class UsersByCity extends HashMap<String, List<Pair<Integer, String>>> {}
|
||||
> ```
|
||||
> Наследование от коллекции - плохая практика: тип получает весь API родителя и наружу просачивается реализация. Частичное облегчение даёт `var` (Java 10+), но только для локальных переменных:
|
||||
> ```java
|
||||
> var data = new HashMap<String, List<String>>(); // тип не пишем дважды
|
||||
> ```
|
||||
>
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | нет аналога | `typealias X = Y` |
|
||||
> | `extends HashMap<...>` как сокращение | `typealias` без наследования |
|
||||
> | `var` для локальных переменных | вывод типов + `typealias` |
|
||||
> | полное имя при конфликте импортов | `typealias` для одного из классов |
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Типобезопасности нет.** Это главное заблуждение:
|
||||
> ```kotlin
|
||||
> typealias Meters = Int
|
||||
> typealias Seconds = Int
|
||||
>
|
||||
> fun speed(distance: Meters, time: Seconds) = distance / time
|
||||
>
|
||||
> fun main() {
|
||||
> println(speed(100, 10)) // 10
|
||||
> val t: Seconds = 10
|
||||
> val d: Meters = 100
|
||||
> println(speed(t, d)) // 0 — аргументы перепутаны, компилятор молчит
|
||||
> }
|
||||
> ```
|
||||
> Для настоящей защиты нужен `value class`.
|
||||
>
|
||||
> **Псевдоним не наследуется и не расширяется.** Добавить метод «к `Meters`» нельзя - расширение попадёт на `Int`:
|
||||
> ```kotlin
|
||||
> typealias Meters = Int
|
||||
>
|
||||
> fun Meters.describe() = "$this м" // это расширение Int!
|
||||
>
|
||||
> fun main() {
|
||||
> println(42.describe()) // 42 м — работает для любого Int
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **В сообщениях компилятора и в стектрейсах видно настоящий тип**, а не псевдоним. При разборе ошибки `expected Map<String, List<Pair<Int, String>>>` придётся сопоставлять его с `UsersByCity` вручную.
|
||||
>
|
||||
> **Из Java псевдоним не виден.** В сигнатурах, доступных Java-коду, стоит раскрытый тип.
|
||||
>
|
||||
> **Псевдоним на nullable-тип легко спутать:**
|
||||
> ```kotlin
|
||||
> typealias MaybeName = String?
|
||||
>
|
||||
> fun greet(n: MaybeName) = "Привет, ${n ?: "гость"}"
|
||||
>
|
||||
> fun main() {
|
||||
> println(greet(null)) // Привет, гость
|
||||
> }
|
||||
> ```
|
||||
> По имени `MaybeName` не очевидно, что тип nullable. Лучше, когда `?` виден в сигнатуре.
|
||||
>
|
||||
> **Нельзя объявить внутри класса или функции** - только на верхнем уровне файла.
|
||||
|
||||
Дополнительно: [Value class](Value%20class.md)
|
||||
Далее: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md)
|
||||
@@ -1,265 +0,0 @@
|
||||
---
|
||||
title: Value class
|
||||
description: Обёртка без аллокации, типобезопасность примитивов, mangling имён и границы инлайнинга
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
- performance
|
||||
---
|
||||
|
||||
`value class` - обёртка над одним значением, которая в байт-коде обычно **исчезает**: вместо объекта остаётся само значение.
|
||||
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class UserId(val value: Int)
|
||||
|
||||
fun load(id: UserId) = "пользователь ${id.value}"
|
||||
|
||||
fun main() {
|
||||
val id = UserId(42)
|
||||
println(load(id)) // пользователь 42
|
||||
println(id.value) // 42
|
||||
}
|
||||
```
|
||||
В скомпилированном коде `load` принимает `int`, а не объект. Аллокации нет.
|
||||
|
||||
Аннотация `@JvmInline` обязательна на JVM - без неё компилятор выдаст ошибку.
|
||||
|
||||
## Зачем
|
||||
Основная причина - **типобезопасность без накладных расходов**. Классическая проблема:
|
||||
```kotlin
|
||||
fun transfer(from: Int, to: Int, amount: Int) = "$from -> $to: $amount"
|
||||
|
||||
fun main() {
|
||||
println(transfer(100, 1, 2)) // перепутали порядок — компилятор молчит
|
||||
}
|
||||
```
|
||||
Все параметры `Int`, поэтому их легко перепутать. С `value class` такая ошибка становится ошибкой компиляции:
|
||||
```kotlin
|
||||
@JvmInline value class AccountId(val value: Int)
|
||||
@JvmInline value class Amount(val cents: Int)
|
||||
|
||||
fun transfer(from: AccountId, to: AccountId, amount: Amount) =
|
||||
"${from.value} -> ${to.value}: ${amount.cents}"
|
||||
|
||||
fun main() {
|
||||
println(transfer(AccountId(1), AccountId(2), Amount(100)))
|
||||
// println(transfer(Amount(100), AccountId(1), AccountId(2))) // не скомпилируется
|
||||
}
|
||||
```
|
||||
Цена - ноль: в байт-коде всё те же три `int`.
|
||||
|
||||
## Что можно внутри
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class Email(val value: String) {
|
||||
init {
|
||||
require(value.contains("@")) { "неверный email: $value" }
|
||||
}
|
||||
|
||||
val domain: String get() = value.substringAfter("@")
|
||||
fun masked() = value.replaceRange(0, value.indexOf("@"), "***")
|
||||
|
||||
companion object {
|
||||
fun parse(s: String): Email? = if (s.contains("@")) Email(s) else null
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val e = Email("ivan@example.com")
|
||||
println(e.domain) // example.com
|
||||
println(e.masked()) // ***@example.com
|
||||
println(Email.parse("нет")) // null
|
||||
}
|
||||
```
|
||||
Разрешены: `init`, свойства с геттером, функции, `companion object`, реализация интерфейсов.
|
||||
|
||||
Запрещены: второе свойство с полем, `var`-свойства с полем, наследование, блоки инициализации полей, `lateinit`.
|
||||
|
||||
```kotlin
|
||||
// @JvmInline value class Bad(val a: Int, val b: Int) // ошибка: только одно поле
|
||||
// @JvmInline value class Bad2(var a: Int) // ошибка: нужен val
|
||||
```
|
||||
|
||||
## Реализация интерфейса
|
||||
```kotlin
|
||||
interface Printable { fun print2(): String }
|
||||
|
||||
@JvmInline
|
||||
value class Code(val value: Int) : Printable {
|
||||
override fun print2() = "код $value"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Code(7).print2()) // код 7
|
||||
val p: Printable = Code(7) // упаковка! см. ниже
|
||||
println(p.print2()) // код 7
|
||||
}
|
||||
```
|
||||
|
||||
## Когда обёртка всё-таки создаётся
|
||||
Инлайнинг не бесплатен в смысле предсказуемости: в некоторых позициях объект приходится создать.
|
||||
|
||||
| Позиция | Инлайнится |
|
||||
|---|---|
|
||||
| параметр и результат функции | да |
|
||||
| локальная переменная | да |
|
||||
| nullable-тип (`UserId?`) | **нет** |
|
||||
| приведение к супертипу/интерфейсу | **нет** |
|
||||
| элемент коллекции (`List<UserId>`) | **нет** - дженерики стираются |
|
||||
| поле дженерик-класса | **нет** |
|
||||
|
||||
```kotlin
|
||||
@JvmInline value class UserId(val value: Int)
|
||||
|
||||
fun main() {
|
||||
val a: UserId = UserId(1) // int, без объекта
|
||||
val b: UserId? = UserId(1) // объект UserId
|
||||
val c: List<UserId> = listOf(UserId(1)) // объекты внутри списка
|
||||
println("${a.value} ${b?.value} ${c.size}")
|
||||
}
|
||||
```
|
||||
То есть `List<UserId>` по памяти не лучше `List<Integer>`. Выгода - в типобезопасности, а не в аллокациях.
|
||||
|
||||
## Mangling имён
|
||||
Чтобы разные `value class` над одним типом не давали одинаковые JVM-сигнатуры, компилятор добавляет суффикс к имени метода:
|
||||
```kotlin
|
||||
@JvmInline value class AccountId(val value: Int)
|
||||
|
||||
fun process(id: AccountId) = id.value // в байт-коде: process-<хеш>(I)I
|
||||
|
||||
fun main() { println(process(AccountId(1))) } // 1
|
||||
```
|
||||
Из Java такую функцию вызвать практически нельзя - имя непредсказуемо. Решение - `@JvmName`:
|
||||
```kotlin
|
||||
@JvmInline value class AccountId(val value: Int)
|
||||
|
||||
@JvmName("processAccount")
|
||||
fun process(id: AccountId) = id.value
|
||||
|
||||
fun main() { println(process(AccountId(1))) } // 1
|
||||
```
|
||||
Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
## Равенство
|
||||
Сравнение идёт по содержимому, причём без упаковки:
|
||||
```kotlin
|
||||
@JvmInline value class UserId(val value: Int)
|
||||
|
||||
fun main() {
|
||||
println(UserId(1) == UserId(1)) // true
|
||||
println(UserId(1) == UserId(2)) // false
|
||||
println(setOf(UserId(1), UserId(1)).size) // 1
|
||||
}
|
||||
```
|
||||
`equals`/`hashCode` генерируются автоматически и переопределять их нельзя.
|
||||
|
||||
## value class и data class
|
||||
```kotlin
|
||||
@JvmInline value class Meters(val value: Double)
|
||||
data class Point(val x: Int, val y: Int)
|
||||
```
|
||||
`value class` - одно поле, без аллокации, нет `copy`. `data class` - любое число полей, всегда объект, есть `copy` и `componentN`.
|
||||
|
||||
Для одного поля `data class` тоже работает, но создаёт объект:
|
||||
```kotlin
|
||||
data class SlowId(val value: Int) // объект на каждый вызов
|
||||
@JvmInline value class FastId(val value: Int) // int
|
||||
|
||||
fun main() {
|
||||
println(SlowId(1) == SlowId(1)) // true, но два объекта были созданы
|
||||
println(FastId(1) == FastId(1)) // true, объектов не было
|
||||
}
|
||||
```
|
||||
|
||||
## typealias - не замена
|
||||
```kotlin
|
||||
typealias UserId2 = Int
|
||||
|
||||
fun load(id: UserId2) = id
|
||||
|
||||
fun main() {
|
||||
println(load(42)) // 42 — любое Int подходит, защиты нет
|
||||
}
|
||||
```
|
||||
`typealias` - только псевдоним, он не создаёт новый тип и не защищает от путаницы. Подробнее: [Typealias](Typealias.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> Прямого аналога нет. Java-разработчик выбирает между:
|
||||
> - **обычным классом-обёрткой** - типобезопасно, но аллокация на каждый объект
|
||||
> - **примитивом с комментарием** - быстро, но ошибки порядка аргументов не ловятся
|
||||
>
|
||||
> ```java
|
||||
> // обёртка: безопасно, но объект
|
||||
> final class UserId {
|
||||
> private final int value;
|
||||
> UserId(int value) { this.value = value; }
|
||||
> // equals, hashCode, геттер...
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> В Java это закрывает Project Valhalla (`value class` в JEP 401), но он ещё не вышел в релиз. Пока единственный способ получить безопасность без аллокации - `value class` Kotlin.
|
||||
>
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | класс-обёртка + аллокация | `@JvmInline value class` |
|
||||
> | `int` напрямую | `value class` с тем же байт-кодом |
|
||||
> | Valhalla (в разработке) | работает с Kotlin 1.5 |
|
||||
>
|
||||
> Сравнить: [Классы-обертки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Классы-обертки.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`@JvmInline` обязательна.** Без неё на JVM компилятор выдаст ошибку, а не предупреждение:
|
||||
> ```kotlin
|
||||
> // value class Bad(val x: Int) // error: value class without @JvmInline
|
||||
> @JvmInline value class Good(val x: Int)
|
||||
>
|
||||
> fun main() { println(Good(1).x) }
|
||||
> ```
|
||||
>
|
||||
> **Nullable-тип отменяет инлайнинг.** `UserId?` - настоящий объект:
|
||||
> ```kotlin
|
||||
> @JvmInline value class UserId(val value: Int)
|
||||
>
|
||||
> fun find(id: UserId?): String = id?.value?.toString() ?: "нет"
|
||||
>
|
||||
> fun main() { println(find(UserId(1))) } // 1 — но обёртка была создана
|
||||
> ```
|
||||
> В горячем коде вместо `UserId?` лучше использовать значение-маркер.
|
||||
>
|
||||
> **В коллекциях выгоды нет.** `List<UserId>` хранит объекты из-за стирания типов.
|
||||
>
|
||||
> **`init` вызывается, но не защищает от Java.** Java-код может создать экземпляр в обход проверки, потому что на уровне байт-кода это просто `int`:
|
||||
> ```kotlin
|
||||
> @JvmInline
|
||||
> value class Age(val value: Int) {
|
||||
> init { require(value >= 0) { "возраст отрицательный" } }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Age(5).value) // 5
|
||||
> // Age(-1) // IllegalArgumentException из Kotlin
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Mangling ломает вызов из Java и из фреймворков, работающих через рефлексию.** Jackson, Spring, JPA требуют отдельной настройки для `value class`; иногда он просто не поддерживается.
|
||||
>
|
||||
> **Переопределить `equals`, `hashCode` нельзя** - они генерируются. `toString` переопределить можно.
|
||||
>
|
||||
> **Два `value class` над одним типом взаимозаменяемы в байт-коде.** Приведение через `Any` обойдёт проверку типов:
|
||||
> ```kotlin
|
||||
> @JvmInline value class Meters(val v: Int)
|
||||
> @JvmInline value class Feet(val v: Int)
|
||||
>
|
||||
> fun main() {
|
||||
> val m = Meters(10)
|
||||
> val any: Any = m
|
||||
> println((any as Meters).v) // 10
|
||||
> // println((any as Feet).v) // ClassCastException — на уровне Kotlin проверка есть
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Нельзя наследоваться и нельзя быть `open`** - иерархии из `value class` не построить.
|
||||
|
||||
Дополнительно: [Typealias](Typealias.md)
|
||||
Далее: [Object и companion object](Object%20и%20companion%20object.md)
|
||||
@@ -1,320 +0,0 @@
|
||||
---
|
||||
title: equals, hashCode и toString
|
||||
description: Контракт равенства, == против ===, правила hashCode и как не сломать HashMap
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Три метода объявлены в [Any](Any,%20Unit,%20Nothing.md) и есть у каждого объекта. Их реализация по умолчанию - по ссылке и по адресу в памяти.
|
||||
|
||||
```kotlin
|
||||
class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val a = Point(1, 2)
|
||||
val b = Point(1, 2)
|
||||
println(a == b) // false — equals по ссылке
|
||||
println(a) // Point@1b6d3586 — toString по хешу
|
||||
}
|
||||
```
|
||||
|
||||
## == и ===
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = "Kotlin"
|
||||
val b = StringBuilder("Kotlin").toString()
|
||||
println(a == b) // true — структурное равенство, вызывает equals
|
||||
println(a === b) // false — ссылочное равенство, тот же объект?
|
||||
println(a !== b) // true
|
||||
}
|
||||
```
|
||||
|
||||
| Оператор | Что делает |
|
||||
|---|---|
|
||||
| `==` | `a?.equals(b) ?: (b === null)` |
|
||||
| `===` | сравнение ссылок (для примитивов - значений) |
|
||||
|
||||
`==` безопасен для `null`: левый операнд может быть `null`, NPE не будет.
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a: String? = null
|
||||
println(a == "x") // false — без исключения
|
||||
println(a == null) // true
|
||||
println(null == null) // true
|
||||
}
|
||||
```
|
||||
|
||||
## Контракт equals
|
||||
Реализация должна быть:
|
||||
- **рефлексивной**: `a == a` всегда `true`
|
||||
- **симметричной**: `a == b` тогда и только тогда, когда `b == a`
|
||||
- **транзитивной**: `a == b` и `b == c` влечёт `a == c`
|
||||
- **согласованной**: результат не меняется, пока объекты не изменились
|
||||
- `a == null` всегда `false`
|
||||
|
||||
```kotlin
|
||||
class Point(val x: Int, val y: Int) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true // быстрый путь
|
||||
if (other !is Point) return false // проверка типа и null сразу
|
||||
return x == other.x && y == other.y
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = 31 * x + y
|
||||
|
||||
override fun toString() = "Point($x, $y)"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = Point(1, 2)
|
||||
println(a == Point(1, 2)) // true
|
||||
println(a == null) // false
|
||||
println(a) // Point(1, 2)
|
||||
}
|
||||
```
|
||||
`other !is Point` закрывает и `null`, и неверный тип: `is` для `null` даёт `false`.
|
||||
|
||||
Параметр обязан иметь тип `Any?` - иначе это не переопределение, а перегрузка:
|
||||
```kotlin
|
||||
class Bad(val x: Int) {
|
||||
fun equals(other: Bad) = x == other.x // перегрузка! override не стоит
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a: Any = Bad(1)
|
||||
println(a == Bad(1)) // false — вызвался equals из Any
|
||||
}
|
||||
```
|
||||
Компилятор не позволит поставить `override` на такую функцию - это и есть подсказка.
|
||||
|
||||
## Контракт hashCode
|
||||
Единственное жёсткое правило: **равные объекты обязаны иметь равный хеш**.
|
||||
|
||||
```kotlin
|
||||
class Broken(val x: Int) {
|
||||
override fun equals(other: Any?) = other is Broken && other.x == x
|
||||
// hashCode не переопределён — контракт нарушен
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = Broken(1)
|
||||
val set = hashSetOf(a)
|
||||
println(set.contains(Broken(1))) // false — объект "потерян"
|
||||
println(a == Broken(1)) // true — хотя equals говорит "равны"
|
||||
}
|
||||
```
|
||||
`HashSet` сначала ищет бакет по хешу. Хеши разные - поиск даже не доходит до `equals`.
|
||||
|
||||
Обратное неверно: разные объекты **могут** иметь одинаковый хеш (коллизия) - это нормально и лишь замедляет поиск.
|
||||
|
||||
```kotlin
|
||||
class Pair2(val a: Int, val b: Int) {
|
||||
override fun equals(other: Any?) = other is Pair2 && other.a == a && other.b == b
|
||||
override fun hashCode() = 31 * a + b
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Pair2(1, 32).hashCode()) // 63
|
||||
println(Pair2(2, 1).hashCode()) // 63 — коллизия, но всё корректно
|
||||
println(Pair2(1, 32) == Pair2(2, 1)) // false
|
||||
}
|
||||
```
|
||||
|
||||
Стандартный способ считать хеш по нескольким полям:
|
||||
```kotlin
|
||||
class User(val id: Int, val name: String, val tags: List<String>) {
|
||||
override fun equals(other: Any?) =
|
||||
other is User && other.id == id && other.name == name && other.tags == tags
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a = User(1, "Иван", listOf("admin"))
|
||||
println(a == User(1, "Иван", listOf("admin"))) // true
|
||||
println(a.hashCode() == User(1, "Иван", listOf("admin")).hashCode()) // true
|
||||
}
|
||||
```
|
||||
Множитель 31 - соглашение из JDK: простое число, а умножение на него оптимизируется в сдвиг и вычитание.
|
||||
|
||||
Короче - через `Objects.hash`, но он создаёт массив:
|
||||
```kotlin
|
||||
import java.util.Objects
|
||||
|
||||
class User(val id: Int, val name: String) {
|
||||
override fun equals(other: Any?) = other is User && other.id == id && other.name == name
|
||||
override fun hashCode() = Objects.hash(id, name)
|
||||
}
|
||||
|
||||
fun main() { println(User(1, "a") == User(1, "a")) } // true
|
||||
```
|
||||
|
||||
## data class делает это за вас
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val a = Point(1, 2)
|
||||
println(a == Point(1, 2)) // true
|
||||
println(a) // Point(x=1, y=2)
|
||||
println(hashSetOf(a).contains(Point(1, 2))) // true
|
||||
}
|
||||
```
|
||||
В большинстве случаев ручная реализация не нужна - достаточно `data class`. Писать руками приходится, когда: равенство только по части полей, в классе есть массив, или класс - сущность с идентификатором. Подробнее: [Data class](Data%20class.md)
|
||||
|
||||
## toString
|
||||
Единственный из трёх, у которого нет контракта - но есть ожидания. Он попадает в логи и в сообщения об ошибках:
|
||||
```kotlin
|
||||
class Order(val id: Int, val items: List<String>) {
|
||||
override fun toString() = "Order(id=$id, items=${items.size})"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Order(1, listOf("a", "b"))) // Order(id=1, items=2)
|
||||
println("заказ: ${Order(1, emptyList())}") // заказ: Order(id=1, items=0)
|
||||
println(listOf(Order(1, emptyList()))) // [Order(id=1, items=0)]
|
||||
}
|
||||
```
|
||||
Строковый шаблон, конкатенация и печать коллекции - все вызывают `toString`. Поэтому класс без нормального `toString` превращает лог в набор адресов.
|
||||
|
||||
## Равенство и наследование
|
||||
Строгая симметричность при наследовании достигается проверкой точного класса:
|
||||
```kotlin
|
||||
open class Base(val x: Int) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
return x == (other as Base).x
|
||||
}
|
||||
override fun hashCode() = x
|
||||
}
|
||||
|
||||
class Derived(x: Int, val y: Int) : Base(x) {
|
||||
override fun equals(other: Any?) =
|
||||
super.equals(other) && y == (other as Derived).y
|
||||
override fun hashCode() = 31 * super.hashCode() + y
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Base(1) == Derived(1, 2)) // false — разные классы
|
||||
println(Derived(1, 2) == Derived(1, 2)) // true
|
||||
}
|
||||
```
|
||||
С `other is Base` вместо сравнения классов получилась бы асимметрия: `base == derived` истина, `derived == base` ложь. Это нарушение контракта, которое ломает коллекции.
|
||||
|
||||
Проще всего не наследоваться там, где важно равенство - именно поэтому `data class` нельзя сделать `open`.
|
||||
|
||||
## Равенство примитивов и упаковка
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a: Int = 1000
|
||||
val b: Int = 1000
|
||||
println(a == b) // true
|
||||
println(a === b) // true — примитивы, сравнение значений
|
||||
|
||||
val c: Int? = 1000
|
||||
val d: Int? = 1000
|
||||
println(c == d) // true
|
||||
println(c === d) // false! разные объекты Integer
|
||||
}
|
||||
```
|
||||
Nullable-число упаковывается в `Integer`, и `===` сравнивает ссылки. Для значений от -128 до 127 работает кеш JVM, и `===` даёт `true` - то есть результат зависит от величины числа. Для чисел используйте только `==`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `a.equals(b)` | `a == b` |
|
||||
> | `a == b` (ссылки) | `a === b` |
|
||||
> | `Objects.equals(a, b)` для null | `a == b` |
|
||||
> | `@Override boolean equals(Object o)` | `override fun equals(other: Any?)` |
|
||||
> | `o instanceof Point p` (Java 16+) | `other is Point` + умное приведение |
|
||||
> | `Objects.hash(a, b)` | то же, или вручную |
|
||||
> | генерация в IDE или Lombok | `data class` |
|
||||
> | `String.valueOf(obj)` для null | `obj.toString()` работает на `null?` |
|
||||
>
|
||||
> **Главное: `==` в Kotlin вызывает `equals`.** Это убирает самый частый класс ошибок Java - сравнение строк через `==`, которое иногда «работает» из-за пула строк и ломается на строках, собранных в рантайме.
|
||||
>
|
||||
> **`==` не бросает NPE**, в отличие от `a.equals(b)` в Java. Обёртка `Objects.equals` больше не нужна.
|
||||
>
|
||||
> **`toString()` можно звать на `null`:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val s: String? = null
|
||||
> println(s.toString()) // "null" — расширение Any?.toString()
|
||||
> }
|
||||
> ```
|
||||
> В Java `null.toString()` - это NPE, нужен `String.valueOf`.
|
||||
>
|
||||
> **`equals` в Kotlin не перегрузить случайно.** Параметр с типом `Any?` обязателен для `override`, поэтому ошибка «написал `equals(Point)` вместо `equals(Object)`» ловится компилятором.
|
||||
>
|
||||
> Сравнить: [Контракт между equals и hashCode](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Контракт%20между%20equals%20и%20hashCode.md), [HashMap в Java](../../../Программирование%20на%20Java/Коллекции/HashMap.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Переопределить `equals` без `hashCode`** - значит получить объект, который теряется в `HashSet` и `HashMap`. IDE предупреждает, компилятор - нет.
|
||||
>
|
||||
> **Изменяемый объект как ключ `HashMap`:**
|
||||
> ```kotlin
|
||||
> data class Key(var id: Int)
|
||||
>
|
||||
> fun main() {
|
||||
> val key = Key(1)
|
||||
> val map = hashMapOf(key to "значение")
|
||||
> key.id = 2
|
||||
> println(map[key]) // null — ищем в новом бакете
|
||||
> println(map.keys.first()) // Key(id=2) — запись на месте, но недоступна
|
||||
> println(map.size) // 1
|
||||
> }
|
||||
> ```
|
||||
> Запись становится невидимой и неудаляемой. Ключи обязаны быть неизменяемыми.
|
||||
>
|
||||
> **Массив внутри класса ломает `equals` по содержимому:**
|
||||
> ```kotlin
|
||||
> class Packet(val data: ByteArray) {
|
||||
> override fun equals(other: Any?) =
|
||||
> other is Packet && data.contentEquals(other.data)
|
||||
> override fun hashCode() = data.contentHashCode()
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // true
|
||||
> }
|
||||
> ```
|
||||
> Без `contentEquals`/`contentHashCode` сравнение идёт по ссылке. Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md)
|
||||
>
|
||||
> **`===` для чисел зависит от значения:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a: Int? = 127
|
||||
> val b: Int? = 127
|
||||
> println(a === b) // true — из кеша Integer
|
||||
> val c: Int? = 128
|
||||
> val d: Int? = 128
|
||||
> println(c === d) // false — за пределами кеша
|
||||
> }
|
||||
> ```
|
||||
> Это не особенность Kotlin, а кеш `Integer.valueOf` в JVM. Вывод: `===` на числах не использовать.
|
||||
>
|
||||
> **`equals` с `is` вместо проверки класса даёт асимметрию при наследовании** - и вместе с ней непредсказуемое поведение коллекций.
|
||||
>
|
||||
> **`hashCode`, зависящий от изменяемого поля, нарушает согласованность.** Объект должен давать один и тот же хеш всё время, пока лежит в хеш-коллекции.
|
||||
>
|
||||
> **`toString` печатает всё, включая секреты:**
|
||||
> ```kotlin
|
||||
> data class Token(val value: String) {
|
||||
> override fun toString() = "Token(***)"
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Token("secret-abc")) } // Token(***)
|
||||
> ```
|
||||
> `data class` с паролем или токеном попадёт в лог целиком, если не переопределить `toString`.
|
||||
>
|
||||
> **`hashCode` не обязан быть стабильным между запусками JVM.** `Object.hashCode` зависит от адреса, `String.hashCode` стабилен по спецификации, но для своего класса гарантий нет - сохранять хеш в БД нельзя.
|
||||
|
||||
Дополнительно: [Data class](Data%20class.md)
|
||||
Далее: [ООП](../../ООП/ООП.md)
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
title: Hello World!
|
||||
description: Первая программа на Kotlin, функция main и запуск через kotlinc
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("Hello World")
|
||||
}
|
||||
```
|
||||
|
||||
Файл `Hello.kt`. Имя файла **не связано** с содержимым - можно назвать как угодно.
|
||||
|
||||
- `fun` - объявление функции
|
||||
- `main` - точка входа, может быть без параметров
|
||||
- `println` - функция из стандартной библиотеки, не метод класса
|
||||
- Точки с запятой не нужны
|
||||
|
||||
## Точка входа
|
||||
Два допустимых варианта:
|
||||
```kotlin
|
||||
fun main() { } // обычно используют этот
|
||||
fun main(args: Array<String>) { } // если нужны аргументы командной строки
|
||||
```
|
||||
Функция `main` должна быть на верхнем уровне файла (top-level), внутри класса её быть не может.
|
||||
|
||||
## Запуск
|
||||
```bash
|
||||
kotlinc Hello.kt -include-runtime -d hello.jar
|
||||
java -jar hello.jar
|
||||
```
|
||||
Или через скриптовый режим, без создания jar:
|
||||
```bash
|
||||
kotlin Hello.kt
|
||||
```
|
||||
Подробнее: [kotlinc](../../Инструментарий/kotlinc.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java `main` обязан лежать внутри класса, быть `public static void` и принимать `String[]`, а имя файла должно совпадать с именем публичного класса ([Hello World! в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md)).
|
||||
> В Kotlin всё это не требуется: функции живут прямо в файле.
|
||||
>
|
||||
> На уровне байт-кода класс всё равно появляется - компилятор создаёт `HelloKt` с методом `public static void main(String[])`. Поэтому JVM запускает Kotlin-код без каких-либо адаптеров.
|
||||
> Подробнее: [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> - `fun Main()` или `fun start()` скомпилируется, но JVM точку входа не найдёт - имя строго `main`
|
||||
> - Если в файле есть `package`, запускать надо с полным именем: `java -cp . com.example.HelloKt`
|
||||
> - Две функции `main()` в одном пакете в разных файлах - это нормально, классы-обёртки будут разными (`HelloKt`, `AppKt`)
|
||||
|
||||
Далее: [Типы данных](Типы%20данных.md)
|
||||
@@ -1,182 +0,0 @@
|
||||
---
|
||||
title: Операторы и выражения
|
||||
description: Арифметика, равенство == и ===, побитовые операции словами, приоритет операторов
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
В Kotlin почти все операторы - это вызовы функций. `a + b` компилируется в `a.plus(b)`, поэтому операторы можно определять для своих типов ([Операторные функции](../../Функциональный%20Kotlin/Инфиксные%20и%20операторные%20функции.md)).
|
||||
|
||||
## Арифметические
|
||||
| Оператор | Функция | Пример |
|
||||
|---|---|---|
|
||||
| `+` | `plus` | `2 + 3` → 5 |
|
||||
| `-` | `minus` | `5 - 3` → 2 |
|
||||
| `*` | `times` | `2 * 3` → 6 |
|
||||
| `/` | `div` | `7 / 2` → 3 |
|
||||
| `%` | `rem` | `7 % 2` → 1 |
|
||||
|
||||
Целочисленное деление отбрасывает дробную часть:
|
||||
```kotlin
|
||||
println(7 / 2) // 3
|
||||
println(7.0 / 2) // 3.5
|
||||
println(7 / 2.0) // 3.5
|
||||
println(-7 / 2) // -3 — округление к нулю
|
||||
println(-7 % 2) // -1 — знак остатка совпадает со знаком делимого
|
||||
```
|
||||
|
||||
### Инкремент и декремент
|
||||
```kotlin
|
||||
var i = 0
|
||||
println(i++) // 0, потом i = 1
|
||||
println(++i) // 2
|
||||
```
|
||||
Работают только с `var`. Присваивания-операторы (`+=`, `-=`, `*=`, `/=`, `%=`) - тоже.
|
||||
|
||||
## Сравнение
|
||||
`<`, `>`, `<=`, `>=` транслируются в `compareTo`:
|
||||
```kotlin
|
||||
println(2 < 3) // true
|
||||
println("abc" < "abd") // true — лексикографически
|
||||
```
|
||||
Работает для любого типа, реализующего `Comparable`.
|
||||
|
||||
## Равенство: == и ===
|
||||
Это главное место, где Kotlin расходится с Java.
|
||||
|
||||
| Оператор | Что делает |
|
||||
|---|---|
|
||||
| `==` | структурное равенство, вызывает `equals()` |
|
||||
| `===` | ссылочное равенство, сравнивает адреса |
|
||||
| `!=`, `!==` | их отрицания |
|
||||
|
||||
```kotlin
|
||||
val a = "Kotlin"
|
||||
val b = StringBuilder("Kotlin").toString()
|
||||
println(a == b) // true — содержимое совпадает
|
||||
println(a === b) // false — разные объекты
|
||||
```
|
||||
|
||||
`==` безопасен для null: `a == b` разворачивается в `a?.equals(b) ?: (b === null)`.
|
||||
```kotlin
|
||||
val s: String? = null
|
||||
println(s == "text") // false, без NPE
|
||||
println(s == null) // true
|
||||
```
|
||||
|
||||
## Логические
|
||||
```kotlin
|
||||
val r1 = true && false // ленивый И
|
||||
val r2 = true || false // ленивый ИЛИ
|
||||
val r3 = !true // НЕ
|
||||
|
||||
val r4 = true and false // НЕ ленивый — оба операнда вычисляются
|
||||
val r5 = true or false // НЕ ленивый
|
||||
val r6 = true xor true // false
|
||||
```
|
||||
В обычном коде используют `&&` и `||`: они не вычисляют правую часть, если результат уже известен.
|
||||
|
||||
## Побитовые операции
|
||||
Символов `&`, `|`, `^`, `<<`, `>>` для чисел в Kotlin **нет** - только инфиксные функции:
|
||||
```kotlin
|
||||
val a = 0b1100
|
||||
val b = 0b1010
|
||||
println(a and b) // 8 = 0b1000
|
||||
println(a or b) // 14 = 0b1110
|
||||
println(a xor b) // 6 = 0b0110
|
||||
println(a.inv()) // -13 — побитовое НЕ
|
||||
println(1 shl 4) // 16 — сдвиг влево
|
||||
println(16 shr 2) // 4 — сдвиг вправо со знаком
|
||||
println(-16 ushr 2) // беззнаковый сдвиг вправо
|
||||
```
|
||||
Доступны для `Int` и `Long`.
|
||||
|
||||
## Операторы диапазонов и коллекций
|
||||
```kotlin
|
||||
println(5 in 1..10) // true
|
||||
println(5 !in 1..10) // false
|
||||
println("a" in listOf("a","b")) // true — вызывает contains()
|
||||
val list = listOf(1, 2)
|
||||
println(list + 3) // [1, 2, 3] — plus для коллекций
|
||||
```
|
||||
Подробнее: [Циклы и диапазоны](Циклы%20и%20диапазоны.md)
|
||||
|
||||
## Приоритет
|
||||
От высшего к низшему:
|
||||
|
||||
| Уровень | Операторы |
|
||||
|---|---|
|
||||
| постфиксные | `++`, `--`, `.`, `?.`, `!!` |
|
||||
| префиксные | `-`, `+`, `++`, `--`, `!` |
|
||||
| приведение | `as`, `as?` |
|
||||
| умножение | `*`, `/`, `%` |
|
||||
| сложение | `+`, `-` |
|
||||
| диапазон | `..`, `..<` |
|
||||
| инфиксные функции | `and`, `shl`, своя функция `infix` |
|
||||
| проверка принадлежности | `in`, `!in` |
|
||||
| проверка типа | `is`, `!is` |
|
||||
| сравнение | `<`, `>`, `<=`, `>=` |
|
||||
| равенство | `==`, `!=`, `===`, `!==` |
|
||||
| конъюнкция | `&&` |
|
||||
| дизъюнкция | `\|\|` |
|
||||
| elvis | `?:` |
|
||||
| присваивание | `=`, `+=`, … |
|
||||
|
||||
На практике: сомневаетесь - ставьте скобки.
|
||||
|
||||
## Всё является выражением
|
||||
`if`, `when`, `try` возвращают значение:
|
||||
```kotlin
|
||||
val max = if (a > b) a else b
|
||||
val name = when (code) {
|
||||
200 -> "ok"
|
||||
else -> "unknown"
|
||||
}
|
||||
val n = try { text.toInt() } catch (e: NumberFormatException) { 0 }
|
||||
```
|
||||
Подробнее: [Условные конструкции](Условные%20конструкции.md)
|
||||
|
||||
Циклы (`for`, `while`) - **не** выражения, они возвращают `Unit`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `==` для объектов - ссылки | `==` - вызов `equals()` |
|
||||
> | `.equals()` - содержимое | `===` - ссылки |
|
||||
> | `a & b` побитовое И | `a and b` |
|
||||
> | `a >>> b` | `a ushr b` |
|
||||
> | `~a` | `a.inv()` |
|
||||
> | тернарник `c ? a : b` | `if (c) a else b` |
|
||||
> | операторы нельзя перегружать | можно, через `operator fun` |
|
||||
> | `s.equals(t)` падает, если `s` null | `s == t` безопасен |
|
||||
>
|
||||
> Главная ловушка при переходе: в Java `==` на строках сравнивает ссылки и работает «случайно» благодаря [интернированию](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md). В Kotlin `==` всегда сравнивает содержимое - это и нужно в 99% случаев.
|
||||
>
|
||||
> Сравнить: [Операторы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`and` и `or` не ленивые.** Для `Boolean` это рабочий код, но правая часть вычислится всегда:
|
||||
> ```kotlin
|
||||
> fun risky(): Boolean { println("вызвана"); return true }
|
||||
> val a = false && risky() // risky() не вызовется
|
||||
> val b = false and risky() // "вызвана" напечатается
|
||||
> ```
|
||||
>
|
||||
> **Деление на целочисленный ноль - исключение, на вещественный - нет:**
|
||||
> ```kotlin
|
||||
> // println(1 / 0) // ArithmeticException
|
||||
> println(1.0 / 0) // Infinity
|
||||
> println(0.0 / 0) // NaN
|
||||
> println(Double.NaN == Double.NaN) // false! NaN не равен себе
|
||||
> ```
|
||||
>
|
||||
> **Приоритет инфиксных функций ниже арифметики:**
|
||||
> ```kotlin
|
||||
> println(1 shl 2 + 3) // 32, а не 7: это 1 shl (2+3)
|
||||
> println((1 shl 2) + 3) // 7
|
||||
> ```
|
||||
>
|
||||
> **`++` на упакованном значении создаёт новый объект** - в горячем цикле с `Int?` это аллокации. Используйте не-nullable типы.
|
||||
|
||||
Далее: [Условные конструкции](Условные%20конструкции.md)
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
title: Пакеты и импорты
|
||||
description: Объявление пакетов, импорты, псевдонимы и автоматически импортируемые пакеты
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
## Пакет
|
||||
Объявляется первой строкой файла:
|
||||
```kotlin
|
||||
package com.example.app
|
||||
|
||||
fun helper() { }
|
||||
class Service
|
||||
```
|
||||
Полное имя функции - `com.example.app.helper`.
|
||||
|
||||
Отличие от Java: **структура каталогов не обязана совпадать с пакетом**, и имя файла не связано с содержимым. Компилятор не возражает. На практике соглашение соблюдают: иначе навигация по проекту ломается.
|
||||
|
||||
Файл без `package` попадает в корневой пакет (default package).
|
||||
|
||||
## Содержимое файла
|
||||
В одном файле может быть сколько угодно объявлений верхнего уровня:
|
||||
```kotlin
|
||||
package com.example
|
||||
|
||||
const val VERSION = "1.0"
|
||||
|
||||
fun parse(text: String): Int = text.toInt()
|
||||
|
||||
class User(val name: String)
|
||||
class Admin(val name: String)
|
||||
|
||||
private fun internalHelper() { } // видна только в этом файле
|
||||
```
|
||||
Никаких `public class` на файл - ограничения «один публичный класс на файл» в Kotlin нет.
|
||||
|
||||
## Импорты
|
||||
```kotlin
|
||||
import com.example.app.Service // конкретный класс
|
||||
import com.example.app.helper // функция верхнего уровня
|
||||
import com.example.app.* // всё из пакета
|
||||
import com.example.app.User.Companion.create // член companion object
|
||||
```
|
||||
Импортировать можно не только типы, но и функции, свойства, enum-константы - всё, что объявлено на верхнем уровне или как статический член.
|
||||
|
||||
```kotlin
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sqrt
|
||||
|
||||
fun main() {
|
||||
println(sqrt(PI)) // 1.7724538509055159
|
||||
}
|
||||
```
|
||||
|
||||
## Псевдонимы - as
|
||||
Когда имена конфликтуют или слишком длинные:
|
||||
```kotlin
|
||||
import com.example.data.User as DataUser
|
||||
import com.example.api.User as ApiUser
|
||||
|
||||
fun convert(u: DataUser): ApiUser = ApiUser(u.name)
|
||||
```
|
||||
Псевдоним действует только в своём файле. Если нужно имя на весь модуль - это [typealias](../Базовые%20типы%20ООП/Typealias.md).
|
||||
|
||||
## Импортируются автоматически
|
||||
Эти пакеты доступны без `import`:
|
||||
|
||||
| Пакет | Что содержит |
|
||||
|---|---|
|
||||
| `kotlin.*` | `Int`, `String`, `Any`, базовые функции |
|
||||
| `kotlin.annotation.*` | аннотации |
|
||||
| `kotlin.collections.*` | `List`, `Map`, `listOf`, операции |
|
||||
| `kotlin.comparisons.*` | `compareBy`, `maxOf` |
|
||||
| `kotlin.io.*` | `println`, `readln` |
|
||||
| `kotlin.ranges.*` | диапазоны, `step`, `downTo` |
|
||||
| `kotlin.sequences.*` | `Sequence`, `sequenceOf` |
|
||||
| `kotlin.text.*` | операции со строками |
|
||||
| `java.lang.*` | только на JVM |
|
||||
| `kotlin.jvm.*` | только на JVM |
|
||||
|
||||
Поэтому `println`, `listOf`, `Int` работают из коробки. А `kotlin.math.sqrt` - нет, его импортируют явно.
|
||||
|
||||
## Видимость на верхнем уровне
|
||||
```kotlin
|
||||
public fun api() { } // видна везде (public — по умолчанию)
|
||||
internal fun moduleOnly() { } // видна в своём модуле
|
||||
private fun fileOnly() { } // видна только в этом файле
|
||||
```
|
||||
`protected` на верхнем уровне запрещён - нет класса, от которого наследоваться.
|
||||
Подробнее: [Модификаторы видимости](../../ООП/Модификаторы%20видимости.md)
|
||||
|
||||
## Имя класса-обёртки
|
||||
Функции верхнего уровня попадают в класс, названный по файлу: `Utils.kt` → класс `UtilsKt`. Имя можно задать:
|
||||
```kotlin
|
||||
@file:JvmName("Utils")
|
||||
package com.example
|
||||
|
||||
fun helper() { } // из Java: Utils.helper()
|
||||
```
|
||||
Аннотации уровня файла ставятся **до** `package`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | каталоги обязаны совпадать с пакетом | не обязаны (но так принято) |
|
||||
> | имя файла = имя публичного класса | имя файла произвольно |
|
||||
> | один публичный класс на файл | сколько угодно объявлений |
|
||||
> | `import static` для статики | обычный `import` для всего |
|
||||
> | псевдонимов нет | `import ... as Name` |
|
||||
> | автоимпорт только `java.lang` | десять пакетов `kotlin.*` |
|
||||
> | `package-private` по умолчанию | `public` по умолчанию |
|
||||
>
|
||||
> Последняя строка важна: в Java класс без модификатора виден только своему пакету, в Kotlin - всем. Для сокрытия внутренностей библиотеки используют `internal`.
|
||||
>
|
||||
> Сравнить: [Модификаторы в Java](../../../Программирование%20на%20Java/ООП/Инкапсуляция/Модификаторы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Нет аналога package-private.** `internal` ограничивает *модулем* (единицей компиляции Gradle), а не пакетом. Разделить код на пакеты и спрятать их друг от друга внутри одного модуля нельзя - только разносить по модулям.
|
||||
>
|
||||
> **`import *` не затягивает вложенные пакеты.** `import com.example.*` не даёт доступа к `com.example.data.User`.
|
||||
>
|
||||
> **Конфликт импортов - ошибка компиляции, а не предупреждение.** Два `User` из разных пакетов требуют `as` или полного имени.
|
||||
>
|
||||
> **Функции с одинаковым именем в разных файлах одного пакета конфликтуют.** Два `private fun helper()` в разных файлах - нормально, а два `fun helper()` с одной сигнатурой - ошибка: оба попадают в один пакет.
|
||||
>
|
||||
> **`@file:JvmName` не действует, если имя файла совпадает с именем класса в нём.** Коллизия имён вызовет ошибку компиляции.
|
||||
|
||||
Дополнительно: [Структура проекта](../../Инструментарий/Структура%20проекта.md)
|
||||
Далее: [Null-безопасность](../Null-безопасность/Null-безопасность.md)
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: Переменные
|
||||
description: val и var, вывод типов, const val, lateinit и области видимости
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
Два ключевых слова:
|
||||
- `val` - **value**, значение присваивается один раз (read-only)
|
||||
- `var` - **variable**, значение можно менять
|
||||
|
||||
```kotlin
|
||||
val name = "Kotlin"
|
||||
var count = 0
|
||||
count++ // ok
|
||||
// name = "Java" // ошибка компиляции: val cannot be reassigned
|
||||
```
|
||||
|
||||
Правило стиля: по умолчанию `val`, `var` - только когда переменная действительно меняется. Это не формальность: с `val` работают [умные приведения](../Null-безопасность/Умные%20приведения.md), а код становится предсказуемее.
|
||||
|
||||
## Объявление
|
||||
```kotlin
|
||||
val a: Int = 1 // тип указан явно
|
||||
val b = 1 // тип выведен: Int
|
||||
val c: Int // без инициализации — тип обязателен
|
||||
c = 2 // присвоить можно позже, но только один раз
|
||||
```
|
||||
Тип ставится **после** имени через двоеточие.
|
||||
|
||||
## Вывод типов
|
||||
Компилятор выводит тип из значения:
|
||||
```kotlin
|
||||
val n = 42 // Int
|
||||
val l = 42L // Long
|
||||
val s = "text" // String
|
||||
val list = listOf(1, 2) // List<Int>
|
||||
val mixed = listOf(1, "a") // List<Any>
|
||||
```
|
||||
Вывод работает только там, где есть что выводить. У свойств класса и возвращаемых значений публичных функций тип лучше писать явно - это часть контракта.
|
||||
|
||||
## val - не значит immutable
|
||||
`val` запрещает переприсваивание ссылки, но не меняет сам объект:
|
||||
```kotlin
|
||||
val list = mutableListOf(1, 2)
|
||||
list.add(3) // ok — меняем объект
|
||||
println(list) // [1, 2, 3]
|
||||
// list = mutableListOf() // ошибка — меняем ссылку
|
||||
```
|
||||
Неизменяемость данных - это выбор типа (`List` вместо `MutableList`), а не `val`. Подробнее: [Изменяемость коллекций](../../Коллекции/Изменяемость.md)
|
||||
|
||||
## val с геттером
|
||||
`val` может вычисляться при каждом обращении:
|
||||
```kotlin
|
||||
val now: Long
|
||||
get() = System.currentTimeMillis()
|
||||
```
|
||||
Это уже свойство, а не поле. Подробнее: [Свойства](../../ООП/Свойства.md)
|
||||
|
||||
## Константы - const val
|
||||
```kotlin
|
||||
const val MAX = 100 // верхний уровень файла
|
||||
const val GREETING = "Hello"
|
||||
|
||||
object Config {
|
||||
const val TIMEOUT = 30 // или внутри object
|
||||
}
|
||||
```
|
||||
Требования к `const val`:
|
||||
- только `String` или примитивный тип
|
||||
- значение известно на этапе компиляции
|
||||
- только на верхнем уровне, в `object` или в `companion object`
|
||||
- нельзя у локальной переменной и нельзя с кастомным геттером
|
||||
|
||||
Значение подставляется в место использования на этапе компиляции (инлайнится), обращения к полю в рантайме нет.
|
||||
|
||||
```kotlin
|
||||
val runtime = computeLimit() // вычисляется при запуске
|
||||
const val compiled = 100 // подставлено компилятором
|
||||
```
|
||||
|
||||
## lateinit
|
||||
Для `var`, который точно будет проинициализирован, но не в момент объявления (DI, настройка в тестах):
|
||||
```kotlin
|
||||
class Service {
|
||||
lateinit var client: HttpClient
|
||||
|
||||
fun setup() {
|
||||
client = HttpClient()
|
||||
}
|
||||
|
||||
fun use() {
|
||||
if (::client.isInitialized) client.send()
|
||||
}
|
||||
}
|
||||
```
|
||||
Ограничения `lateinit`:
|
||||
- только `var`
|
||||
- тип не nullable
|
||||
- не примитивный тип (`Int`, `Double`, … запрещены)
|
||||
- нельзя у локальных переменных
|
||||
- нельзя с кастомным геттером/сеттером
|
||||
|
||||
Обращение до инициализации - `UninitializedPropertyAccessException`.
|
||||
|
||||
Альтернатива для ленивых вычислений - `by lazy`, он работает с `val`:
|
||||
```kotlin
|
||||
val config: Config by lazy { loadConfig() } // вычислится при первом обращении
|
||||
```
|
||||
Подробнее: [Свойства](../../ООП/Свойства.md)
|
||||
|
||||
## Области видимости
|
||||
```kotlin
|
||||
const val TOP = 1 // весь файл и, если public, другие файлы
|
||||
|
||||
fun demo() {
|
||||
val local = 2 // только тело функции
|
||||
if (local > 1) {
|
||||
val inner = 3 // только тело if
|
||||
}
|
||||
// println(inner) // ошибка: не видна
|
||||
}
|
||||
```
|
||||
Вложенная переменная может скрыть внешнюю - компилятор выдаст предупреждение `name shadowed`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `final int x = 1;` | `val x = 1` |
|
||||
> | `int x = 1;` | `var x = 1` |
|
||||
> | `var x = 1;` (только локально, с Java 10) | вывод типов везде, включая свойства класса |
|
||||
> | `static final int MAX = 100;` | `const val MAX = 100` |
|
||||
> | мутабельность по умолчанию | иммутабельность предпочтительна |
|
||||
> | `int x;` - значение по умолчанию 0 | неинициализированную переменную читать нельзя |
|
||||
>
|
||||
> Важное следствие: у свойств класса в Kotlin **нет значений по умолчанию**. В Java поле `int count;` автоматически 0, в Kotlin `var count: Int` без инициализатора не скомпилируется.
|
||||
>
|
||||
> Сравнить: [Переменные в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`val` не делает объект неизменяемым** - см. пример с `MutableList` выше. Частая ошибка на собеседованиях.
|
||||
>
|
||||
> **`lateinit` прячет ошибку до рантайма.** Если забыли вызвать `setup()`, падение случится в момент использования, далеко от причины. Там, где можно, лучше конструктор или `by lazy`.
|
||||
>
|
||||
> **Вывод типов может дать не тот тип, что ожидали:**
|
||||
> ```kotlin
|
||||
> val x = 1 / 2 // 0, а не 0.5 — оба операнда Int
|
||||
> val y = 1.0 / 2 // 0.5
|
||||
> ```
|
||||
>
|
||||
> **`const val` зашивается в вызывающий код.** Если константа лежит в библиотеке и её значение изменилось, зависимый модуль нужно перекомпилировать - иначе он продолжит использовать старое значение.
|
||||
|
||||
Дополнительно: [Свойства](../../ООП/Свойства.md)
|
||||
Далее: [Операторы и выражения](Операторы%20и%20выражения.md)
|
||||
@@ -1,191 +0,0 @@
|
||||
---
|
||||
title: Строковые шаблоны и вывод
|
||||
description: Интерполяция строк, raw strings, trimIndent, println и форматированный вывод
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
## Шаблоны строк
|
||||
Вместо конкатенации - подстановка значений прямо в строку:
|
||||
```kotlin
|
||||
val name = "Kotlin"
|
||||
val version = 2.4
|
||||
|
||||
println("Привет, $name!") // Привет, Kotlin!
|
||||
println("Версия: $version") // Версия: 2.4
|
||||
println("Длина имени: ${name.length}") // Длина имени: 6
|
||||
println("Сумма: ${2 + 3}") // Сумма: 5
|
||||
```
|
||||
|
||||
Правила:
|
||||
- `$имя` - для простой переменной
|
||||
- `${выражение}` - для всего остального: вызовов, свойств, арифметики
|
||||
- вызывается `toString()` подставляемого значения
|
||||
- `null` подставляется как текст `null`
|
||||
|
||||
```kotlin
|
||||
val x: String? = null
|
||||
println("значение: $x") // значение: null
|
||||
```
|
||||
|
||||
Фигурные скобки можно ставить всегда - многие так и делают для единообразия.
|
||||
|
||||
## Экранирование
|
||||
```kotlin
|
||||
println("цена: \$100") // цена: $100
|
||||
println("перенос\nстроки")
|
||||
println("табуляция\tтут")
|
||||
println("кавычка \" внутри")
|
||||
println("слеш \\ внутри")
|
||||
println("символ \u00A9") // ©
|
||||
```
|
||||
Поддерживаются `\t`, `\b`, `\n`, `\r`, `\'`, `\"`, `\\`, `\$`, `\uXXXX`.
|
||||
|
||||
## Raw strings
|
||||
Три кавычки - никакого экранирования, переносы сохраняются как есть:
|
||||
```kotlin
|
||||
val path = """C:\Users\name\file.txt""" // слеши не экранируются
|
||||
val json = """
|
||||
{
|
||||
"name": "Kotlin",
|
||||
"stable": true
|
||||
}
|
||||
"""
|
||||
```
|
||||
Удобно для regex, JSON, SQL, HTML.
|
||||
|
||||
Шаблоны `$` в raw strings **работают**:
|
||||
```kotlin
|
||||
val user = "admin"
|
||||
val query = """SELECT * FROM users WHERE name = '$user'"""
|
||||
```
|
||||
Чтобы вставить настоящий `$`, используют подстановку символа:
|
||||
```kotlin
|
||||
val price = """цена: ${'$'}100""" // цена: $100
|
||||
```
|
||||
|
||||
## trimIndent и trimMargin
|
||||
Raw string сохраняет отступы исходного кода - их убирают явно:
|
||||
```kotlin
|
||||
val text = """
|
||||
Первая строка
|
||||
Вторая строка
|
||||
""".trimIndent()
|
||||
// "Первая строка\nВторая строка" — общий отступ убран,
|
||||
// пустые первая и последняя строки удалены
|
||||
|
||||
val bar = """
|
||||
|Первая
|
||||
|Вторая
|
||||
""".trimMargin() // по умолчанию маркер |
|
||||
```
|
||||
`trimIndent()` вычисляет минимальный общий отступ непустых строк и срезает его у всех.
|
||||
|
||||
## println и print
|
||||
```kotlin
|
||||
print("без переноса")
|
||||
println("с переносом")
|
||||
println() // пустая строка
|
||||
println(listOf(1, 2, 3)) // [1, 2, 3]
|
||||
```
|
||||
Это функции стандартной библиотеки, доступны без импорта. Под капотом - `System.out`.
|
||||
|
||||
## Форматированный вывод
|
||||
Kotlin использует механизм форматирования Java:
|
||||
```kotlin
|
||||
import java.util.Locale
|
||||
|
||||
fun main() {
|
||||
println("%05d".format(42)) // 00042
|
||||
println("%-10s|".format("left")) // left |
|
||||
println(String.format("%s = %d", "x", 5)) // x = 5
|
||||
|
||||
// вещественные числа и разряды зависят от локали — её лучше задавать явно
|
||||
println("%.2f".format(Locale.US, 3.14159)) // 3.14
|
||||
println("%,d".format(Locale.US, 1234567)) // 1,234,567
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning] Локаль по умолчанию
|
||||
> `format` без явной локали берёт системную. На русской локали получится `3,14` и `1 234 567` - с запятой и неразрывными пробелами. Если строка идёт в JSON, SQL или лог, это ломает парсинг. Для машинных форматов всегда передавайте `Locale.US` (или `Locale.ROOT`).
|
||||
|
||||
Основные спецификаторы:
|
||||
|
||||
| Спецификатор | Значение |
|
||||
|---|---|
|
||||
| `%d` | целое число |
|
||||
| `%f` | вещественное |
|
||||
| `%.Nf` | вещественное с N знаками |
|
||||
| `%s` | строка (через `toString()`) |
|
||||
| `%b` | boolean |
|
||||
| `%x` | шестнадцатеричное |
|
||||
| `%e` | экспоненциальная запись |
|
||||
| `%n` | перенос строки платформы |
|
||||
| `%%` | символ процента |
|
||||
|
||||
Флаги ширины: `%10d` - по правому краю, `%-10d` - по левому, `%010d` - с ведущими нулями.
|
||||
|
||||
## Чтение ввода
|
||||
```kotlin
|
||||
val line = readLine() // String? — null при конце потока
|
||||
val text = readln() // String — бросает исключение при EOF
|
||||
val n = readln().toInt() // падает на некорректном вводе
|
||||
val safe = readln().toIntOrNull() ?: 0 // безопасный разбор
|
||||
```
|
||||
`readln()` появился в Kotlin 1.6 и удобнее, когда ввод гарантирован.
|
||||
|
||||
## Конкатенация
|
||||
Работает, но в шаблонах почти не нужна:
|
||||
```kotlin
|
||||
println("Hello" + " " + "world") // Hello world
|
||||
println("Hello" + 5 + 10) // Hello510 — слева направо
|
||||
println("Hello" + (5 + 10)) // Hello15
|
||||
```
|
||||
Важно: складывать можно только когда **слева** строка. У `Int` нет `plus(String)`, поэтому в Kotlin не скомпилируется то, что в Java работает:
|
||||
```kotlin
|
||||
// println(5 + 10 + "Hello") // ошибка: Int.plus ожидает Int, а не String
|
||||
println("${5 + 10}Hello") // 15Hello — через шаблон
|
||||
```
|
||||
Для сборки строки в цикле - `StringBuilder` или `buildString`:
|
||||
```kotlin
|
||||
val result = buildString {
|
||||
repeat(3) { append("ха") }
|
||||
}
|
||||
println(result) // хахаха
|
||||
```
|
||||
Подробнее: [Строки](../Работа%20с%20данными/Строки.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `System.out.println(x)` | `println(x)` |
|
||||
> | `"a" + x + "b"` | `"a${x}b"` |
|
||||
> | `String.format("%d", n)` | `"%d".format(n)` |
|
||||
> | text blocks `"""` (Java 15+) | raw strings `"""` + шаблоны |
|
||||
> | `.stripIndent()` | `.trimIndent()` |
|
||||
> | `new Scanner(System.in)` | `readln()` |
|
||||
>
|
||||
> Шаблоны - не просто синтаксический сахар: компилятор собирает их через `StringBuilder` или `invokedynamic`, то есть без промежуточных строк. Писать `"a" + b + "c"` ради производительности не нужно.
|
||||
>
|
||||
> Сравнить: [Форматированный вывод в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Форматированный%20вывод.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`$` перед словом всегда считается шаблоном.** Строка `"цена: $100"` скомпилируется (цифра не может начинать имя), а `"$name_suffix"` - нет: Kotlin ищет переменную `name_suffix`. Нужны скобки: `"${name}_suffix"`.
|
||||
>
|
||||
> **`trimIndent()` чувствителен к смешению табов и пробелов** - отступ считается по символам, таб и четыре пробела для него разные вещи.
|
||||
>
|
||||
> **`$` в regex внутри raw string.** `"""^\d+$"""` работает, потому что после `$` идёт конец строки. Но `"""$\d"""` компилятор попытается разобрать как шаблон.
|
||||
>
|
||||
> **Шаблон вызывает `toString()` у массива, а не его содержимое:**
|
||||
> ```kotlin
|
||||
> val arr = arrayOf(1, 2, 3)
|
||||
> println("$arr") // [Ljava.lang.Object;@1b6d3586
|
||||
> println(arr.contentToString()) // [1, 2, 3]
|
||||
> println("${arr.joinToString()}") // 1, 2, 3
|
||||
> ```
|
||||
>
|
||||
> **`readLine()` возвращает `String?`.** Вызов `readLine().toInt()` не скомпилируется - нужен `readln()` или обработка null.
|
||||
|
||||
Дополнительно: [Строки](../Работа%20с%20данными/Строки.md)
|
||||
Далее: [Пакеты и импорты](Пакеты%20и%20импорты.md)
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: Типы данных
|
||||
description: Система типов Kotlin, числа, boolean, Char и отсутствие примитивов на уровне языка
|
||||
tags:
|
||||
- kotlin
|
||||
- types
|
||||
---
|
||||
|
||||
В Kotlin: статическая типизация, сильная, с выводом типов.
|
||||
|
||||
Главное отличие от Java: **на уровне языка примитивов нет**. Всё - объект, у всего есть методы.
|
||||
```kotlin
|
||||
val x: Int = 42
|
||||
println(x.toString()) // у числа есть методы
|
||||
println(42.coerceIn(0, 10)) // 10
|
||||
```
|
||||
Но в байт-коде `Int` компилируется в `int`, если это возможно - производительность не теряется. Подробнее в [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md).
|
||||
|
||||
## Иерархия типов
|
||||
```mermaid
|
||||
graph TD
|
||||
Any["Any — корень всех non-null типов"]
|
||||
Any --> Number
|
||||
Any --> Char
|
||||
Any --> Boolean
|
||||
Any --> String
|
||||
Any --> Other["классы, интерфейсы, ..."]
|
||||
Number --> Byte
|
||||
Number --> Short
|
||||
Number --> Int
|
||||
Number --> Long
|
||||
Number --> Float
|
||||
Number --> Double
|
||||
Nothing["Nothing — наследник всех типов"]
|
||||
```
|
||||
`Any`, `Unit` и `Nothing` - особые типы, им посвящена отдельная заметка: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
## Числовые типы
|
||||
| Тип | Размер, бит | Диапазон |
|
||||
|---|---|---|
|
||||
| `Byte` | 8 | −128 … 127 |
|
||||
| `Short` | 16 | −32 768 … 32 767 |
|
||||
| `Int` | 32 | ≈ ±2 млрд |
|
||||
| `Long` | 64 | ≈ ±9,2 · 10¹⁸ |
|
||||
| `Float` | 32 | ≈ 6-7 значащих цифр |
|
||||
| `Double` | 64 | ≈ 15-16 значащих цифр |
|
||||
|
||||
Все наследуются от `Number`, но **не** друг от друга: `Int` не является подтипом `Long`.
|
||||
|
||||
### Литералы
|
||||
```kotlin
|
||||
val i = 42 // Int по умолчанию
|
||||
val l = 42L // Long — суффикс L
|
||||
val d = 42.0 // Double по умолчанию
|
||||
val f = 42.0f // Float — суффикс f
|
||||
val hex = 0xFF // 255
|
||||
val bin = 0b1010 // 10
|
||||
val big = 1_000_000 // подчёркивания для читаемости
|
||||
```
|
||||
Восьмеричных литералов (`017` в Java) в Kotlin **нет**.
|
||||
|
||||
## Беззнаковые типы
|
||||
`UByte`, `UShort`, `UInt`, `ULong` - то, чего в Java нет совсем.
|
||||
```kotlin
|
||||
val u: UInt = 4_000_000_000u // суффикс u
|
||||
println(UInt.MAX_VALUE) // 4294967295
|
||||
```
|
||||
Нужны редко: работа с бинарными форматами, протоколами, хешами.
|
||||
|
||||
## Boolean
|
||||
```kotlin
|
||||
val yes: Boolean = true
|
||||
val both = yes && other() // ленивые операторы, как в Java
|
||||
val eager = yes and other() // не ленивый: other() вызовется всегда
|
||||
```
|
||||
|
||||
## Char
|
||||
```kotlin
|
||||
val c: Char = 'A'
|
||||
println(c.code) // 65 — числовой код
|
||||
println(c + 1) // B — Char + Int даёт Char
|
||||
println(c.isLetter()) // true
|
||||
```
|
||||
`Char` **не является числом**: `val n: Int = 'A'` не скомпилируется, нужен `'A'.code`.
|
||||
Подробнее: [Char](../Работа%20с%20данными/Char.md)
|
||||
|
||||
## Преобразования
|
||||
Неявных расширяющих преобразований нет вообще - только явные вызовы:
|
||||
```kotlin
|
||||
val i = 42
|
||||
val l: Long = i.toLong() // обязательно явно
|
||||
val b = i.toByte()
|
||||
val d = i.toDouble()
|
||||
```
|
||||
Доступны `toByte()`, `toShort()`, `toInt()`, `toLong()`, `toFloat()`, `toDouble()`, `toChar()`.
|
||||
|
||||
## Строки
|
||||
`String` - неизменяемый тип, элементы доступны по индексу:
|
||||
```kotlin
|
||||
val s = "Kotlin"
|
||||
println(s[0]) // K
|
||||
println(s.length) // 6
|
||||
```
|
||||
Подробнее: [Строки](../Работа%20с%20данными/Строки.md)
|
||||
|
||||
## Nullable-типы
|
||||
Любой тип имеет nullable-вариант с `?`:
|
||||
```kotlin
|
||||
// val a: Int = null // ошибка компиляции
|
||||
val b: Int? = null // ok
|
||||
```
|
||||
Это ядро языка, ему посвящён отдельный раздел: [Null-безопасность](../Null-безопасность/Null-безопасность.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `int` и `Integer` - разные типы | только `Int`, компилятор сам выбирает представление |
|
||||
> | автоупаковка неявная | упаковка скрыта, в коде не видна |
|
||||
> | `long x = intValue;` работает | нужен `intValue.toLong()` |
|
||||
> | `char` - числовой тип | `Char` - отдельный тип, не число |
|
||||
> | нет беззнаковых типов | `UInt`, `ULong`, … |
|
||||
> | `017` - восьмеричный литерал | восьмеричных литералов нет |
|
||||
>
|
||||
> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Переполнение не проверяется**, как и в Java:
|
||||
> ```kotlin
|
||||
> println(Int.MAX_VALUE + 1) // -2147483648
|
||||
> ```
|
||||
>
|
||||
> **Равенство упакованных значений.** Если тип nullable, значение упаковано, и `===` сравнивает ссылки:
|
||||
> ```kotlin
|
||||
> val a: Int = 127
|
||||
> val b: Int? = a
|
||||
> val c: Int? = a
|
||||
> println(b == c) // true — сравнение значений
|
||||
> println(b === c) // true — кеш Integer для -128..127
|
||||
>
|
||||
> val x: Int? = 1000
|
||||
> val y: Int? = 1000
|
||||
> println(x == y) // true
|
||||
> println(x === y) // false — за пределами кеша это разные объекты
|
||||
> ```
|
||||
> Вывод: для чисел всегда `==`, никогда `===`.
|
||||
>
|
||||
> **`Float` и `Double` не точны**, это стандарт IEEE 754:
|
||||
> ```kotlin
|
||||
> println(0.1 + 0.2) // 0.30000000000000004
|
||||
> ```
|
||||
> Для денег - `java.math.BigDecimal`.
|
||||
|
||||
Дополнительно: [Числа и переполнение](../Работа%20с%20данными/Числа%20и%20переполнение.md)
|
||||
Далее: [Переменные](Переменные.md)
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
title: Условные конструкции
|
||||
description: if и when как выражения, исчерпывающий when, отсутствие тернарного оператора и switch
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
В Kotlin две конструкции ветвления: `if` и `when`. Обе могут работать и как инструкция, и как **выражение**, возвращающее значение.
|
||||
|
||||
## if как инструкция
|
||||
```kotlin
|
||||
if (x > 0) {
|
||||
println("положительное")
|
||||
} else if (x < 0) {
|
||||
println("отрицательное")
|
||||
} else {
|
||||
println("ноль")
|
||||
}
|
||||
```
|
||||
Условие обязано быть `Boolean`. Число или null туда подставить нельзя - компилятор не пропустит.
|
||||
|
||||
## if как выражение
|
||||
```kotlin
|
||||
val max = if (a > b) a else b
|
||||
|
||||
val label = if (x > 0) {
|
||||
log("positive")
|
||||
"плюс" // последнее выражение блока — результат
|
||||
} else {
|
||||
"минус"
|
||||
}
|
||||
```
|
||||
Правила:
|
||||
- в режиме выражения ветка `else` **обязательна** - иначе значение не определено
|
||||
- результат блока - его последнее выражение
|
||||
- тип результата - общий супертип всех ветвей
|
||||
|
||||
```kotlin
|
||||
val r = if (flag) 1 else "text" // тип Any — общий супертип
|
||||
```
|
||||
|
||||
Тернарного оператора `?:` в смысле Java в Kotlin нет: его роль играет `if`. Оператор `?:` в Kotlin - это **elvis**, у него другая задача ([Операторы null-безопасности](../Null-безопасность/Операторы%20null-безопасности.md)).
|
||||
|
||||
## when - замена switch
|
||||
Две формы. С аргументом:
|
||||
```kotlin
|
||||
when (code) {
|
||||
200 -> println("ok")
|
||||
404 -> println("not found")
|
||||
500, 502, 503 -> println("ошибка сервера") // несколько значений в одной ветке
|
||||
else -> println("неизвестно")
|
||||
}
|
||||
```
|
||||
Без аргумента - вместо цепочки `if/else if`:
|
||||
```kotlin
|
||||
when {
|
||||
x < 0 -> println("отрицательное")
|
||||
x == 0 -> println("ноль")
|
||||
else -> println("положительное")
|
||||
}
|
||||
```
|
||||
|
||||
Ветки проверяются **сверху вниз**, выполняется первая подошедшая. Никакого `break` не нужно - провала в следующую ветку (fall-through) в Kotlin не бывает.
|
||||
|
||||
## when как выражение
|
||||
```kotlin
|
||||
val name = when (code) {
|
||||
200 -> "ok"
|
||||
404 -> "not found"
|
||||
else -> "unknown"
|
||||
}
|
||||
```
|
||||
В режиме выражения `when` обязан быть **исчерпывающим**: либо есть `else`, либо компилятор видит, что перечислены все варианты.
|
||||
|
||||
## Условия в ветках when
|
||||
Не только константы:
|
||||
```kotlin
|
||||
when (x) {
|
||||
in 1..9 -> "одна цифра" // диапазон
|
||||
in listOf(10, 20, 30) -> "круглое" // коллекция
|
||||
!in 0..100 -> "вне диапазона"
|
||||
else -> "прочее"
|
||||
}
|
||||
|
||||
when (value) {
|
||||
is String -> value.length // проверка типа + умное приведение
|
||||
is Int -> value + 1
|
||||
is List<*> -> value.size
|
||||
else -> 0
|
||||
}
|
||||
|
||||
when (x) {
|
||||
computeLimit() -> "предел" // произвольное выражение
|
||||
else -> "нет"
|
||||
}
|
||||
```
|
||||
После `is` переменная автоматически приводится к проверенному типу - см. [Умные приведения](../Null-безопасность/Умные%20приведения.md).
|
||||
|
||||
## Исчерпывающий when без else
|
||||
С `enum` и `sealed` компилятор сам проверяет полноту:
|
||||
```kotlin
|
||||
enum class State { LOADING, READY, ERROR }
|
||||
|
||||
fun describe(s: State) = when (s) {
|
||||
State.LOADING -> "загрузка"
|
||||
State.READY -> "готово"
|
||||
State.ERROR -> "ошибка"
|
||||
} // else не нужен — все варианты перечислены
|
||||
```
|
||||
Добавили `State.EMPTY` - код **перестанет компилироваться** во всех таких `when`. Это главная причина предпочитать `sealed` и `enum` строкам и числам: компилятор показывает каждое место, которое нужно обновить.
|
||||
|
||||
Подробнее: [Enum class](../Базовые%20типы%20ООП/Enum%20class.md), [Sealed class и interface](../Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Переменная в when
|
||||
Аргумент можно объявить прямо в заголовке - он будет виден только внутри:
|
||||
```kotlin
|
||||
when (val response = request()) {
|
||||
is Success -> response.data
|
||||
is Failure -> response.error
|
||||
}
|
||||
```
|
||||
Удобно, когда `request()` дорогой: вызов происходит один раз.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `switch` только по числам, строкам, enum | `when` по любым значениям и условиям |
|
||||
> | нужен `break`, иначе fall-through | провала нет, `break` не нужен |
|
||||
> | `c ? a : b` | `if (c) a else b` |
|
||||
> | `if` - только инструкция | `if` - ещё и выражение |
|
||||
> | `switch` не проверяет полноту (до pattern matching в Java 21) | `when` по enum/sealed проверяется компилятором |
|
||||
>
|
||||
> `when` ближе всего к `switch`-выражению из Java 14+ и pattern matching из Java 21, но появился в Kotlin задолго до них и покрывает больше случаев.
|
||||
>
|
||||
> Сравнить: [Условные конструкции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Условные%20конструкции%20и%20операторы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Полнота проверяется и у `when`-инструкции - но только для `enum` и `sealed`.** С Kotlin 2.0 пропуск ветки - ошибка компиляции даже когда результат не используется:
|
||||
> ```kotlin
|
||||
> fun handle(s: State) {
|
||||
> when (s) {
|
||||
> State.LOADING -> show()
|
||||
> State.READY -> done()
|
||||
> } // error: 'when' expression must be exhaustive.
|
||||
> } // Add the 'ERROR' branch or an 'else' branch.
|
||||
> ```
|
||||
> А вот для `Int`, `String` и прочих открытых типов полноту проверить невозможно - там `when`-инструкция без `else` компилируется молча и просто ничего не делает:
|
||||
> ```kotlin
|
||||
> fun handle(code: Int) {
|
||||
> when (code) {
|
||||
> 200 -> show()
|
||||
> 404 -> fail()
|
||||
> } // для code = 500 тихо ничего не произойдёт
|
||||
> }
|
||||
> ```
|
||||
> Это ещё одна причина моделировать состояния через `enum` и `sealed`, а не через числа и строки: только тогда компилятор на вашей стороне.
|
||||
>
|
||||
> **Порядок ветвей важен.** Первая подошедшая выигрывает, более общая ветка выше «съест» частные:
|
||||
> ```kotlin
|
||||
> when (x) {
|
||||
> in 0..100 -> "диапазон" // сработает и для 42
|
||||
> 42 -> "ответ" // недостижимо
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`if` без `else` не выражение:**
|
||||
> ```kotlin
|
||||
> // val x = if (flag) 1 // ошибка компиляции
|
||||
> val x = if (flag) 1 else 0 // ok
|
||||
> ```
|
||||
>
|
||||
> **`is List<*>` не проверяет тип элементов** - дженерики стираются в рантайме. `is List<String>` написать нельзя. Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
Далее: [Циклы и диапазоны](Циклы%20и%20диапазоны.md)
|
||||
@@ -1,159 +0,0 @@
|
||||
---
|
||||
title: Циклы и диапазоны
|
||||
description: for-in, диапазоны и прогрессии, while, метки и отсутствие классического for
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
В Kotlin один цикл обхода - `for (x in source)`. Классического `for (int i = 0; i < n; i++)` нет: его роль выполняют диапазоны.
|
||||
|
||||
## Диапазоны
|
||||
```kotlin
|
||||
for (i in 1..5) print(i) // 12345 — включая 5
|
||||
for (i in 1..<5) print(i) // 1234 — исключая 5
|
||||
for (i in 5 downTo 1) print(i) // 54321
|
||||
for (i in 1..10 step 3) print(i) // 1 4 7 10
|
||||
for (c in 'a'..'e') print(c) // abcde
|
||||
```
|
||||
|
||||
| Конструкция | Смысл |
|
||||
|---|---|
|
||||
| `a..b` | от `a` до `b` включительно |
|
||||
| `a..<b` | от `a` до `b`, не включая `b` |
|
||||
| `a until b` | то же, что `a..<b` (старая форма) |
|
||||
| `a downTo b` | по убыванию, включительно |
|
||||
| `step n` | с шагом `n` |
|
||||
|
||||
Шаг всегда положительный, даже в `downTo`: направление задаёт `downTo`, а не знак шага.
|
||||
|
||||
Диапазон - это объект, его можно хранить и проверять:
|
||||
```kotlin
|
||||
val valid = 1..100
|
||||
println(42 in valid) // true
|
||||
println(valid.first) // 1
|
||||
println(valid.last) // 100
|
||||
println((1..10 step 3).last) // 10
|
||||
```
|
||||
|
||||
## Обход по индексам
|
||||
```kotlin
|
||||
val list = listOf("a", "b", "c")
|
||||
|
||||
for (i in list.indices) print(i) // 012
|
||||
for (i in 0..<list.size) print(i) // 012 — то же самое
|
||||
|
||||
for (item in list) print(item) // abc — предпочтительно
|
||||
for ((i, item) in list.withIndex()) { // 0:a 1:b 2:c
|
||||
print("$i:$item ")
|
||||
}
|
||||
```
|
||||
`withIndex()` возвращает пары, которые тут же разбираются деструктуризацией ([Деструктуризация](../Работа%20с%20данными/Деструктуризация.md)).
|
||||
|
||||
## Обход коллекций и Map
|
||||
```kotlin
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
for ((key, value) in map) {
|
||||
println("$key=$value")
|
||||
}
|
||||
```
|
||||
`for` работает с любым типом, у которого есть функция `iterator()` - включая свои классы.
|
||||
|
||||
## while и do-while
|
||||
```kotlin
|
||||
var i = 0
|
||||
while (i < 3) {
|
||||
print(i) // 012
|
||||
i++
|
||||
}
|
||||
|
||||
do {
|
||||
val line = readLine()
|
||||
} while (line != null) // тело выполнится минимум один раз
|
||||
```
|
||||
Переменная, объявленная в `do`, видна в условии `while` - такого в Java нет.
|
||||
|
||||
## break и continue
|
||||
```kotlin
|
||||
for (i in 1..10) {
|
||||
if (i == 3) continue // пропустить итерацию
|
||||
if (i == 5) break // выйти из цикла
|
||||
print(i) // 124
|
||||
}
|
||||
```
|
||||
|
||||
## Метки
|
||||
Метка - идентификатор с `@`. Нужна, чтобы выйти из внешнего цикла:
|
||||
```kotlin
|
||||
outer@ for (i in 1..3) {
|
||||
for (j in 1..3) {
|
||||
if (j == 2) continue@outer // к следующей i
|
||||
if (i == 3) break@outer // выход из обоих циклов
|
||||
print("$i$j ") // 11 21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Циклы - не выражения
|
||||
`for` и `while` возвращают `Unit`, присвоить их результат нельзя. Если нужен результат обхода - это операции над коллекциями:
|
||||
```kotlin
|
||||
val doubled = (1..5).map { it * 2 } // [2, 4, 6, 8, 10]
|
||||
val evens = (1..10).filter { it % 2 == 0 } // [2, 4, 6, 8, 10]
|
||||
val sum = (1..100).sum() // 5050
|
||||
```
|
||||
В идиоматичном Kotlin явные циклы встречаются редко. Подробнее: [Операции над коллекциями](../../Коллекции/Операции.md)
|
||||
|
||||
## repeat
|
||||
Когда индекс не нужен:
|
||||
```kotlin
|
||||
repeat(3) { println("повтор") }
|
||||
repeat(3) { i -> println("итерация $i") } // индекс доступен, с нуля
|
||||
```
|
||||
Это не оператор, а [inline-функция](../../Функциональный%20Kotlin/Inline-функции.md) стандартной библиотеки.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `for (int i=0; i<n; i++)` | `for (i in 0..<n)` |
|
||||
> | `for (T x : collection)` | `for (x in collection)` |
|
||||
> | `label: for (...)` | `outer@ for (...)` |
|
||||
> | `break label;` | `break@outer` |
|
||||
> | три части в заголовке `for` | только `in` |
|
||||
> | нет диапазонов | `1..10`, `step`, `downTo` |
|
||||
>
|
||||
> Что потеряли: заголовок `for` с несколькими переменными и произвольным условием. Если нужна такая логика - пишется `while`.
|
||||
>
|
||||
> Сравнить: [Циклы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Циклы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`1..5` включает 5.** Самая частая ошибка при переходе с Java: индексы массива - это `0..<size`, а не `0..size`:
|
||||
> ```kotlin
|
||||
> val list = listOf("a", "b", "c")
|
||||
> // for (i in 0..list.size) println(list[i]) // IndexOutOfBounds на i=3
|
||||
> for (i in 0..<list.size) println(list[i]) // верно
|
||||
> for (i in list.indices) println(list[i]) // ещё лучше
|
||||
> ```
|
||||
>
|
||||
> **`downTo` со `step` требует положительного шага:**
|
||||
> ```kotlin
|
||||
> for (i in 5 downTo 1 step 2) print(i) // 531
|
||||
> // for (i in 5 downTo 1 step -2) // IllegalArgumentException в рантайме
|
||||
> ```
|
||||
>
|
||||
> **Пустой диапазон не ошибка, просто ноль итераций:**
|
||||
> ```kotlin
|
||||
> for (i in 5..1) print(i) // ничего не напечатает
|
||||
> ```
|
||||
> Молчаливый баг, если границы перепутаны. `(5..1).isEmpty()` - `true`.
|
||||
>
|
||||
> **Коллекцию нельзя изменять во время обхода:**
|
||||
> ```kotlin
|
||||
> val list = mutableListOf(1, 2, 3)
|
||||
> // for (x in list) if (x == 2) list.remove(x) // ConcurrentModificationException
|
||||
> list.removeAll { it == 2 } // правильный способ
|
||||
> ```
|
||||
>
|
||||
> **Диапазон `Long` и `Char` есть, а `Double` - нет.** `1.0..2.0` создаст `ClosedFloatingPointRange`: проверять `in` можно, но обойти циклом - нельзя (нет `iterator()`).
|
||||
|
||||
Дополнительно: [Операции над коллекциями](../../Коллекции/Операции.md)
|
||||
Далее: [Строковые шаблоны и вывод](Строковые%20шаблоны%20и%20вывод.md)
|
||||
@@ -1,140 +0,0 @@
|
||||
---
|
||||
title: Char
|
||||
description: Char как отдельный тип, код символа, проверки категорий и работа с цифрами
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
`Char` - один символ в одинарных кавычках. Хранит кодовую единицу UTF-16 (16 бит).
|
||||
|
||||
```kotlin
|
||||
val c: Char = 'A'
|
||||
val tab = '\t'
|
||||
val quote = '\''
|
||||
val unicode = '\u00A9' // ©
|
||||
```
|
||||
|
||||
Ключевое отличие от Java: **`Char` не является числом**. Арифметику с ним делать можно, но неявного превращения в `Int` нет.
|
||||
|
||||
```kotlin
|
||||
val c = 'A'
|
||||
// val n: Int = c // ошибка компиляции
|
||||
val n: Int = c.code // 65 — явно
|
||||
println(c + 1) // B — Char + Int = Char
|
||||
println('z' - 'a') // 25 — Char - Char = Int
|
||||
println(c.toString()) // A
|
||||
```
|
||||
|
||||
| Выражение | Результат | Тип |
|
||||
|---|---|---|
|
||||
| `'A' + 1` | `'B'` | `Char` |
|
||||
| `'z' - 'a'` | `25` | `Int` |
|
||||
| `'A'.code` | `65` | `Int` |
|
||||
| `65.toChar()` | `'A'` | `Char` |
|
||||
|
||||
## Проверки категорий
|
||||
```kotlin
|
||||
println('a'.isLetter()) // true
|
||||
println('5'.isDigit()) // true
|
||||
println('a'.isLetterOrDigit()) // true
|
||||
println(' '.isWhitespace()) // true
|
||||
println('a'.isLowerCase()) // true
|
||||
println('A'.isUpperCase()) // true
|
||||
```
|
||||
Все эти функции учитывают Unicode: `'я'.isLetter()` тоже `true`.
|
||||
|
||||
## Регистр
|
||||
```kotlin
|
||||
println('a'.uppercaseChar()) // A — возвращает Char
|
||||
println('A'.lowercaseChar()) // a
|
||||
println('a'.uppercase()) // "A" — возвращает String
|
||||
println('ß'.uppercase()) // "SS" — два символа, поэтому String
|
||||
```
|
||||
Есть две версии: `uppercaseChar()` даёт `Char`, `uppercase()` - `String`. Вторая нужна, потому что в Unicode регистр не всегда меняется один-к-одному.
|
||||
|
||||
## Цифры
|
||||
```kotlin
|
||||
println('7'.digitToInt()) // 7 — число, а не код символа
|
||||
println('f'.digitToInt(16)) // 15
|
||||
println('x'.digitToIntOrNull()) // null — безопасная версия
|
||||
println(7.digitToChar()) // '7'
|
||||
```
|
||||
Это главный способ разобрать цифру. Старый трюк из Java (`c - '0'`) тоже работает, но `digitToInt()` понятнее и проверяет корректность:
|
||||
```kotlin
|
||||
println('7' - '0') // 7 — работает
|
||||
// println('x'.digitToInt()) // IllegalArgumentException
|
||||
```
|
||||
|
||||
## Диапазоны и сравнение
|
||||
```kotlin
|
||||
println('a' in 'a'..'z') // true
|
||||
println('5' in '0'..'9') // true
|
||||
for (c in 'a'..'e') print(c) // abcde
|
||||
println('a' < 'b') // true — по коду
|
||||
println(('a'..'e').toList()) // [a, b, c, d, e]
|
||||
```
|
||||
|
||||
## Строки и Char
|
||||
```kotlin
|
||||
val s = "Kotlin"
|
||||
println(s[0]) // K — элемент строки это Char
|
||||
println(s.first()) // K
|
||||
println(s.toCharArray().size) // 6
|
||||
println(String(charArrayOf('a','b'))) // ab — собрать строку из массива
|
||||
println(s.toList()) // [K, o, t, l, i, n]
|
||||
println("abc".map { it + 1 }) // [b, c, d]
|
||||
println(charArrayOf('a','b').concatToString()) // ab
|
||||
```
|
||||
|
||||
Приписать `Char` к строке можно сложением - слева обязательно строка:
|
||||
```kotlin
|
||||
println("ab" + 'c') // abc
|
||||
// println('c' + "ab") // ошибка: Char.plus ожидает Int
|
||||
println("${'c'}ab") // cab — через шаблон
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `char` - числовой тип | `Char` - отдельный тип |
|
||||
> | `int n = c;` работает | нужен `c.code` |
|
||||
> | `Character.isDigit(c)` | `c.isDigit()` |
|
||||
> | `Character.toUpperCase(c)` | `c.uppercaseChar()` |
|
||||
> | `Character.getNumericValue(c)` | `c.digitToInt()` |
|
||||
> | `c - '0'` для цифры | `c.digitToInt()` |
|
||||
> | `switch (c)` по числам | `when (c)` по символам |
|
||||
>
|
||||
> В Java `char` участвует в арифметике как число, из-за чего `System.out.println('a' + 1)` печатает `98`, а не `b`. В Kotlin тот же код даёт `b` - тип сохраняется.
|
||||
>
|
||||
> Все утилиты из `Character` стали методами `Char`, поэтому пишутся через точку и находятся автодополнением.
|
||||
>
|
||||
> Сравнить: [Character в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Character.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Char` не вмещает символы за пределами BMP.** Эмодзи и редкие иероглифы кодируются двумя `Char` (surrogate pair):
|
||||
> ```kotlin
|
||||
> val emoji = "😀"
|
||||
> println(emoji.length) // 2
|
||||
> println(emoji[0].isLetter()) // false — половина пары бессмысленна
|
||||
> println(emoji.codePointAt(0)) // 128512 — полный код символа
|
||||
> ```
|
||||
> Для посимвольной обработки текста с эмодзи нужны кодовые точки, а не `Char`.
|
||||
>
|
||||
> **`digitToInt()` бросает исключение, а не возвращает -1:**
|
||||
> ```kotlin
|
||||
> println('x'.digitToIntOrNull()) // null — безопасно
|
||||
> // println('x'.digitToInt()) // IllegalArgumentException
|
||||
> ```
|
||||
>
|
||||
> **`'1'.code` - это 49, а не 1.** Путать код символа с его числовым значением - классическая ошибка:
|
||||
> ```kotlin
|
||||
> println('1'.code) // 49
|
||||
> println('1'.digitToInt()) // 1
|
||||
> ```
|
||||
>
|
||||
> **`Char` в `when` требует `else`** - перечислить все 65 536 значений компилятор не может.
|
||||
>
|
||||
> **`uppercase()` возвращает `String`, и это не придирка.** `'ß'.uppercase()` даёт `"SS"` - два символа. Если нужен именно `Char`, используйте `uppercaseChar()`, но помните, что для `'ß'` он вернёт сам `'ß'`.
|
||||
|
||||
Далее: [Числа и переполнение](Числа%20и%20переполнение.md)
|
||||
@@ -1,218 +0,0 @@
|
||||
---
|
||||
title: Деструктуризация
|
||||
description: Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
Деструктуризация - разбор объекта на отдельные переменные одной конструкцией.
|
||||
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val (x, y) = Point(1, 2)
|
||||
println("$x $y") // 1 2
|
||||
}
|
||||
```
|
||||
|
||||
Компилятор превращает это в вызовы `component1()`, `component2()` и так далее:
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val p = Point(1, 2)
|
||||
val x = p.component1()
|
||||
val y = p.component2()
|
||||
println("$x $y") // 1 2
|
||||
}
|
||||
```
|
||||
|
||||
## Что можно деструктурировать
|
||||
Любой тип, у которого есть функции `componentN()`. Автоматически они появляются у:
|
||||
- [data class](../Базовые%20типы%20ООП/Data%20class.md) - по свойствам основного конструктора
|
||||
- `Pair` и `Triple`
|
||||
- `Map.Entry` - ключ и значение
|
||||
- списков и массивов (первые 5 элементов)
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val (a, b) = Pair(1, "x")
|
||||
val (c, d, e) = Triple(1, 2, 3)
|
||||
val (first, second) = listOf("a", "b", "c") // третий игнорируется
|
||||
println("$a $b | $c $d $e | $first $second")
|
||||
}
|
||||
```
|
||||
|
||||
## Свой componentN
|
||||
Для обычного класса операторы объявляются вручную:
|
||||
```kotlin
|
||||
class Range2(val from: Int, val to: Int) {
|
||||
operator fun component1() = from
|
||||
operator fun component2() = to
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val (from, to) = Range2(1, 10)
|
||||
println("$from..$to") // 1..10
|
||||
}
|
||||
```
|
||||
Слово `operator` обязательно.
|
||||
|
||||
## Пропуск компонентов
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val (_, name, _) = User(1, "Иван", "i@x.ru")
|
||||
println(name) // Иван
|
||||
}
|
||||
```
|
||||
Подчёркивание означает «этот компонент не нужен» - `componentN()` для него даже не вызовется.
|
||||
|
||||
Если нужны только первые компоненты, лишние просто не перечисляют:
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val (id, name) = User(1, "Иван", "i@x.ru") // email не берём
|
||||
println("$id $name") // 1 Иван
|
||||
}
|
||||
```
|
||||
|
||||
## В циклах
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
for ((key, value) in map) {
|
||||
println("$key=$value") // a=1, затем b=2
|
||||
}
|
||||
|
||||
val list = listOf("x", "y")
|
||||
for ((index, item) in list.withIndex()) {
|
||||
println("$index:$item") // 0:x, затем 1:y
|
||||
}
|
||||
}
|
||||
```
|
||||
Это самое частое применение деструктуризации.
|
||||
|
||||
## В лямбдах
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
map.forEach { (k, v) -> println("$k$v") } // a1 b2
|
||||
println(map.map { (k, v) -> "$k=$v" }) // [a=1, b=2]
|
||||
map.forEach { (_, v) -> print(v) } // 12
|
||||
println()
|
||||
|
||||
val points = listOf(1 to 2, 3 to 4)
|
||||
println(points.map { (x, y) -> x + y }) // [3, 7]
|
||||
}
|
||||
```
|
||||
Параметр в скобках `{ (k, v) -> }` - это **один** параметр, который разбирается, а не два отдельных. Без скобок `{ k, v -> }` лямбда ожидала бы два аргумента, и код не скомпилировался бы.
|
||||
|
||||
## Возврат нескольких значений
|
||||
Классическое применение - функция, которая должна вернуть пару значений:
|
||||
```kotlin
|
||||
data class Result2(val value: Int, val error: String?)
|
||||
|
||||
fun parse(s: String): Result2 =
|
||||
s.toIntOrNull()?.let { Result2(it, null) } ?: Result2(0, "не число")
|
||||
|
||||
fun main() {
|
||||
val (value, error) = parse("42")
|
||||
println("$value $error") // 42 null
|
||||
|
||||
val (v2, e2) = parse("abc")
|
||||
println("$v2 $e2") // 0 не число
|
||||
}
|
||||
```
|
||||
`Pair` для этого тоже подходит, но свой `data class` лучше: имена компонентов осмысленны, а не `first`/`second`.
|
||||
|
||||
## Деструктуризация по именам (Kotlin 2.3+)
|
||||
Классическая деструктуризация позиционная - порядок переменных должен совпадать с порядком свойств. Это хрупко: перестановка свойств в `data class` молча меняет смысл кода.
|
||||
|
||||
С Kotlin 2.3 разрабатывается деструктуризация по именам. Синтаксис меняется так:
|
||||
- `val [a, b] = obj` - **квадратные** скобки, позиционно (как было)
|
||||
- `val (name, email) = obj` - **круглые** скобки, теперь **по именам** свойств
|
||||
|
||||
```kotlin
|
||||
data class User(val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val user = User("ИМЯ", "ПОЧТА")
|
||||
|
||||
val (email, name) = user // по именам — порядок не важен
|
||||
println("name=$name email=$email") // name=ИМЯ email=ПОЧТА
|
||||
|
||||
val [first, second] = user // позиционно
|
||||
println("first=$first") // first=ИМЯ
|
||||
}
|
||||
```
|
||||
|
||||
Фича пока за флагом компилятора:
|
||||
```bash
|
||||
kotlinc -Xname-based-destructuring=complete App.kt
|
||||
```
|
||||
Значения флага: `only-syntax` (включить синтаксис `[]` и полную форму), `name-mismatch` (предупреждать, когда имена в позиционной деструктуризации не совпадают со свойствами), `complete` (включить короткую форму по именам).
|
||||
|
||||
> [!warning] Это ломающее изменение
|
||||
> Круглые скобки меняют смысл: был порядок, станут имена. Код `val (email, name) = user` сегодня присвоит `email` первое свойство, а после включения фичи - свойство с именем `email`. Пока флаг не включён, вся деструктуризация в вашем коде позиционная.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java деструктуризации объектов нет. Ближайший аналог - record patterns из Java 21:
|
||||
> ```java
|
||||
> record Point(int x, int y) {}
|
||||
> if (obj instanceof Point(int x, int y)) { ... }
|
||||
> ```
|
||||
> Но это работает только в `instanceof` и `switch`, а не при обычном объявлении переменных. Разбор `Map.Entry` в цикле в Java невозможен - нужно писать `e.getKey()` и `e.getValue()`.
|
||||
>
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `var k = e.getKey(); var v = e.getValue();` | `val (k, v) = e` |
|
||||
> | `for (var e : map.entrySet())` + геттеры | `for ((k, v) in map)` |
|
||||
> | `p.x(); p.y();` | `val (x, y) = p` |
|
||||
> | нет возврата нескольких значений | `Pair`, `Triple`, `data class` |
|
||||
>
|
||||
> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Деструктуризация позиционная, а не по именам.** Это главная опасность:
|
||||
> ```kotlin
|
||||
> data class User(val name: String, val email: String)
|
||||
>
|
||||
> fun main() {
|
||||
> val (name, email) = User("Иван", "i@x.ru")
|
||||
> println("$name / $email") // Иван / i@x.ru
|
||||
> }
|
||||
> ```
|
||||
> Если позже в `data class` поменять порядок свойств на `(email, name)`, этот код **продолжит компилироваться**, но переменные получат чужие значения. Компилятор не поможет, потому что типы совпадают. Поэтому порядок свойств в `data class` - часть публичного API.
|
||||
>
|
||||
> **У обычного класса деструктуризации нет** - только у `data class` или при ручном объявлении `componentN()`.
|
||||
>
|
||||
> **В лямбде нужны скобки:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val map = mapOf("a" to 1)
|
||||
> map.forEach { (k, v) -> println("$k$v") } // верно: один параметр-пара
|
||||
> map.forEach { e -> println("${e.key}${e.value}") } // тоже верно
|
||||
> }
|
||||
> ```
|
||||
> Запись `{ k, v -> }` для `forEach` на `Map` не скомпилируется.
|
||||
>
|
||||
> **Список короче числа переменных - исключение в рантайме:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1)
|
||||
> val (a, b) = list // компилируется!
|
||||
> println("$a $b") // IndexOutOfBoundsException
|
||||
> }
|
||||
> ```
|
||||
> У `List` есть `component1()`..`component5()`, и компилятор не знает фактическую длину. С `data class` такой проблемы нет.
|
||||
>
|
||||
> **`component1()` вызывается даже для неиспользуемой переменной** - если это не `_`. Для свойства с тяжёлым геттером лишняя работа.
|
||||
>
|
||||
> **Деструктуризация `Pair` теряет смысл имён.** `val (a, b) = getCoordinates()` не говорит, что где. Лучше `data class Coordinates(val lat: Double, val lon: Double)`.
|
||||
|
||||
Далее: [Приведение типов](Приведение%20типов.md)
|
||||
@@ -1,273 +0,0 @@
|
||||
---
|
||||
title: Лямбды и функциональные типы
|
||||
description: Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции
|
||||
tags:
|
||||
- kotlin
|
||||
- functional
|
||||
---
|
||||
|
||||
Функциональный тип описывает функцию как значение: `(параметры) -> результат`.
|
||||
|
||||
```kotlin
|
||||
val square: (Int) -> Int = { x -> x * x }
|
||||
val greet: (String) -> String = { "Привет, $it" }
|
||||
val action: () -> Unit = { println("сделано") }
|
||||
val sum: (Int, Int) -> Int = { a, b -> a + b }
|
||||
|
||||
fun main() {
|
||||
println(square(4)) // 16
|
||||
println(greet("мир")) // Привет, мир
|
||||
action() // сделано
|
||||
println(sum(2, 3)) // 5
|
||||
}
|
||||
```
|
||||
|
||||
## Синтаксис лямбды
|
||||
Лямбда всегда в фигурных скобках:
|
||||
```kotlin
|
||||
{ параметры -> тело }
|
||||
```
|
||||
```kotlin
|
||||
val f = { x: Int, y: Int -> x + y } // типы указаны в лямбде
|
||||
val g: (Int) -> Int = { it * 2 } // тип указан у переменной
|
||||
```
|
||||
Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть.
|
||||
|
||||
Результат - **последнее выражение** тела, `return` не нужен и в обычной лямбде запрещён:
|
||||
```kotlin
|
||||
val describe: (Int) -> String = { n ->
|
||||
val prefix = if (n > 0) "плюс" else "минус"
|
||||
"$prefix $n" // это и есть результат
|
||||
}
|
||||
|
||||
fun main() { println(describe(5)) } // плюс 5
|
||||
```
|
||||
|
||||
## it - неявный параметр
|
||||
Если параметр один, его можно не объявлять:
|
||||
```kotlin
|
||||
val double: (Int) -> Int = { it * 2 }
|
||||
println(listOf(1, 2, 3).map { it * 10 }) // [10, 20, 30]
|
||||
```
|
||||
`it` удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя:
|
||||
```kotlin
|
||||
val users = listOf("Иван", "Пётр")
|
||||
println(users.map { name -> name.uppercase() }) // [ИВАН, ПЁТР]
|
||||
```
|
||||
|
||||
## Лямбда последним аргументом
|
||||
Если последний параметр функции - функция, лямбду выносят за скобки:
|
||||
```kotlin
|
||||
fun measure(name: String, block: () -> Unit) {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
println("$name занял ${System.nanoTime() - start} нс")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
measure("цикл") { // лямбда вне скобок
|
||||
repeat(1000) { }
|
||||
}
|
||||
measure("пусто", { }) // так тоже можно, но не принято
|
||||
}
|
||||
```
|
||||
Когда функция принимает **только** лямбду, круглые скобки опускаются совсем. Именно на этом правиле построены `forEach`, `let`, `apply` и все DSL:
|
||||
```kotlin
|
||||
listOf(1, 2).forEach { println(it) }
|
||||
```
|
||||
Подробнее: [DSL](../../Функциональный%20Kotlin/DSL.md)
|
||||
|
||||
## Неиспользуемые параметры
|
||||
```kotlin
|
||||
val map = mapOf("a" to 1)
|
||||
map.forEach { (_, value) -> println(value) } // 1 — ключ не нужен
|
||||
```
|
||||
Подчёркивание говорит компилятору, что параметр намеренно игнорируется.
|
||||
|
||||
## Ссылки на функции - ::
|
||||
Вместо лямбды, которая только вызывает существующую функцию, используют ссылку:
|
||||
```kotlin
|
||||
fun isEven(n: Int) = n % 2 == 0
|
||||
|
||||
fun main() {
|
||||
val nums = listOf(1, 2, 3, 4)
|
||||
println(nums.filter { isEven(it) }) // [2, 4] — лямбда-обёртка
|
||||
println(nums.filter(::isEven)) // [2, 4] — ссылка, короче
|
||||
nums.forEach(::println)
|
||||
}
|
||||
```
|
||||
|
||||
Виды ссылок:
|
||||
```kotlin
|
||||
class User(val name: String) {
|
||||
fun greet() = "Привет, $name"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val byName: (User) -> String = User::name // ссылка на свойство
|
||||
val greeter: (User) -> String = User::greet // на метод класса
|
||||
val factory: (String) -> User = ::User // на конструктор
|
||||
|
||||
val u = User("Иван")
|
||||
println(byName(u)) // Иван
|
||||
println(greeter(u)) // Привет, Иван
|
||||
println(factory("Пётр").name) // Пётр
|
||||
|
||||
val bound = u::greet // привязанная к объекту
|
||||
println(bound()) // Привет, Иван
|
||||
}
|
||||
```
|
||||
|
||||
## Анонимные функции
|
||||
Когда нужен явный `return` или явный тип результата:
|
||||
```kotlin
|
||||
val parse = fun(s: String): Int? {
|
||||
if (s.isBlank()) return null // return работает
|
||||
return s.toIntOrNull()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parse(" ")) // null
|
||||
println(parse("42")) // 42
|
||||
}
|
||||
```
|
||||
Отличие от лямбды: `return` выходит из самой анонимной функции, а в лямбде он запрещён (или выходит из внешней функции, если лямбда inline).
|
||||
|
||||
## Функциональные типы в подробностях
|
||||
```kotlin
|
||||
val a: () -> Unit // без параметров, без результата
|
||||
val b: (Int) -> String // один параметр
|
||||
val c: (Int, Int) -> Int // два параметра
|
||||
val d: ((Int) -> Int) -> Int // принимает функцию
|
||||
val e: (Int) -> ((Int) -> Int) // возвращает функцию
|
||||
val f: String.(Int) -> String // с получателем (receiver)
|
||||
val g: ((Int) -> Int)? // nullable
|
||||
val h: suspend () -> Unit // suspend-функция
|
||||
```
|
||||
|
||||
Тип с получателем вызывается как метод:
|
||||
```kotlin
|
||||
val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) }
|
||||
|
||||
fun main() {
|
||||
println("ab".repeatTimes(2)) // abab
|
||||
}
|
||||
```
|
||||
На этом построены `apply`, `buildString` и DSL. Подробнее: [Receivers и context parameters](../../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
|
||||
|
||||
## Замыкания
|
||||
Лямбда захватывает переменные из окружения - **включая изменяемые**:
|
||||
```kotlin
|
||||
fun counter(): () -> Int {
|
||||
var count = 0 // захвачена по ссылке
|
||||
return { ++count }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val next = counter()
|
||||
println(next()) // 1
|
||||
println(next()) // 2
|
||||
println(next()) // 3
|
||||
}
|
||||
```
|
||||
Это важное отличие от Java, где захватить можно только effectively final переменную.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
var sum = 0
|
||||
listOf(1, 2, 3).forEach { sum += it } // в Java так нельзя
|
||||
println(sum) // 6
|
||||
}
|
||||
```
|
||||
|
||||
## Вызов
|
||||
```kotlin
|
||||
val f: (Int) -> Int = { it + 1 }
|
||||
|
||||
fun main() {
|
||||
println(f(1)) // 2 — обычный вызов
|
||||
println(f.invoke(1)) // 2 — то же самое
|
||||
val g: ((Int) -> Int)? = null
|
||||
println(g?.invoke(1)) // null — безопасный вызов nullable-функции
|
||||
}
|
||||
```
|
||||
|
||||
## Что происходит под капотом
|
||||
Функциональный тип компилируется в интерфейс `FunctionN`:
|
||||
```kotlin
|
||||
val f: (Int) -> Int = { it * 2 } // объект класса Function1<Int, Int>
|
||||
```
|
||||
Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует `inline` - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе.
|
||||
|
||||
Подробнее: [Inline-функции](../../Функциональный%20Kotlin/Inline-функции.md), [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `x -> x * 2` | `{ x -> x * 2 }` или `{ it * 2 }` |
|
||||
> | `Function<Integer,Integer>` | `(Int) -> Int` |
|
||||
> | `BiFunction<A,B,C>` | `(A, B) -> C` |
|
||||
> | `Supplier<T>` | `() -> T` |
|
||||
> | `Consumer<T>` | `(T) -> Unit` |
|
||||
> | `Predicate<T>` | `(T) -> Boolean` |
|
||||
> | `Runnable` | `() -> Unit` |
|
||||
> | `String::length` | `String::length` |
|
||||
> | нет неявного параметра | `it` |
|
||||
> | захват только effectively final | захват изменяемых переменных |
|
||||
> | лямбда только для SAM-интерфейса | функциональный тип первого класса |
|
||||
>
|
||||
> В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (`Function`, `BiFunction`, `Supplier`, `Consumer`, `Predicate`…). В Kotlin тип функции записывается напрямую: `(Int, String) -> Boolean` не требует придумывать имя.
|
||||
>
|
||||
> Захват изменяемых переменных - практическая разница. Java требует `final` или effectively final, вынуждая использовать массив-обёртку `int[] sum = {0}`. В Kotlin `var sum = 0` захватывается как есть (компилятор оборачивает его в `Ref`-объект).
|
||||
>
|
||||
> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`return` в лямбде запрещён** - он относился бы к внешней функции:
|
||||
> ```kotlin
|
||||
> fun find(items: List<Int>): Int? {
|
||||
> items.forEach {
|
||||
> if (it > 1) return it // выход из find, а не из лямбды!
|
||||
> } // работает только потому, что forEach inline
|
||||
> return null
|
||||
> }
|
||||
>
|
||||
> fun main() { println(find(listOf(1, 2, 3))) } // 2
|
||||
> ```
|
||||
> Это «нелокальный return», он разрешён только в inline-функциях. Для выхода из самой лямбды нужна метка:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> listOf(1, 2, 3).forEach {
|
||||
> if (it == 2) return@forEach // пропустить итерацию, как continue
|
||||
> println(it) // 1, затем 3
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`it` во вложенных лямбдах затеняется:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val nested = listOf(listOf(1, 2))
|
||||
> // println(nested.map { it.map { it * 2 } }) // непонятно, какое it
|
||||
> println(nested.map { row -> row.map { v -> v * 2 } }) // [[2, 4]]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Каждая не-inline лямбда с захватом - объект.** В цикле на миллион итераций это миллион аллокаций. Стандартные операции (`map`, `filter`, `forEach`) объявлены `inline`, поэтому бесплатны; свои функции высшего порядка в горячем коде тоже стоит делать `inline`.
|
||||
>
|
||||
> **Лямбда, захватившая `this`, держит объект в памяти** - классическая утечка в Android, когда колбэк переживает активити.
|
||||
>
|
||||
> **Ссылка на перегруженную функцию неоднозначна:**
|
||||
> ```kotlin
|
||||
> fun show(x: Int) = "int"
|
||||
> fun show(x: String) = "str"
|
||||
>
|
||||
> fun main() {
|
||||
> // val f = ::show // ошибка: какая из двух?
|
||||
> val f: (Int) -> String = ::show // тип разрешает неоднозначность
|
||||
> println(f(1)) // int
|
||||
> }
|
||||
> ```
|
||||
|
||||
Дополнительно: [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
|
||||
Далее: [Деструктуризация](Деструктуризация.md)
|
||||
@@ -1,249 +0,0 @@
|
||||
---
|
||||
title: Массивы
|
||||
description: Array, специализированные IntArray, сравнение через contentEquals и почему обычно нужен List
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
Массив - фиксированная по размеру последовательность элементов. В Kotlin это класс `Array<T>`, а не синтаксическая конструкция.
|
||||
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val b = Array(3) { it * 2 } // [0, 2, 4] — по лямбде от индекса
|
||||
val c = arrayOfNulls<String>(3) // [null, null, null]
|
||||
val d = emptyArray<Int>()
|
||||
|
||||
println(a[0]) // 1
|
||||
a[0] = 10
|
||||
println(a.size) // 3
|
||||
println(a.joinToString()) // 10, 2, 3
|
||||
```
|
||||
Размер задаётся при создании и не меняется. Элементы изменяемы всегда - даже если массив объявлен через `val`.
|
||||
|
||||
## Специализированные массивы
|
||||
Для чисел есть отдельные классы без упаковки:
|
||||
```kotlin
|
||||
val ints = intArrayOf(1, 2, 3)
|
||||
val zeros = IntArray(5) // [0, 0, 0, 0, 0] — заполнен нулями
|
||||
val squares = IntArray(5) { it * it } // [0, 1, 4, 9, 16]
|
||||
val doubles = doubleArrayOf(1.0, 2.0)
|
||||
val chars = charArrayOf('a', 'b')
|
||||
val bools = BooleanArray(3) // [false, false, false]
|
||||
```
|
||||
|
||||
| Класс | Байт-код | Аналог в Java |
|
||||
|---|---|---|
|
||||
| `IntArray` | `int[]` | `int[]` |
|
||||
| `LongArray` | `long[]` | `long[]` |
|
||||
| `DoubleArray` | `double[]` | `double[]` |
|
||||
| `CharArray` | `char[]` | `char[]` |
|
||||
| `BooleanArray` | `boolean[]` | `boolean[]` |
|
||||
| `Array<Int>` | `Integer[]` | `Integer[]` |
|
||||
|
||||
Разница принципиальна: `Array<Int>` хранит **упакованные** объекты, `IntArray` - примитивы. В горячем коде это заметно по памяти и скорости.
|
||||
|
||||
```kotlin
|
||||
val boxed: Array<Int> = arrayOf(1, 2, 3) // Integer[] — аллокация на элемент
|
||||
val raw: IntArray = intArrayOf(1, 2, 3) // int[] — без упаковки
|
||||
println("${boxed.size} ${raw.size}")
|
||||
```
|
||||
|
||||
## Сравнение - только contentEquals
|
||||
`==` на массивах сравнивает **ссылки**, а не содержимое. Это главная ловушка:
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val b = arrayOf(1, 2, 3)
|
||||
println(a == b) // false — разные объекты!
|
||||
println(a.contentEquals(b)) // true — так правильно
|
||||
println(a.contentHashCode()) // хеш по содержимому
|
||||
```
|
||||
Для вложенных массивов - глубокие версии:
|
||||
```kotlin
|
||||
val x = arrayOf(arrayOf(1), arrayOf(2))
|
||||
val y = arrayOf(arrayOf(1), arrayOf(2))
|
||||
println(x.contentEquals(y)) // false — сравнивает ссылки вложенных
|
||||
println(x.contentDeepEquals(y)) // true
|
||||
```
|
||||
|
||||
## Печать
|
||||
`toString()` массива бесполезен:
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
println(a) // [Ljava.lang.Integer;@1b6d3586
|
||||
println(a.contentToString()) // [1, 2, 3]
|
||||
println(a.joinToString()) // 1, 2, 3
|
||||
println("$a".startsWith("[L")) // true — шаблон вызывает тот же toString
|
||||
```
|
||||
Внутри строкового шаблона всегда нужен `contentToString()` или `joinToString()`.
|
||||
|
||||
## Обход
|
||||
```kotlin
|
||||
val a = arrayOf("x", "y", "z")
|
||||
for (item in a) print(item) // xyz
|
||||
for (i in a.indices) print(a[i]) // xyz
|
||||
for ((i, v) in a.withIndex()) print("$i$v ") // 0x 1y 2z
|
||||
a.forEach { print(it) } // xyz
|
||||
a.forEachIndexed { i, v -> print("$i$v ") }
|
||||
println()
|
||||
println(a.lastIndex) // 2
|
||||
```
|
||||
|
||||
## Операции
|
||||
Массивы поддерживают те же операции, что коллекции:
|
||||
```kotlin
|
||||
val a = intArrayOf(3, 1, 2)
|
||||
println(a.sorted()) // [1, 2, 3] — возвращает List!
|
||||
println(a.sortedArray()) // новый IntArray
|
||||
a.sort() // сортировка на месте
|
||||
println(a.contentToString()) // [1, 2, 3]
|
||||
println(a.sum()) // 6
|
||||
println(a.average()) // 2.0
|
||||
println(a.max()) // 3
|
||||
println(a.filter { it > 1 }) // [2, 3] — List
|
||||
println(a.map { it * 2 }) // [2, 4, 6] — List
|
||||
println(a.reversed()) // [3, 2, 1] — List
|
||||
println(a.reversedArray().contentToString()) // [3, 2, 1]
|
||||
println(a.contains(2)) // true
|
||||
println(2 in a) // true
|
||||
println(a.indexOf(2)) // 1
|
||||
```
|
||||
Обратите внимание: `filter`, `map`, `sorted`, `reversed` возвращают `List`, а не массив. Версии, сохраняющие тип, имеют суффикс `Array`.
|
||||
|
||||
## Копирование и изменение размера
|
||||
Размер массива неизменен, поэтому «добавление» - это создание нового:
|
||||
```kotlin
|
||||
val a = intArrayOf(1, 2, 3)
|
||||
println((a + 4).contentToString()) // [1, 2, 3, 4] — новый массив
|
||||
println(a.copyOf().contentToString()) // [1, 2, 3]
|
||||
println(a.copyOf(5).contentToString()) // [1, 2, 3, 0, 0] — дополнен нулями
|
||||
println(a.copyOf(2).contentToString()) // [1, 2] — обрезан
|
||||
println(a.copyOfRange(1, 3).contentToString()) // [2, 3]
|
||||
println(a.sliceArray(0..1).contentToString()) // [1, 2]
|
||||
a.fill(0)
|
||||
println(a.contentToString()) // [0, 0, 0]
|
||||
```
|
||||
|
||||
## Преобразование в коллекции и обратно
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val list: List<Int> = a.toList()
|
||||
val set = a.toSet()
|
||||
val back: Array<Int> = list.toTypedArray()
|
||||
val raw: IntArray = list.toIntArray()
|
||||
println("${list} ${set} ${back.size} ${raw.size}")
|
||||
```
|
||||
|
||||
## Многомерные массивы
|
||||
Отдельного типа нет - это массив массивов:
|
||||
```kotlin
|
||||
val grid = Array(3) { IntArray(4) } // 3x4, заполнен нулями
|
||||
grid[1][2] = 5
|
||||
println(grid[1].contentToString()) // [0, 0, 5, 0]
|
||||
|
||||
val table = Array(2) { r -> IntArray(2) { c -> r * 2 + c } }
|
||||
println(table.joinToString { it.contentToString() }) // [0, 1], [2, 3]
|
||||
```
|
||||
Строки могут иметь разную длину (рваный массив):
|
||||
```kotlin
|
||||
val jagged = arrayOf(intArrayOf(1), intArrayOf(1, 2, 3))
|
||||
println(jagged.joinToString { it.size.toString() }) // 1, 3
|
||||
```
|
||||
|
||||
## Когда массив, а когда List
|
||||
Массив нужен редко. Используйте его, если:
|
||||
- важна производительность и нужны примитивы без упаковки (`IntArray`)
|
||||
- работаете с API, требующим массив (`vararg`, Java-библиотеки)
|
||||
- размер фиксирован и известен заранее
|
||||
|
||||
В остальных случаях - `List`: он не даёт менять себя случайно, имеет больше операций и корректный `equals`.
|
||||
|
||||
```kotlin
|
||||
val list = listOf(1, 2, 3)
|
||||
println(list == listOf(1, 2, 3)) // true — работает из коробки
|
||||
```
|
||||
Подробнее: [Коллекции](../../Коллекции/Коллекции.md)
|
||||
|
||||
## vararg
|
||||
Параметр с переменным числом аргументов внутри функции - массив:
|
||||
```kotlin
|
||||
fun sum(vararg nums: Int): Int {
|
||||
var total = 0
|
||||
for (n in nums) total += n // nums это IntArray
|
||||
return total
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(sum(1, 2, 3)) // 6
|
||||
println(sum()) // 0
|
||||
val arr = intArrayOf(1, 2)
|
||||
println(sum(*arr)) // 3 — spread-оператор
|
||||
}
|
||||
```
|
||||
Подробнее: [Функции](Функции.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `int[] a = {1,2,3}` | `val a = intArrayOf(1,2,3)` |
|
||||
> | `new int[5]` | `IntArray(5)` |
|
||||
> | `a.length` | `a.size` |
|
||||
> | `Arrays.toString(a)` | `a.contentToString()` |
|
||||
> | `Arrays.equals(a,b)` | `a.contentEquals(b)` |
|
||||
> | `Arrays.deepEquals(a,b)` | `a.contentDeepEquals(b)` |
|
||||
> | `Arrays.sort(a)` | `a.sort()` |
|
||||
> | `Arrays.copyOf(a,n)` | `a.copyOf(n)` |
|
||||
> | `Arrays.fill(a,0)` | `a.fill(0)` |
|
||||
> | `int[]` и `Integer[]` | `IntArray` и `Array<Int>` |
|
||||
> | `f(arr)` для varargs | `f(*arr)` — нужен spread |
|
||||
>
|
||||
> Статические методы `Arrays` стали методами массива - находятся автодополнением, а не поиском по документации.
|
||||
>
|
||||
> Разделение `IntArray` / `Array<Int>` сделано намеренно: в Java `Integer[]` и `int[]` тоже разные, но это легко не заметить. В Kotlin разные имена не позволяют спутать упакованный массив с примитивным.
|
||||
>
|
||||
> **Массивы в Kotlin инвариантны**, в Java - ковариантны. Java разрешает `Object[] o = new String[1]; o[0] = 1;` и падает в рантайме (`ArrayStoreException`). В Kotlin `Array<Any> = Array<String>` не скомпилируется. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
|
||||
>
|
||||
> Сравнить: [Массивы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`==` на массивах сравнивает ссылки.** Самая частая ошибка:
|
||||
> ```kotlin
|
||||
> val a = arrayOf(1, 2)
|
||||
> println(a == arrayOf(1, 2)) // false
|
||||
> println(a.contentEquals(arrayOf(1, 2))) // true
|
||||
> ```
|
||||
> По этой же причине массив нельзя использовать как ключ `Map` или в `Set` - `hashCode` считается по ссылке.
|
||||
>
|
||||
> **Массив внутри `data class` ломает `equals`:**
|
||||
> ```kotlin
|
||||
> data class Packet(val data: ByteArray)
|
||||
>
|
||||
> fun main() {
|
||||
> val p1 = Packet(byteArrayOf(1))
|
||||
> println(p1 == Packet(byteArrayOf(1))) // false!
|
||||
> }
|
||||
> ```
|
||||
> Сгенерированный `equals` сравнивает массив по ссылке, а не по содержимому. Поэтому в `data class` с массивом `equals`/`hashCode` пишут руками. Подробнее: [Data class](../Базовые%20типы%20ООП/Data%20class.md)
|
||||
>
|
||||
> **`println(array)` печатает мусор** вида `[Ljava.lang.Integer;@1b6d3586`. То же в строковых шаблонах.
|
||||
>
|
||||
> **`val` не защищает элементы:**
|
||||
> ```kotlin
|
||||
> val a = intArrayOf(1, 2)
|
||||
> a[0] = 99 // разрешено
|
||||
> println(a.contentToString()) // [99, 2]
|
||||
> ```
|
||||
> Неизменяемость даёт `List`, а не `val`.
|
||||
>
|
||||
> **`Array<Int>` вместо `IntArray` - скрытая просадка.** Каждый элемент упакован в `Integer`: больше памяти и промахи кеша.
|
||||
>
|
||||
> **`arrayOfNulls` и `Array(n)` для непримитивных типов требуют разного подхода:**
|
||||
> ```kotlin
|
||||
> val a = arrayOfNulls<String>(3) // Array<String?>
|
||||
> val b = Array(3) { "" } // Array<String> — инициализатор обязателен
|
||||
> println("${a.size} ${b.size}")
|
||||
> ```
|
||||
> Создать `Array<String>` без инициализатора нельзя - компилятор не позволит оставить non-null элементы пустыми.
|
||||
|
||||
Дополнительно: [Коллекции](../../Коллекции/Коллекции.md)
|
||||
Далее: [Функции](Функции.md)
|
||||
@@ -1,254 +0,0 @@
|
||||
---
|
||||
title: Приведение типов
|
||||
description: Проверка is, приведение as и as?, числовые преобразования и стирание типов
|
||||
tags:
|
||||
- kotlin
|
||||
- types
|
||||
---
|
||||
|
||||
## Проверка типа - is
|
||||
```kotlin
|
||||
fun describe(x: Any): String = when {
|
||||
x is String -> "строка длины ${x.length}"
|
||||
x is Int -> "число ${x + 1}"
|
||||
x !is Boolean -> "не логическое"
|
||||
else -> "логическое"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe("ab")) // строка длины 2
|
||||
println(describe(5)) // число 6
|
||||
println(describe(1.0)) // не логическое
|
||||
}
|
||||
```
|
||||
После успешной проверки компилятор сам приводит тип - это [умное приведение](../Null-безопасность/Умные%20приведения.md), явный каст не нужен.
|
||||
|
||||
## Небезопасное приведение - as
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
val s = x as String // ok
|
||||
println(s.length) // 4
|
||||
|
||||
val n: Any = 42
|
||||
// val bad = n as String // ClassCastException
|
||||
}
|
||||
```
|
||||
`as` используют, когда тип известен точно, а компилятор об этом знать не может.
|
||||
|
||||
## Безопасное приведение - as?
|
||||
Возвращает `null` вместо исключения:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
println(x as? Int) // null
|
||||
println(x as? String) // text
|
||||
println((x as? Int) ?: 0) // 0 — частая связка с elvis
|
||||
}
|
||||
```
|
||||
Результат всегда nullable, даже при приведении к non-null типу.
|
||||
|
||||
Сравнение способов:
|
||||
|
||||
| Запись | При несовпадении типа |
|
||||
|---|---|
|
||||
| `x is T` | `false` |
|
||||
| `x as T` | `ClassCastException` |
|
||||
| `x as? T` | `null` |
|
||||
|
||||
## Приведение к nullable
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a: Any? = null
|
||||
println(a as? String) // null
|
||||
// println(a as String) // NPE: null cannot be cast to non-null type
|
||||
println(a as String?) // null — приведение к nullable разрешено
|
||||
}
|
||||
```
|
||||
|
||||
## Числовые преобразования
|
||||
Для чисел `as` не применяется - только явные функции:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val i = 42
|
||||
println(i.toLong()) // 42
|
||||
println(i.toDouble()) // 42.0
|
||||
println(i.toByte()) // 42
|
||||
println(i.toChar()) // *
|
||||
println(3.99.toInt()) // 3 — обрезание, не округление
|
||||
println("42".toInt()) // 42
|
||||
}
|
||||
```
|
||||
Неявных преобразований между числовыми типами нет:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val i: Int = 42
|
||||
// val l: Long = i // ошибка компиляции
|
||||
val l: Long = i.toLong() // так
|
||||
}
|
||||
```
|
||||
|
||||
Потеря данных происходит молча:
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(300.toByte()) // 44 — старшие биты отброшены
|
||||
println(1e20.toInt()) // 2147483647 — обрезано
|
||||
println(Double.NaN.toInt()) // 0
|
||||
}
|
||||
```
|
||||
Подробнее: [Числа и переполнение](Числа%20и%20переполнение.md)
|
||||
|
||||
## Стирание типов и дженерики
|
||||
Параметры типа стираются при компиляции, поэтому проверить их в рантайме нельзя:
|
||||
```kotlin
|
||||
fun check(x: Any) {
|
||||
if (x is List<*>) { // можно: звёздочка вместо типа
|
||||
println("список из ${x.size}")
|
||||
}
|
||||
// if (x is List<String>) // ошибка: cannot check for instance
|
||||
// // of erased type
|
||||
}
|
||||
|
||||
fun main() { check(listOf(1, 2)) } // список из 2
|
||||
```
|
||||
`List<*>` - это star-проекция: «список чего-то неизвестного». Элементы из него читаются как `Any?`.
|
||||
|
||||
Приведение к конкретному дженерику компилируется с предупреждением и небезопасно:
|
||||
```kotlin
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> castList(x: Any): List<T> = x as List<T> // unchecked cast
|
||||
|
||||
fun main() {
|
||||
val nums: List<Int> = castList(listOf("a", "b")) // не упадёт здесь!
|
||||
// println(nums.first() + 1) // ClassCastException только при обращении
|
||||
println(nums.size) // 2 — работает, типы не проверяются
|
||||
}
|
||||
```
|
||||
Это отложенное падение: ошибка проявится далеко от места приведения.
|
||||
|
||||
Обойти стирание можно через `reified` в inline-функциях:
|
||||
```kotlin
|
||||
inline fun <reified T> Any.safeCast(): T? = this as? T
|
||||
|
||||
fun main() {
|
||||
println("text".safeCast<String>()) // text
|
||||
println("text".safeCast<Int>()) // null
|
||||
}
|
||||
```
|
||||
Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## Приведение в иерархии
|
||||
```kotlin
|
||||
open class Animal(val name: String)
|
||||
class Dog(name: String) : Animal(name) {
|
||||
fun bark() = "Гав"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a: Animal = Dog("Рекс") // вверх по иерархии — неявно
|
||||
println(a.name) // Рекс
|
||||
|
||||
val d = a as Dog // вниз — только явно
|
||||
println(d.bark()) // Гав
|
||||
|
||||
val other: Animal = Animal("кто-то")
|
||||
println((other as? Dog)?.bark()) // null — безопасно
|
||||
}
|
||||
```
|
||||
Приведение вверх (к супертипу) всегда безопасно и неявно. Приведение вниз требует `as` или `as?`.
|
||||
|
||||
## Коллекции и вариантность
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strings: List<String> = listOf("a")
|
||||
val anys: List<Any> = strings // ok — List ковариантен по out T
|
||||
println(anys.size) // 1
|
||||
|
||||
val mStrings: MutableList<String> = mutableListOf("a")
|
||||
// val mAnys: MutableList<Any> = mStrings // ошибка — MutableList инвариантен
|
||||
}
|
||||
```
|
||||
`List<out T>` разрешает такое присваивание, `MutableList<T>` - нет, иначе можно было бы положить `Int` в список строк. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
|
||||
|
||||
## Получение типа в рантайме
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
println(x::class) // class kotlin.String
|
||||
println(x::class.simpleName) // String
|
||||
println(x::class.java) // class java.lang.String — Java-класс
|
||||
println(x is CharSequence) // true — проверка по интерфейсу
|
||||
}
|
||||
```
|
||||
Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `x instanceof String` | `x is String` |
|
||||
> | `(String) x` | `x as String` |
|
||||
> | нет безопасного каста | `x as? String` |
|
||||
> | `(int) doubleValue` | `doubleValue.toInt()` |
|
||||
> | `long l = intValue;` неявно | `intValue.toLong()` явно |
|
||||
> | `x.getClass()` | `x::class` или `x::class.java` |
|
||||
> | после `instanceof` нужен каст | умное приведение |
|
||||
> | массивы ковариантны | массивы инвариантны |
|
||||
>
|
||||
> Главные приобретения: `as?` вместо пары `instanceof` + каст, и запрет неявных числовых преобразований, из-за которых в Java `long total = intA * intB` тихо переполняется.
|
||||
>
|
||||
> Ковариантность массивов в Java - известная дыра в типизации:
|
||||
> ```java
|
||||
> Object[] objs = new String[1];
|
||||
> objs[0] = 42; // компилируется, падает в рантайме
|
||||
> ```
|
||||
> В Kotlin `Array<T>` инвариантен, и такой код не соберётся.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`as` на null даёт NPE, а не ClassCastException:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a: Any? = null
|
||||
> // println(a as String) // NullPointerException
|
||||
> println(a as String?) // null — ok
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`as?` маскирует обе причины.** `null` в результате означает либо неверный тип, либо что значение изначально было `null` - различить невозможно:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a: Any? = null
|
||||
> val b: Any = 42
|
||||
> println("${a as? String} ${b as? String}") // null null
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Unchecked cast падает не там, где написан:**
|
||||
> ```kotlin
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> fun main() {
|
||||
> val list = listOf("a") as List<Int> // здесь не падает
|
||||
> println(list.size) // 1 — и здесь тоже
|
||||
> // println(list[0] + 1) // ClassCastException только тут
|
||||
> }
|
||||
> ```
|
||||
> Поэтому `@Suppress("UNCHECKED_CAST")` - всегда повод задуматься.
|
||||
>
|
||||
> **`toInt()` на `Double` обрезает дробную часть, а не округляет.** `3.99.toInt()` даёт `3`. Для округления - `roundToInt()`.
|
||||
>
|
||||
> **`toByte()` и `toShort()` молча теряют старшие биты:** `300.toByte()` возвращает `44`.
|
||||
>
|
||||
> **`is` не проверяет параметры дженерика.** `x is List<String>` не скомпилируется, а `x is List<*>` ничего не говорит об элементах. Проверять придётся вручную:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val x: Any = listOf(1, "a")
|
||||
> val strings = (x as? List<*>)?.filterIsInstance<String>()
|
||||
> println(strings) // [a] — только элементы нужного типа
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`x::class` и `x::class.java` - разные вещи.** Первое - `KClass` Kotlin, второе - `Class` Java. Для `Int` они могут отличаться (`int` против `Integer`).
|
||||
|
||||
Дополнительно: [Умные приведения](../Null-безопасность/Умные%20приведения.md)
|
||||
Далее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||