Compare commits

...

21 Commits

Author SHA1 Message Date
Aklivtsov b62e54bbb0 Merge branch 'master' into AK_Additions 2026-10-20 12:55:54 +03:00
Aklivtsov 6853ada348 Kotlin guide done. (20.10.26) 2026-10-20 12:54:25 +03:00
roma-dxunvrs 4d0e404259 add vliw, empty risc and m68k
deploy / Pull and Restart (push) Successful in 16s
2026-10-15 16:39:49 +03:00
Aklivtsov da3ad9dd1a Added kotlin guide. (WIP) (15.10.26) 2026-10-15 00:24:21 +03:00
roma-dxunvrs 005017595b 10.10
deploy / Pull and Restart (push) Successful in 16s
2026-10-10 19:41:44 +03:00
roma-dxunvrs 23a9e06d53 update index
deploy / Pull and Restart (push) Successful in 16s
2026-10-08 21:27:47 +03:00
roma-dxunvrs 8c95d15f6c add deploy guide 2026-10-08 21:00:18 +03:00
roma-dxunvrs 61e293f81b update zsh guide
deploy / Pull and Restart (push) Successful in 17s
2026-10-05 23:57:00 +03:00
roma-dxunvrs 75e45eb930 web add deploy guide
deploy / Pull and Restart (push) Successful in 17s
2026-10-05 22:49:35 +03:00
roma-dxunvrs 12a473f46e web update
deploy / Pull and Restart (push) Successful in 17s
2026-10-04 19:57:51 +03:00
roma-dxunvrs 11432e782f csa update
deploy / Pull and Restart (push) Successful in 16s
2026-10-03 13:42:57 +03:00
roma-dxunvrs fec7f50b3e 02.10.26
deploy / Pull and Restart (push) Successful in 16s
2026-10-02 22:58:53 +03:00
roma-dxunvrs 4886625dac fix
deploy / Pull and Restart (push) Successful in 18s
2026-10-01 20:57:53 +03:00
roma-dxunvrs 977e016822 other: important
deploy / Pull and Restart (push) Successful in 17s
2026-10-01 20:55:51 +03:00
roma-dxunvrs 3764f7dafa js: add table of contents
deploy / Pull and Restart (push) Successful in 17s
2026-10-01 11:32:48 +03:00
roma-dxunvrs ecd928d72d other: add zsh guide
deploy / Pull and Restart (push) Successful in 16s
2026-09-30 20:41:07 +03:00
roma-dxunvrs c0b8ac0043 fix PL, add x86 in CS
deploy / Pull and Restart (push) Successful in 16s
2026-09-29 21:49:51 +03:00
roma 88b307c311 Merge pull request 'ProgLangs added. 29.09.26' (#4) from AK_Additions into master
deploy / Pull and Restart (push) Successful in 17s
Reviewed-on: #4
2026-09-29 21:29:11 +03:00
AK dc82207723 ProgLangs added. 29.09.26 2026-09-29 21:22:14 +03:00
roma-dxunvrs 8ebd7b0b4f ci: Add 2 AM daily schedule to deploy workflow
deploy / Pull and Restart (push) Successful in 1m15s
2026-09-28 11:29:36 +03:00
roma-dxunvrs dd3ac17361 docs: Add RKN IP ban workaround to VPN notes 2026-09-28 11:29:01 +03:00
146 changed files with 36684 additions and 25 deletions
+2
View File
@@ -4,6 +4,8 @@ on:
push:
branches:
- master
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+6 -1
View File
@@ -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. Процедурное программирование и ООП - тот же принцип
![](../../Attachments/Машина%20фон%20Неймана.png)
## Особенности
1. Использование двоичного кодирования
- Встречается и троичное, и двоично-десятичное кодирование
2. Программное управление. Команды выполняются последовательно
- Последовательность и порядок сегодня условны
3. Память компьютера однородно хранит данные и программы
- Однородность спорна
4. Ячейки памяти компьютера имеют адреса. Random-Access Memory
- Адрес не сводится к целому числу сегодня
- Память не является пассивным элементом компьютера
5. Возможность условного перехода
- И перехода по иным причинам
## Окружение
### (Микро)процессор
Цифровая схема, которая выполняет операции с внешним источником данных (обычно с памятью или потоком данных)
### (Микро)контроллер
Микросхема, предназначенная для управления электронными устройствами. Типичный микроконтроллер сочетает функции процессора и периферийных устройств, содержит ОЗУ и/или ПЗУ
### Оперативная память (память)
Обычно часть компьютерной памяти, в которой хранится выполняемый машинный код, а также входные, выходные и промежуточные данные
## Control Unit и DataPath
### Control Unit
Компонент центрального процессора (CPU), управляющей работой процессора. CU часто использует двоичный декодер для преобразования закодированных инструкций в управляющие сигналы, координирующие работу остальных блоков
### DataPath
АЛУ, набор регистров и внутренние шины CPU, по которым данные перемещаются между ними
![](../../Attachments/Control%20Unit%20and%20DataPath.png)
## Виды инструкций
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
Относительно просто реализуются в виде аппаратных схем
### Конечные автоматы
![](../../Attachments/FSM.png)
Несколько наборов состояний системы и условия переходов между ними
#### Автомат с магазинной памятью
![](../../Attachments/Автомат%20с%20магазинной%20памятью.png)
Например, парсеры
#### Достоинства
- Таблица состояний
- Верифицируемость переходов
- Простая реализация в схемотехнике и ПО
### Машина Тьюринга
Включает:
- Неограниченную двустороннюю ленту, разделенную на ячейки
- Устройство управления (головка записи-чтения), которое может находиться в конечном числе состояний
Управляющее устройство может:
- Перемещаться по ленте влево и вправо
- Читать и записывать в ячейки символы конечного алфавита
Машина Тьюринга
1. Не может быть реализована на практике (нет бесконечной ленты)
2. Обладает полнотой по Тьюрингу (можно реализовать любой известный алгоритм)
3. Проблема остановки
4. Данные и управление отделены
5. Ориентирована на реализацию
### Random Access Machine
![](../../Attachments/RAM.png)
## Функциональные модели
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
![](../../Attachments/Процессоры%20и%20системы%20команд.png)
## Архитектура процессора
То, как видит компьютер программист. Определена набором команд (язык), местом нахождения операндов (регистры и память) и вычислительными механизмами (кеш, прерывания и тд)
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
![](../../Attachments/CoDesign%20HW-SW.png)
В итоге: сокращение цикла разработки
## Как выбирать инструменты для разработки 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)
+145
View File
@@ -0,0 +1,145 @@
---
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)
@@ -0,0 +1,385 @@
---
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)
@@ -0,0 +1,434 @@
---
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)
@@ -0,0 +1,525 @@
---
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)
@@ -0,0 +1,202 @@
---
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)
@@ -0,0 +1,359 @@
---
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)
@@ -0,0 +1,351 @@
---
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&lt;String&gt;"] --> CA["Producer&lt;Any&gt;"]
end
subgraph contra["in T: контравариантность"]
KA["Consumer&lt;Any&gt;"] --> KS["Consumer&lt;String&gt;"]
end
subgraph inv["T: инвариантность"]
IS["Box&lt;String&gt;"]
IA["Box&lt;Any&gt;"]
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)
@@ -0,0 +1,393 @@
---
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)
@@ -0,0 +1,331 @@
---
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)
@@ -0,0 +1,383 @@
---
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)
+383
View File
@@ -0,0 +1,383 @@
---
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)
@@ -0,0 +1,273 @@
---
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)
@@ -0,0 +1,205 @@
---
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)
@@ -0,0 +1,386 @@
---
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)
@@ -0,0 +1,385 @@
---
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)
@@ -0,0 +1,379 @@
---
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)
@@ -0,0 +1,336 @@
---
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&lt;T&gt;"]
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)
@@ -0,0 +1,403 @@
---
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)
@@ -0,0 +1,370 @@
---
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)
+365
View File
@@ -0,0 +1,365 @@
---
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)
+368
View File
@@ -0,0 +1,368 @@
---
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)
@@ -0,0 +1,323 @@
---
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)
@@ -0,0 +1,282 @@
---
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)
@@ -0,0 +1,234 @@
---
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&lt;out T&gt;"] --> C["Collection&lt;out T&gt;"]
C --> L["List&lt;out T&gt;"]
C --> S["Set&lt;out T&gt;"]
I --> MI["MutableIterable&lt;T&gt;"]
MI --> MC["MutableCollection&lt;T&gt;"]
C --> MC
MC --> ML["MutableList&lt;T&gt;"]
L --> ML
MC --> MS["MutableSet&lt;T&gt;"]
S --> MS
M["Map&lt;K, out V&gt;"] --> MM["MutableMap&lt;K, V&gt;"]
```
Два важных факта из этой схемы:
- `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)
@@ -0,0 +1,424 @@
---
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)
+545
View File
@@ -0,0 +1,545 @@
---
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)
+783
View File
@@ -0,0 +1,783 @@
---
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)
@@ -0,0 +1,618 @@
---
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)
@@ -0,0 +1,395 @@
---
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)
@@ -0,0 +1,417 @@
---
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)
@@ -0,0 +1,404 @@
---
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)
@@ -0,0 +1,529 @@
---
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)
@@ -0,0 +1,344 @@
---
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)
+277
View File
@@ -0,0 +1,277 @@
---
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)
@@ -0,0 +1,525 @@
---
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)
@@ -0,0 +1,516 @@
---
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)
@@ -0,0 +1,478 @@
---
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)
@@ -0,0 +1,306 @@
---
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)
@@ -0,0 +1,284 @@
---
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)
+372
View File
@@ -0,0 +1,372 @@
---
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)
+320
View File
@@ -0,0 +1,320 @@
---
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)
+332
View File
@@ -0,0 +1,332 @@
---
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)
@@ -0,0 +1,326 @@
---
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)
@@ -0,0 +1,290 @@
---
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)
+307
View File
@@ -0,0 +1,307 @@
---
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)
+197
View File
@@ -0,0 +1,197 @@
---
title: ООП
description: Обзор объектной модели Kotlin: что закрыто по умолчанию, чего нет совсем и почему
tags:
- kotlin
- oop
---
Объектная модель Kotlin устроена так же, как в Java - один суперкласс, множество интерфейсов, виртуальные вызовы. Отличия в значениях по умолчанию и в том, что из Java убрано.
## Главное отличие: всё закрыто по умолчанию
```kotlin
class User(val name: String) {
fun greet() = "Привет, $name"
}
// class Admin : User("админ") // ошибка: User закрыт для наследования
```
Класс нельзя унаследовать, пока он не объявлен `open`. Метод нельзя переопределить, пока он не `open`. Это противоположность Java, где `final` нужно писать явно.
```kotlin
open class User(val name: String) {
open fun greet() = "Привет, $name"
fun id() = name.hashCode() // переопределить нельзя
}
class Admin(name: String) : User(name) {
override fun greet() = "Админ $name"
// override fun id() = 0 // ошибка: id не open
}
fun main() {
val u: User = Admin("Иван")
println(u.greet()) // Админ Иван — виртуальный вызов
}
```
Причина - проблема хрупкого базового класса. Автор класса, который не проектировал его под наследование, не может гарантировать, что подкласс не нарушит инварианты. `open` - это явное обещание поддерживать наследование как часть контракта.
## Карта раздела
```mermaid
graph TD
A["Классы и конструкторы"] --> B["Свойства"]
B --> C["Инициализация"]
C --> D["Модификаторы видимости"]
D --> E["Наследование"]
E --> F["Переопределение"]
F --> G["Абстрактные классы"]
G --> H["Интерфейсы"]
H --> I["Делегирование"]
I --> J["Вложенные классы"]
J --> K["Полиморфизм"]
```
| Заметка | О чём |
|---|---|
| [Классы и конструкторы](Классы%20и%20конструкторы.md) | основной и вторичные конструкторы, параметры-свойства |
| [Свойства](Свойства.md) | геттеры, сеттеры, backing field, `lateinit`, делегаты |
| [Инициализация](Инициализация.md) | порядок выполнения `init`, ловушка вызова open-метода |
| [Модификаторы видимости](Модификаторы%20видимости.md) | `public`, `internal`, `protected`, `private` |
| [Наследование](Наследование.md) | `open`, вызов конструктора базового класса |
| [Переопределение](Переопределение.md) | `override`, `final`, `super`, ковариантность результата |
| [Абстрактные классы](Абстрактные%20классы.md) | `abstract` против `open`, выбор с интерфейсом |
| [Интерфейсы](Интерфейсы.md) | реализация по умолчанию, свойства, ромбовидное наследование |
| [Делегирование](Делегирование.md) | `by` для интерфейсов и свойств |
| [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md) | `nested` по умолчанию, `inner` для ссылки на внешний |
| [Полиморфизм](Полиморфизм.md) | подтипы, перегрузка, дженерики |
## Чего в Kotlin нет
| Из Java | Что вместо |
|---|---|
| `static` | [companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md), функции верхнего уровня |
| поля (`field`) в публичном API | [свойства](Свойства.md) с геттером и сеттером |
| checked exceptions | все исключения непроверяемые, см. [Система исключений](../Исключения/Система%20исключений.md) |
| `package-private` (без модификатора) | `internal` - видимость в пределах модуля |
| анонимные классы | [объект-выражение](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) |
| примитивы и обёртки как разные типы | один `Int`, упаковка по необходимости |
| `wait`/`notify` в `Object` | [корутины](../Корутины/Корутины.md) и примитивы JDK |
## Классы на любой вкус
Kotlin предлагает специализированные формы класса вместо одного универсального:
```kotlin
class Service // обычный
data class Point(val x: Int, val y: Int) // носитель данных
enum class Status { OK, FAIL } // фиксированный набор значений
sealed interface Result3 // фиксированный набор типов
object Registry // синглтон
@JvmInline value class UserId(val value: Int) // обёртка без аллокации
abstract class Shape // частичная реализация
interface Named // контракт
annotation class Marker // аннотация
fun interface Handler { fun handle(x: Int) } // SAM-интерфейс
```
Правильный выбор формы снимает большую часть ручной работы: `data class` избавляет от написания `equals`, `sealed` - от `else` в `when`, `object` - от реализации синглтона.
Подробнее о первых пяти: [Базовые типы ООП](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
## Свойства вместо полей
В Kotlin нет публичных полей - только свойства. Снаружи обращение выглядит как к полю, но всегда идёт через методы доступа:
```kotlin
class Rect(val width: Int, val height: Int) {
val area: Int get() = width * height // вычисляется при каждом чтении
var scale: Double = 1.0
set(value) {
require(value > 0) { "масштаб должен быть положительным" }
field = value
}
}
fun main() {
val r = Rect(2, 3)
println(r.area) // 6 — похоже на поле, но это вызов геттера
r.scale = 2.0
println(r.scale) // 2.0
}
```
Практическое следствие: заменить хранимое свойство на вычисляемое можно без изменения кода клиентов. В Java для этого пришлось бы менять `obj.field` на `obj.getField()` везде.
Подробнее: [Свойства](Свойства.md)
## Композиция удобнее наследования
Kotlin делает композицию такой же короткой, как наследование, - через делегирование:
```kotlin
interface Logger2 { fun log(msg: String) }
class ConsoleLogger : Logger2 {
override fun log(msg: String) = println("[LOG] $msg")
}
class Service2(logger: Logger2) : Logger2 by logger // делегирует всё
fun main() {
Service2(ConsoleLogger()).log("работает") // [LOG] работает
}
```
`Service2` реализует `Logger2`, не наследуя реализацию: методы просто перенаправляются. Подробнее: [Делегирование](Делегирование.md)
## Что осталось от Java без изменений
- одиночное наследование классов, множественная реализация интерфейсов
- динамическая диспетчеризация: вызов идёт по фактическому типу объекта
- `Any` в корне иерархии (аналог `Object`)
- стирание типов в дженериках
- модель памяти и сборка мусора JVM
То есть байт-код, который порождает Kotlin, - обычные JVM-классы. Любая Java-библиотека работает с Kotlin-классами, и наоборот. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | класс открыт, `final` закрывает | класс закрыт, `open` открывает |
> | метод виртуален по умолчанию | метод финален по умолчанию |
> | `@Override` опциональна | `override` обязательно |
> | поля + геттеры/сеттеры | свойства |
> | `static` | `companion object` |
> | нет модификатора = package-private | нет модификатора = `public` |
> | нет аналога | `internal` (видимость в модуле) |
> | checked exceptions | их нет |
> | делегирование вручную | `by` |
>
> **Обязательный `override`** закрывает два класса ошибок Java: опечатка в имени метода создаёт новый метод вместо переопределения, а добавление метода в базовый класс может случайно «переопределиться» существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции.
>
> **Отсутствие модификатора означает `public`**, а не package-private. Это ловушка при переходе: Java-разработчик по привычке не пишет модификатор, рассчитывая на видимость в пакете, и получает публичный API.
>
> Сравнить: [ООП в Java](../../Программирование%20на%20Java/ООП/ООП.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **`open` не наследуется транзитивно для метода.** Переопределённый метод остаётся открытым, пока его явно не закроют:
> ```kotlin
> open class A3 { open fun f() = "A" }
> open class B3 : A3() { override fun f() = "B" } // всё ещё open!
> class C3 : B3() { override fun f() = "C" } // можно
>
> open class D3 : A3() { final override fun f() = "D" } // так закрывают
>
> fun main() {
> println(listOf(A3(), B3(), C3(), D3()).joinToString { it.f() }) // A, B, C, D
> }
> ```
>
> **Публичный `val` - часть API навсегда.** Превратить его в `var` можно, а убрать или переименовать - ломающее изменение, потому что клиенты обращаются к нему как к полю.
>
> **`internal` не защищает от Java.** В байт-коде такой член становится `public` с искажённым именем, поэтому Java-код из другого модуля может его вызвать:
> ```kotlin
> class Service3 {
> internal fun secret() = "виден из Java"
> }
>
> fun main() { println(Service3().secret()) }
> ```
>
> **Отсутствие checked exceptions означает, что сигнатура не говорит об ошибках.** Функция может бросить что угодно, и компилятор об этом не предупредит. Способы это компенсировать - в [Result и runCatching](../Исключения/Result%20и%20runCatching.md).
>
> **Наследование от `data class` невозможно**, потому что он не может быть `open`. Иерархии строят на `sealed`-типах или на интерфейсах.
Далее: [Классы и конструкторы](Классы%20и%20конструкторы.md)
@@ -0,0 +1,341 @@
---
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)
+325
View File
@@ -0,0 +1,325 @@
---
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)
+433
View File
@@ -0,0 +1,433 @@
---
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)
@@ -0,0 +1,653 @@
---
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)
@@ -0,0 +1,505 @@
---
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)
@@ -0,0 +1,185 @@
---
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)
@@ -0,0 +1,194 @@
---
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)
@@ -0,0 +1,160 @@
---
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)
@@ -0,0 +1,241 @@
---
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)
@@ -0,0 +1,274 @@
---
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)
@@ -0,0 +1,276 @@
---
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)
@@ -0,0 +1,298 @@
---
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)
@@ -0,0 +1,347 @@
---
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)
@@ -0,0 +1,294 @@
---
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)
@@ -0,0 +1,228 @@
---
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)
@@ -0,0 +1,265 @@
---
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)
@@ -0,0 +1,320 @@
---
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)
@@ -0,0 +1,53 @@
---
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)
@@ -0,0 +1,182 @@
---
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)
@@ -0,0 +1,131 @@
---
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)
@@ -0,0 +1,154 @@
---
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)
@@ -0,0 +1,191 @@
---
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)
@@ -0,0 +1,154 @@
---
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)
@@ -0,0 +1,175 @@
---
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)
@@ -0,0 +1,159 @@
---
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)
@@ -0,0 +1,140 @@
---
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)
@@ -0,0 +1,218 @@
---
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)
@@ -0,0 +1,273 @@
---
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)
@@ -0,0 +1,249 @@
---
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)

Some files were not shown because too many files have changed in this diff Show More