Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b62e54bbb0 | |||
| 6853ada348 | |||
| 4d0e404259 | |||
| da3ad9dd1a | |||
| 005017595b | |||
| 23a9e06d53 | |||
| 8c95d15f6c | |||
| 61e293f81b | |||
| 75e45eb930 | |||
| 12a473f46e | |||
| 11432e782f | |||
| fec7f50b3e | |||
| 4886625dac | |||
| 977e016822 | |||
| 3764f7dafa | |||
| ecd928d72d | |||
| c0b8ac0043 | |||
| 88b307c311 | |||
| dc82207723 | |||
| 8ebd7b0b4f | |||
| dd3ac17361 | |||
| fc45caa657 | |||
| 7637cab7a9 | |||
| 0e6e583e6d | |||
| 6421fd3a48 | |||
| 08e557da92 | |||
| c90a404c40 | |||
| d5adddc4f1 | |||
| 05a67d2998 | |||
| 98eeb949fc | |||
| 3e4e10da10 |
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -22,6 +24,7 @@ jobs:
|
||||
cd /opt/docs/obsidian
|
||||
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S git reset --hard origin/master
|
||||
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S git pull origin master
|
||||
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S git submodule sync
|
||||
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S git submodule update --init --recursive --remote
|
||||
cd ..
|
||||
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S docker compose down
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[submodule "LeterZP"]
|
||||
path = LeterZP
|
||||
url = https://gitea.aklivtsov.tech/LeterZP/Info.git
|
||||
url = ../../LeterZP/Info.git
|
||||
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -7,14 +7,20 @@
|
||||
- Сформировать насмотренность и знание вычислительных механизмов (которые воспроизводятся в разных технологиях).
|
||||
- Устранить элементы "магического мышления" и "карго культа".
|
||||
## Системная инженерия
|
||||
- [[Понятие системы]]
|
||||
- [[Компьютерные системы]]
|
||||
- [Понятие системы](Системная%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
|
||||
- C Language
|
||||
- [[Системные паттерны]]
|
||||
- [Системные паттерны](Системные%20паттерны/Системные%20паттерны.md)
|
||||
## Сети
|
||||
- Компьютерные сети
|
||||
@@ -1,6 +1,6 @@
|
||||
- 32-битная стековая архитектура
|
||||
- Главный принцип - команды не имеют операндов в коде. Все данные неявно берутся с веришны стека, результат кладется обратно в стек.
|
||||
- Небольшая пасхалка на [[Evaluate Reverse Polish Notation|вот эту задачу с литкода]]
|
||||
- Небольшая пасхалка на [вот эту задачу с литкода](../../АиСД/Задачи/LeetCode/Evaluate%20Reverse%20Polish%20Notation.md)
|
||||
## Регистры, флаги и стеки
|
||||
- Имеет два независимых аппаратных стека и два регистра-указателя
|
||||
### Стеки
|
||||
@@ -60,7 +60,7 @@
|
||||
- `>r` - снять элемент с `Data Stack` и положить на `Return Stack`
|
||||
## Подробнее про умножение и деление
|
||||
### `+*`
|
||||
- Как работает умноежение чисел в двоичной системе (`A*S`): смотрим на каждый бит числа A, если бит 1, то прибавляем S к результату, если бит 0, то пропускаем, в конце сдвигаем результат
|
||||
- Как работает умножение чисел в двоичной системе (`A*S`): смотрим на каждый бит числа A, если бит 1, то прибавляем S к результату, если бит 0, то пропускаем, в конце сдвигаем результат
|
||||
- Как использует эта команда данные
|
||||
1. Регистр `A` - множитель
|
||||
2. `S` (второй элемент стека данных) - множимое
|
||||
|
||||
@@ -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]]
|
||||
- [Платформа Wrench](Платформа%20Wrench.md)
|
||||
## Архитектуры
|
||||
- [[acc32]]
|
||||
- [[f32a]]
|
||||
- [x86_64](x86_64.md) (реальная архитектура)
|
||||
- [acc32](acc32.md)
|
||||
- [f32a](f32a.md)
|
||||
- [m68k](m68k.md) (будет)
|
||||
- [risc-iv](risc-iv.md) (будет)
|
||||
- [vliw-iv](vliw-iv.md) (в работе, есть интересные факты про Эльбрус)
|
||||
@@ -63,6 +63,6 @@
|
||||
### Сдвиги
|
||||
- Если сдвинуть числа больше чем на 32, то просто зануление - для сдвига влево
|
||||
- Для сдвигов вправо, если исходное число было положительным - зануление
|
||||
- Для сдвигов вправо, если иходное число было отрицательным - `-1`
|
||||
- Для сдвигов вправо, если исходное число было отрицательным - `-1`
|
||||
### Переходы
|
||||
- В отличие от БЭВМ переходы тут работают иначе. Переходы, которые сравнивают с нулем, учитывают только знак (в бэвм учитывали еще `n+v(mod2)`). Поэтому надо самому реализовывать переходы, которые учитывают специфику доп. кода через `bvz+bltz` например
|
||||
@@ -4,15 +4,15 @@
|
||||
- Принцип расчета - воплощение таблицы значений в виде, позволяющем быстро находить нужные зависимости
|
||||
- Расчет производится путем выставления линейки в требуемое положение и считывания результата
|
||||
|
||||
![[Лог линейка.png]]
|
||||

|
||||
### Линейка и операция деления
|
||||
|
||||
![[Деление на линейке.png]]
|
||||

|
||||
|
||||
Двигаем делимое напротив делителя, в начале получаем частное
|
||||
### Номограмма и операция умножения
|
||||
|
||||
![[Номограмма.png]]
|
||||

|
||||
|
||||
Соединяем две точки, которые хотим перемножить. Пересечение этого отрезка с осью Y - наш результат
|
||||
# Табличное исполнение
|
||||
@@ -26,7 +26,7 @@
|
||||
- Принципы расчета воплощены в мехнизмах устройства
|
||||
- Расчет производится по шагам (алгоритмически), с учетом переноса между разрядами
|
||||
|
||||
![[Арифмометр.png]]
|
||||

|
||||
# Отличие арифмометра от логарифмической линейки
|
||||
1. Рост области определения функций:
|
||||
- для арифмометра: медленный рост сложности устройства, средний рост длительности расчета (зависит от функции)
|
||||
@@ -53,7 +53,7 @@
|
||||
### Архитектура эффективного решения
|
||||
Реконфигурируемый потоковой вычислитель
|
||||
|
||||
![[Потоковый вычислитель.png]]
|
||||

|
||||
|
||||
1. Расчетная формула представляется в виде графа
|
||||
2. Каждой вершине графа сопоставляется человек, способный выполнить соответствующую операцию (только одну)
|
||||
@@ -63,7 +63,7 @@
|
||||
6. Для повышения надежности расчетов используются двоирование (две роты, сравниваем результаты, не совпадение - повтор)
|
||||
### Структура белкового вычислителя
|
||||
|
||||
![[Белковый вычислитель.png]]
|
||||

|
||||
### Достоинства
|
||||
1. Радикальный рост производительности (параллелизм инструкций)
|
||||
- Параллельное исполнение независимых операций
|
||||
@@ -73,11 +73,11 @@
|
||||
- Минимальная вариативность процесса
|
||||
3. Возможность реконфигурации вычислителя под новую задачу
|
||||
|
||||
![[Конвейризация расчетов.png]]
|
||||

|
||||
|
||||
Цвета - 3 партии входных данных. Цифры - на каком такте будут на данном этапе вычисления
|
||||
### Практика
|
||||
На практике все перечисленные механизмы встречаются в современных [[Компьютерные системы|компьютерных системах]], в том числе:
|
||||
На практике все перечисленные механизмы встречаются в современных [компьютерных системах](../Системная%20инженерия/Компьютерные%20системы.md), в том числе:
|
||||
- В процессорах общего назначения
|
||||
- В специализированных процессорах
|
||||
- В распределенных системах
|
||||
@@ -87,7 +87,7 @@
|
||||
- вертикальные нити - натянуты равномерно
|
||||
- горизонтальные нити разных цветов, дисбаланс петель с одной из сторон формирует узор
|
||||
|
||||
![[Жаккард.png]]
|
||||

|
||||
### Проблема создания станка
|
||||
Плетение полотна:
|
||||
1. Нити разделяются на верхние и нижние по схеме узора
|
||||
@@ -98,7 +98,7 @@
|
||||
- Разделение на верхнюю и нижнюю нить нерегулярно
|
||||
- "Гребенка" не работает
|
||||
|
||||
![[Обычный ткацкий станок.png]]
|
||||

|
||||
### Жаккардовый ткацкий станок
|
||||
Считается первой программно-управляемой машиной
|
||||
Элементы решения:
|
||||
@@ -107,17 +107,17 @@
|
||||
- Сильное нажатие и поворот - освободить нить
|
||||
- Как автоматизировать регулярное нажатие на кнопочки?
|
||||
|
||||
![[Идея крючка 1.png]]
|
||||

|
||||
|
||||
Идея крючка
|
||||
|
||||
![[Идея крючка 2.png]]
|
||||

|
||||
### Программное обеспечение
|
||||
Элементы решения:
|
||||
- Пластина с отверстиями позволяет разом "нажать" нужные крючки (перфокарта)
|
||||
- Лента из пластин - программа узора
|
||||
|
||||
![[Жаккард программное решение.png]]
|
||||

|
||||
|
||||
Структура шага работы станка:
|
||||
1. Нажатие пластиной на крючки (разделение верхней и нижней)
|
||||
@@ -151,4 +151,4 @@
|
||||
- Линейный рост скорости поиска от количества городов (до 8 штук)
|
||||
- При росте вычислительной сложности по экспоненте
|
||||
|
||||
Как развитие и скачок произошел переход к использованию электричества в сигналах и состояниях. ([[Цифровая схемотехника]])
|
||||
Как развитие и скачок произошел переход к использованию электричества в сигналах и состояниях. ([Цифровая схемотехника](../Цифровая%20схемотехника/Цифровая%20схемотехника.md))
|
||||
@@ -0,0 +1,61 @@
|
||||
1. Развитие [Машины Тьюринга](Модель%20вычислений.md#Машина%20Тьюринга). Отличия:
|
||||
- Лента заменена на Random-Access Memory (RAM)
|
||||
- Инструкции и данные объединены
|
||||
2. Призвана быть максимально простой в реализации и производстве
|
||||
3. Процедурное программирование и ООП - тот же принцип
|
||||
|
||||

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

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

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

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

|
||||
## Функциональные модели
|
||||
Functional models
|
||||
Представление вычислительного процесса как совокупности символов и правил их преобразований
|
||||
1. Арифметика
|
||||
2. Лямбда исчисление (полнота по Тьюрингу)
|
||||
3. Combinatory logic
|
||||
4. General recursive functions
|
||||
5. Abstract rewriting systems
|
||||
Программа/данные - математический объект
|
||||
Возможна Тьюринг неполнота
|
||||
### Лямбда исчисление
|
||||
#### 4 вида выражений
|
||||
1. Переменная: $x, y\ \text{и} \ z$
|
||||
2. Константа: $a,b,c$
|
||||
3. Коминация (вызов): $s\ t$
|
||||
4. Абстракция: $\lambda x.s$
|
||||
#### 3 вида редукций/подстановки
|
||||
1. Переименование переменных: $\lambda u.u\ v \xrightarrow{\alpha} \lambda w.w\ v$
|
||||
2. Применение функции (подстановка аргумента): $(\lambda x.s)\ t \xrightarrow{\beta} s[t/x]$
|
||||
3. Сокращение аргумента: $\lambda u.v\ u \xrightarrow{\eta} v$
|
||||
## Параллельные модели
|
||||
Системы, включающие несколько взаимодействующих процессов
|
||||
- Kahn process networks - однонаправленные каналы с неограниченными буферами (+- как в гошке)
|
||||
- Actor model - отправка сообщений в "почтовые ящики" (как в Erlang)
|
||||
- Discrete Event - события в заданные моменты времени (цифровые схемы)
|
||||
- Multi-Thread Model - общая память (как в джаве)
|
||||
- Synchronous Data Flow - синхронизированные потоки данных
|
||||
## Близкие понятия
|
||||
### Парадигмы программирования
|
||||
Способ классификации языков программирования на основе их функций. Языки могут относиться к нескольким парадигмам
|
||||
### Стиль программирования
|
||||
Это набор правил или рекомендации, используемых при написании исходного кода компьютерной программы
|
||||
## Применение на практике
|
||||
1. Computer Science. Формальные модели
|
||||
2. Дизайн ЯПов
|
||||
3. Ограничение творческого порыва, контроль сложности
|
||||
4. Модель-ориентированная инженерия
|
||||
## Model-Driven Engineering
|
||||
1. Разработка через моделирование предметной области
|
||||
2. Автоматическая генерация кода из моделей
|
||||
3. Повышение уровня абстракции при разработке
|
||||
4. Сокращение разрыва между требованиями и реализацией
|
||||
5. Улучшение коммуникации между заинтересованными сторонами
|
||||
6. Снижение сложности разработки масштабных систем
|
||||
@@ -0,0 +1,41 @@
|
||||
## Реле -> Программируемые логические контроллеры
|
||||
### Проблемы автоматизации релейными схемами
|
||||
- Плохо масштабируется
|
||||
- Сложная настройка и поддержка
|
||||
- Ненадежность сложных схем
|
||||
- Проблема ведения документации
|
||||
### ПЛК
|
||||
- Специальная разновидность электронной вычислительной машины
|
||||
- Особенности
|
||||
1. [Управляющие системы](../Системная%20инженерия/Управляющие%20системы.md)
|
||||
2. Законченные устройства для встроенного использования
|
||||
3. Специализация ввода-вывода, модульность
|
||||
4. Выделение инструментальной составляющей
|
||||
5. Эксплуатация в тяжелых условиях
|
||||
### Типовое устройство ПЛК
|
||||
|
||||

|
||||
|
||||
- Существуют и полностью программные реализации
|
||||
- Отделение инструментальной составляющей
|
||||
P.S. оптическая изоляция нужна, чтобы не сгорел сам ПЛК, а лишь модули ввода/вывода (на входе - защита от большого напряжения, на выходе - если модуль вывода выйдет из строя, то начнет накапливаться ток - от этого и защита)
|
||||
### Программирование ПЛК (1 штука)
|
||||
- **IEC 61121** - standard for programmable controllers
|
||||
- Языки для АСУТП (автоматизированная система управления технологическим процессом)
|
||||
1. Ladder Diagram (релейно-контактные схемы)
|
||||
2. Function Block Diagram (функциональные блоковые диаграммы)
|
||||
3. Sequential Function Chart (последовательные функциональные диаграммы)
|
||||
4. Structured Text (структурированный текст)
|
||||
- Почему именно они
|
||||
1. Использование не программистами
|
||||
2. Визуальность
|
||||
3. Формальные свойства, реальное время
|
||||
|
||||

|
||||
### Программирование ПЛК (N штук)
|
||||
- **IEC 61499** - Standard for distributed Automation
|
||||
- System of Systems
|
||||
- Разделение управления и данных
|
||||
- Интероперабельность по данным (возможность обмена данными между разными системами, даже разных фирм)
|
||||
|
||||

|
||||
@@ -0,0 +1,64 @@
|
||||
## Инорфмационный процессор
|
||||
- Система (электрическая, механическая или биологическая), которая принимает информацию (последовательность символов или состояний) в одной форме и обрабатывает (преобразует) ее в другую форму
|
||||
- Например: исходный сигнал в статистику, посредством алгоритмического процесса
|
||||
- Включает:
|
||||
1. Ввод
|
||||
2. Процессор
|
||||
3. Хранилище
|
||||
4. Вывод
|
||||
## Универсальный процессор
|
||||
Позволяет решать широкий круг задач, настройка которых производится после производства "по месту" или в run-time
|
||||
### Примечания
|
||||
1. Универсальность != полнота по Тьюрингу
|
||||
2. Теоретическая универсальность != практическая универсальность
|
||||
3. Универсальность противоречит эффективности
|
||||
4. Современный процессор - совокупность программного и аппаратного обеспечения
|
||||
### Свойства
|
||||
1. [Hardware and Software](../Системная%20инженерия/Hardware%20and%20Software.md) (двухэтапное производство)
|
||||
2. Полнота по Тьюрингу
|
||||
3. Отсутствие "серьезных" ограничений на "объем" программы
|
||||
4. Изменяемость ПО
|
||||
## Виды процессоров
|
||||
1. Application-Specific Integrated Circuit
|
||||
2. Coarse-Grained Reconfigurable Arrays
|
||||
3. Field-Programmable Gate Array
|
||||
4. Digital Signal Processor
|
||||
5. Graphics Processing Unit
|
||||
6. Central Processing Unit
|
||||
|
||||

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

|
||||
|
||||
В итоге: сокращение цикла разработки
|
||||
## Как выбирать инструменты для разработки Software
|
||||
1. От доступного Hardware и инструментальных средств
|
||||
2. От доступной команды разработки
|
||||
3. От прикладной задачи
|
||||
- От области (библиотеки)
|
||||
- От модели вычислений (принципов организации вычислительного процесса: параллелизм, конвейризация, оптимизации и тд)
|
||||
@@ -5,11 +5,11 @@
|
||||
3. Кластерные и облачные вычисления
|
||||
## История
|
||||
|
||||
![[История инф систем.png]]
|
||||

|
||||
|
||||
1. Mainframes - большие компьютерные системы, колоды перфокарт. Пакетный режим, долго считает
|
||||
2. Minis - миниЭВМ, компьютеры значительно меньше, эксклюзивный доступ к компьютеру, человек логиниться уже под своим юзером, а не кучу людей на одно вычисление
|
||||
3. Desktops - компьютеры на рабочем месте, почти у всех
|
||||
4. Сервисы - решения прикладных задач, чтобы не делать их локально
|
||||
5. Мобильные вычисления - повсеместно
|
||||
6. Вычисления окружают нас уже повсеместно
|
||||
6. Вычисления окружают нас уже повсеместно
|
||||
@@ -10,8 +10,8 @@
|
||||
- станок
|
||||
- и т.п.
|
||||
## Классификация компьютерных систем
|
||||
1. [[Информационные системы]]
|
||||
2. [[Управляющие системы]]
|
||||
1. [Информационные системы](Информационные%20системы.md)
|
||||
2. [Управляющие системы](Управляющие%20системы.md)
|
||||
|
||||
Про [[Уровни компьютерных систем]]
|
||||
История: [[Докомпьютерные вычисления]]
|
||||
Про [Уровни компьютерных систем](../Системная%20инженерия/Уровни%20компьютерных%20систем.md)
|
||||
История: [Докомпьютерные вычисления](Докомпьютерные%20вычисления.md)
|
||||
@@ -0,0 +1,120 @@
|
||||
Задача производства: превратить документацию в изделие
|
||||
## Монтаж радиоэлектронной аппаратуры (РЭА)
|
||||
### Ключевые тенденции
|
||||
1. Рост уровня интеграции
|
||||
2. Снижение затрат на производство изделия
|
||||
3. Усложнение производственной цепочки
|
||||
### Навесной монтаж
|
||||
Способ монтажа электронных схем, при котором расположенные на изолирующем шасси радиоэлементы соединяются друг с другом проводами или непосредственно выводами
|
||||
#### Достоинства
|
||||
1. Простота производства
|
||||
2. Простота подготовки производства
|
||||
3. Гибкость
|
||||
#### Недостатки
|
||||
1. Сложность автоматизации, дорого в серии
|
||||
2. Надежность
|
||||
3. Плотность ограничена прямотой рук монтажника
|
||||
|
||||
На макетной плате
|
||||
|
||||

|
||||
### Монтаж на печатную плату
|
||||
Печатная плата - пластина из диэлектрика, на поверхности и/или в объеме которой сформированы электропроводящие цепи электронной схемы. Электрическое и механическое соединение компонентов
|
||||
Различают:
|
||||
- одно- и двухслойные платы
|
||||
- многослойные платы
|
||||
## Виды монтажа компонентов
|
||||
- Выводной (в отверстие)
|
||||
- Поверхностный
|
||||
- Встроенный
|
||||
## Поверхностный монтаж (SMD) и Surface-mount technology (SMT)
|
||||
|
||||

|
||||
### Виды SMD компонентов
|
||||
|
||||

|
||||
### Встроенные компоненты
|
||||
|
||||

|
||||
### Достоинства
|
||||
1. Автоматизация
|
||||
2. Плотность размещения компонентов
|
||||
3. Размеры
|
||||
4. Стоимость в серии
|
||||
### Недостатки
|
||||
1. Необходимость подготовки производства
|
||||
2. Удлинение производственной цепочки
|
||||
3. Сложность внесения изменений
|
||||
### Гибкость
|
||||
1. Джампер (by design)
|
||||
2. "Перерезать дорожку"
|
||||
3. "Навесная дорожка"
|
||||
### Производственная цепочка
|
||||
|
||||

|
||||
## Интегральные схемы. Кремниевое произодство
|
||||
|
||||

|
||||
### Кремниевый песок
|
||||
Начало производственной цепочки
|
||||
|
||||

|
||||
### Выращиваем монокристалл кремния (буля
|
||||
|
||||

|
||||
### Производим подложку будущих чипов (Wafer)
|
||||
|
||||

|
||||
### Фотолитография и "море" транзисторов
|
||||
"свет-шаблон-фоторезист"
|
||||
1. На кремниевую подложку наносят материал для рисунка
|
||||
2. Наносится фоторезист
|
||||
3. Экспонирование через фотошаблон
|
||||
4. Удаление отработанного фоторезиста
|
||||
|
||||

|
||||
### Interconnect
|
||||
|
||||

|
||||
### Структура чипа в разрезе
|
||||
|
||||

|
||||
### Извлечение будущих чипов
|
||||
|
||||

|
||||
### Корпус и контакты
|
||||
|
||||

|
||||
### Современный степпер (засветка фотошаблона)
|
||||
|
||||

|
||||
### Надежность
|
||||
Производственный брак неизбежен: не все транзисторы на пластине работают корректно
|
||||
**Чип биннинг** - один и тот же кристалл продается под разными марками: дефектные или нестабильные блоки блокируются программно или лазером
|
||||
1. Разные частоты процессоров -> разные SKU из одного wafer
|
||||
2. Core Single / Core Duo / Core Quad -> одинаковый кристалл, разное число активных ядер
|
||||
3. Intel Pentium / Celeron -> урезанные Core с отключенными кэшем и функциями
|
||||
## Реальный мир
|
||||
### Производство
|
||||
1. Логистика
|
||||
2. Склады
|
||||
3. Специалисты
|
||||
4. Производственная цепочка
|
||||
5. Тестирование
|
||||
6. Упаковка
|
||||
7. Дистрибуция
|
||||
8. Гарантийный ремонт
|
||||
### Эксплуатация
|
||||
1. Амортизация. Выход оборудования из строя
|
||||
2. Необходимость физического доступа
|
||||
3. Особенности среды эксплуатации
|
||||
4. Долгосрочные эффекты
|
||||
### Сложность изменений
|
||||
1. Чем совершеннее технология, тем сложнее внести изменения
|
||||
2. Физический контакт
|
||||
### Варианты замены устаревшей аппаратуры
|
||||
1. Перепроектирование на новой элементной базе
|
||||
2. Модульная организация. Стандартные интерфейсы. Имитация старых систем
|
||||
3. Виртуализация
|
||||
|
||||
Как решаем проблемы аппаратуры - [Hardware and Software](Hardware%20and%20Software.md)
|
||||
@@ -11,10 +11,10 @@
|
||||
## 1. Система как совокупность частей
|
||||
Система состоит из подсистем, которые в свою очередь тоже являются системами, и так далее - иерархия систем
|
||||
|
||||
![[Иерархия систем.png]]
|
||||

|
||||
### Структура системы
|
||||
|
||||
![[Структура системы.png]]
|
||||

|
||||
|
||||
Одна система при разных взглядах. При разных взглядах на систему что-то может и совпадать. В зависимости от нужд от системы - она меняется.
|
||||
Например: дом с точки зрения сантехника, электрика и строителя
|
||||
@@ -55,6 +55,6 @@
|
||||
5. Рассмотрения ее обеспечивающих систем
|
||||
## Фундаментальная проблема систем
|
||||
Множественность точек зрения на систему. Систему может разрушить проблема коммуникации при разработке
|
||||
Формальный способ зафиксировать эти точки зрения и передать их другим - [[Понятие архитектуры|архитектура]]
|
||||
Формальный способ зафиксировать эти точки зрения и передать их другим - [архитектура](Понятие%20архитектуры.md)
|
||||
## Применение в Computer Science
|
||||
- [[Компьютерные системы]]
|
||||
- [Компьютерные системы](Компьютерные%20системы.md)
|
||||
@@ -17,7 +17,7 @@
|
||||
Пример, где один код может быть как ИС, так и УС - видеокодек ffmpeg ( 1 сценарий - конвертация видео на сервере - классика ИС, 2 сценарий - воспроизведение на устройстве пользователя - УС)
|
||||
## Эволюция
|
||||
|
||||
![[Эволюция УС.png]]
|
||||

|
||||
|
||||
1. Информационная УС - объект управления + кабели (тот же мейнфрейм, только без входного пакета данных, данные были всегда одни и те же)
|
||||
2. Встроенные системы - большая интеграция, ну и размеры стали меньше
|
||||
@@ -22,4 +22,7 @@
|
||||
- *Языки* (Languages) - на чем описываем/пишем
|
||||
- *Типы* (Types) - предопределенные платформой абстракции и структуры данных
|
||||
- *Экземпляры* (Instances) - готовые, предопределенные платформой объекты и компоненты
|
||||
- *Шаблоны* (Patterns) - связывают языки, типы и экземпляры, задают повторяемые решения (про [[Системные паттерны]])
|
||||
- *Шаблоны* (Patterns) - связывают языки, типы и экземпляры, задают повторяемые решения (про [Системные паттерны](../Системные%20паттерны/Системные%20паттерны.md))
|
||||
## Принципы формирования уровней (платформ)
|
||||
- **Принцип развития иерархически систем Седова** - закономерности и механизмы, по которым развиваются сложные иерархические системы. Ключевой принцип: ограничение разнообразия на нижележащем уровне для получения разнообразия на вышележащем уровне
|
||||
- **Метасистемный подход** - концепция, предложенная Валентином Турчиным, описывающая процесс перехода системы на новый уровень организации, становясь метасистемой. В этом процессе несколько систем интегрируются в одну более сложную, обладающую новыми свойствами
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
## Кодирование
|
||||
### Двоичное кодирование
|
||||
|
||||

|
||||
|
||||
#### Достоинства
|
||||
1. Надежно и помехоустойчиво
|
||||
2. Простая арифметика
|
||||
3. Диапазоны и точность наращиваются разрядностью (слева и справа соотвественно)
|
||||
4. Погрешности "by design", а не "by implementation"
|
||||
#### Недостатки
|
||||
1. Нечитаемое представление
|
||||
2. Простые десятичные дроби записываются в виде бесконечных двоичных дробей
|
||||
3. Дискретное кодирование сигналов (точность)
|
||||
## Машинное слово. Представления данных
|
||||
- Машинное слово - единица данных, естественная для обработки вычислителем
|
||||
- Примеры: сложение, пересылка и т.п.
|
||||
- Целые числа
|
||||
- Позиционные системы счисления
|
||||
- Дополнительный код
|
||||
- Big & Little endian
|
||||
- Дроби
|
||||
- С фиксированной точкой
|
||||
- С плавающей точкой
|
||||
- Перечисления
|
||||
- Двоично-десятичное кодирование
|
||||
- Символы (старые кодировки)
|
||||
- Тегированные данные
|
||||
|
||||

|
||||
|
||||
### Классификация типов по версии IBM
|
||||
|
||||

|
||||
## Код Грея и BCD
|
||||
### Код Грея
|
||||
Две "соседние" кодовые комбинации различаются только в одном двоичном разряде
|
||||
### BCD
|
||||
Форма записи рациональных чисел, когда каждый десятичный разряд числа записывается в виде его четырехбитного двоичного кода
|
||||
|
||||

|
||||
### Base64 и Base58
|
||||
### Base64
|
||||
Стандарт кодирования двоичных данных при помощи только 64 символов ASCII. Алфавит кодирования содержит латинские символы A-Z, a-z, цифры 0-9 (всего 62 знака) и 2 дополнительных символа
|
||||
|
||||

|
||||
### Base58
|
||||
Модификация Base64 для избежания неоднозначных символов (0, I, O, |). В конце сообщения - хешсумма (checksum)
|
||||
@@ -2,4 +2,5 @@
|
||||
- Сторожевые таймеры - микроконтроллеры, OC/гипервизоры, оркестраторы
|
||||
- Кеши и когерентность - CPU, OC (page cache), диски, CDN, браузер
|
||||
- Прерывания и события - аппартные IRQ, сигналы OC, event loop, message queue
|
||||
- Системы команд - ISA процессора, байткод виртуальных машин ([[JVM]], CLR), API/протоколы (свой Language на каждом уровне), DSL
|
||||
- Системы команд - ISA процессора, байткод виртуальных машин ([JVM](../../Программирование%20на%20Java/JVM/JVM.md), CLR), API/протоколы (свой Language на каждом уровне), DSL
|
||||
- [Представление данных](Представление%20данных.md)
|
||||
@@ -0,0 +1,9 @@
|
||||
## Цифровая схемотехника. Булев базис
|
||||
### Элементы цифровой схемотехники
|
||||
1. Двоичная логика
|
||||
2. Полный набор булевых функций
|
||||
- И, ИЛИ, НЕ
|
||||
- Штрих Шеффера ($x|y=\overline{xy}$)
|
||||
- Стрелка Пирса ($x \downarrow y = \overline{x \lor y}$)
|
||||
3. [Комбинационные схемы](../Комбинационные%20схемы/Комбинационные%20схемы.md)
|
||||
4. Триггер - хранение состояния
|
||||
@@ -13,13 +13,13 @@
|
||||
- `( )` - нормально неактивный актуатор
|
||||
- `(\)` - нормально активный актуатор
|
||||
|
||||
![[Реле 1.png]]
|
||||

|
||||
## Управление двигателем
|
||||
Классическая схема включения электродвигателя с аварийной остановкой
|
||||
|
||||
![[Реле двигатель.png]]
|
||||

|
||||
|
||||
![[Реле двигатель 2.png]]
|
||||

|
||||
## Практика применения реле
|
||||
1. Когда-то использовали, чтобы делать компьютеры
|
||||
2. Непосредственное применение, силовые шкафы управления. Высокий ток, неблагоприятная среда эксплуатации
|
||||
@@ -0,0 +1,28 @@
|
||||
### Комбинационная схема
|
||||
Схема, составленная из набора логических элементов, в совокупности реализующая заданную таблицу истинности
|
||||
### Простейшие элементы комбинационных схем
|
||||
|
||||

|
||||
### КС: Возможности
|
||||
Реализация функций - отображение множества A на множество B
|
||||
A и B конечны (представимы конечной последовательностью бит) -> возможно построить комбинационную схему
|
||||
### КС: Подход к построению
|
||||
1. Через таблицу истинности (черный ящик)
|
||||
2. Через алгоритмизацию (белый ящик, этапность/структура расчета) - логарифмическая линейка и арифмометр
|
||||
#### Через таблицу истинности
|
||||
1. Формирование таблицы истинности
|
||||
2. Запись таблицы истинности в каноническую форму (ДНФ и КНФ), отображаемую в комбинационную схему
|
||||
3. Минимизация булевой функции (сокращаем размер схемы)
|
||||
Пример: шифратор (encoder, 4-to-2)
|
||||
|
||||

|
||||
#### Через алгоритмизацию
|
||||
Схема строится на основе понимания функции, взаимосвязи входа и выхода. Творческий процесс. Схема может быть каскадной
|
||||
|
||||

|
||||
### Свойства комбинационных схем
|
||||
1. Возможность **установления стабильного состояния** при корректном входе (нет циклов)
|
||||
2. **Задержка** установления состояния после изменения входных значений
|
||||
3. **Накопление ошибки** в физическом процессе, что может привести к ошибке на логическом уровне (буферы)
|
||||
4. **Параллелизм уровня бит** (узлы с параллельным включением работают параллельно)
|
||||
5. **Уровень линии**, не сигнал (как и в реле)
|
||||
@@ -0,0 +1,5 @@
|
||||
## Условный оператор. Мультиплексор
|
||||
Схемотехника не может не работать. Уровень не может не быть
|
||||
На уровне комбинационных схем без состояния: спекулятивные вычисления и выбор нужного результата (мультиплексор является классом [комбинационных схем](Комбинационные%20схемы.md))
|
||||
|
||||

|
||||
@@ -1,7 +1,7 @@
|
||||
Асинхронный триггер, который сохраняет свое предыдущее состояние при неактивном состоянии обоих входов и изменяет свой состояние при подаче на один из его входов активного уровня
|
||||
|
||||
![[RS-триггер.png]]
|
||||

|
||||
|
||||
Реализация на [[Электрическое реле|реле]]:
|
||||
Реализация на [реле](../Базовые%20элементы/Электрическое%20реле.md):
|
||||
|
||||
![[RS-триггер на реле.png]]
|
||||

|
||||
@@ -0,0 +1,40 @@
|
||||
### 1. Конвейрное исполнение
|
||||
- Количество стадий - количество паралельных задач
|
||||
- Больше "разбиений" - ниже задержка схемы, выше частота
|
||||
Пример: сравнение 4 чисел:
|
||||
|
||||

|
||||
### 2. Spatial and Temporal Computation
|
||||
- Сложение 16-битных чисел:
|
||||
- Сумматор на 16 бит и 1 такт
|
||||
- Сумматор на 8 бит и 2 такта + система управления
|
||||
|
||||

|
||||
|
||||
Сумматор на 8 бит и 2 така + система управления
|
||||
|
||||

|
||||
### 3. Синхронная схемотехника
|
||||
- Меньше гонок, проще синхронизация
|
||||
- Частота по самой медленной комбинационной схеме
|
||||
- Дискредитация входных сигналов по времени
|
||||
- Синхронность - в заданном диапазоне
|
||||
|
||||

|
||||
### 4. Программное управление
|
||||
## Пример схемы с состоянием
|
||||
|
||||

|
||||
|
||||
### Реализация схемы на Verilog
|
||||
|
||||

|
||||
### Схема детальная с состоянием
|
||||
|
||||

|
||||
|
||||
- Так как память используется только для чтения, она выродилась в комбинационную схему
|
||||
- Смысл цветов линий неизвестен...
|
||||
### Временная диаграмма схемы с состоянием
|
||||
|
||||

|
||||
@@ -0,0 +1,15 @@
|
||||
## Состояние в комбинационной схеме
|
||||
Триггер - класс электронных устройств, обладающих способностью длительно находиться в одном из двух устойчивых состояний и чередовать их под воздействием внешних сигналов
|
||||
Триггер позволяет:
|
||||
1. Зафиксировать состояние линии на длительное время
|
||||
2. Ограничить распространение сигнала по схеме
|
||||
- Разорвать цикл
|
||||
- Сократить задержку в комбинационной схеме
|
||||
### Виды триггеров
|
||||
1. Условие изменения состояния
|
||||
- По фронту
|
||||
- По уровню
|
||||
2. Интерфейс
|
||||
- D-триггер - запоминает состояние входа и выдает его на выход
|
||||
- [RS-триггер](RS-триггер.md)
|
||||
- И другие
|
||||
@@ -1,8 +1,15 @@
|
||||
## Коммутация и базовые элементы
|
||||
- [[Базовая логика]]
|
||||
- [[Логические вентили]]
|
||||
- [[Электрическое реле]]
|
||||
## Память и состояние
|
||||
- [[RS-триггер]]
|
||||
|
||||
## Комбинационные схемы
|
||||
- [Базовая логика](Базовые%20элементы/Базовая%20логика.md)
|
||||
- [Логические вентили](Базовые%20элементы/Логические%20вентили.md)
|
||||
- [Электрическое реле](Базовые%20элементы/Электрическое%20реле.md)
|
||||
## Комбинационные схемы
|
||||
- [Комбинационные схемы](Комбинационные%20схемы/Комбинационные%20схемы.md)
|
||||
- [Мультиплексор](Комбинационные%20схемы/Мультиплексор.md)
|
||||
## Схемы с памятью
|
||||
- [Многотактовые схемы](Схемы%20с%20памятью/Многотактовые%20схемы.md)
|
||||
- [Триггеры](Схемы%20с%20памятью/Триггеры.md)
|
||||
## Ключевые отличия схемотехники от программирования
|
||||
1. Все процессы между регистрами всегда происходят параллельно. Не читайте код как алгоритм, рисуйте схему
|
||||
2. Передача сигнала - физический аналоговый процесс. Есть питание и контакт - есть передача. Сигнал не может не идти
|
||||
3. Нет понятия "система остановилась". Она всегда работает, если есть питание
|
||||
4. Таблица истинности неполна - результат будет слуйчайным, но будет (и возможно воспроизводимым)
|
||||
@@ -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<String>"] --> CA["Producer<Any>"]
|
||||
end
|
||||
subgraph contra["in T: контравариантность"]
|
||||
KA["Consumer<Any>"] --> KS["Consumer<String>"]
|
||||
end
|
||||
subgraph inv["T: инвариантность"]
|
||||
IS["Box<String>"]
|
||||
IA["Box<Any>"]
|
||||
end
|
||||
```
|
||||
|
||||
## Функциональные типы
|
||||
Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val produceString: () -> String = { "текст" }
|
||||
val produceAny: () -> Any = produceString // возврат ковариантен
|
||||
println(produceAny()) // текст
|
||||
|
||||
val acceptAny: (Any) -> Unit = { println("принял $it") }
|
||||
val acceptString: (String) -> Unit = acceptAny // параметр контравариантен
|
||||
acceptString("строка") // принял строка
|
||||
}
|
||||
```
|
||||
Тип `(P) -> R` объявлен как `Function1<in P, out R>`: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая.
|
||||
|
||||
Практическое следствие для API:
|
||||
```kotlin
|
||||
class EventBus {
|
||||
private val handlers = mutableListOf<(Any) -> Unit>()
|
||||
|
||||
fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) }
|
||||
fun publish(event: Any) = handlers.forEach { it(event) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val bus = EventBus()
|
||||
bus.subscribe { println("получено: $it") }
|
||||
bus.publish("событие") // получено: событие
|
||||
bus.publish(42) // получено: 42
|
||||
}
|
||||
```
|
||||
|
||||
## Когда помечать свой класс
|
||||
Правило простое: если параметр типа используется **только** на выходе - ставьте `out`, только на входе - `in`. Компилятор проверит и подскажет.
|
||||
|
||||
```kotlin
|
||||
interface Reader<out T> {
|
||||
fun read(): T
|
||||
fun readAll(count: Int): List<T> // List<T> — тоже выход, ок
|
||||
}
|
||||
|
||||
interface Writer<in T> {
|
||||
fun write(value: T)
|
||||
fun writeAll(values: List<T>) // List<T> на входе, ок
|
||||
}
|
||||
|
||||
interface Storage<T> : Reader<T>, Writer<T> // и то и другое — инвариантен
|
||||
|
||||
class MemoryStorage<T>(private var value: T) : Storage<T> {
|
||||
override fun read(): T = value
|
||||
override fun readAll(count: Int) = List(count) { value }
|
||||
override fun write(value: T) { this.value = value }
|
||||
override fun writeAll(values: List<T>) { values.lastOrNull()?.let { value = it } }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val storage = MemoryStorage("начальное")
|
||||
val reader: Reader<Any> = storage // ковариантность работает
|
||||
storage.write("новое")
|
||||
println(reader.read()) // новое
|
||||
println(reader.readAll(2)) // [новое, новое]
|
||||
}
|
||||
```
|
||||
Разделение на `Reader` и `Writer` - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный.
|
||||
|
||||
## Nothing и вариантность
|
||||
`Nothing` - подтип всех типов, поэтому `List<Nothing>` подходит под любой `List<T>`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val empty: List<Nothing> = emptyList()
|
||||
val ints: List<Int> = empty // работает благодаря out
|
||||
val strings: List<String> = empty
|
||||
println("${ints.size} ${strings.size}") // 0 0
|
||||
}
|
||||
```
|
||||
Именно поэтому `emptyList()` один на все типы - это синглтон `EmptyList : List<Nothing>`. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
Тот же приём в sealed-иерархиях:
|
||||
```kotlin
|
||||
sealed interface Outcome<out T> {
|
||||
data class Ok<T>(val value: T) : Outcome<T>
|
||||
data object Empty : Outcome<Nothing>
|
||||
}
|
||||
|
||||
fun describe4(o: Outcome<Int>): String = when (o) {
|
||||
is Outcome.Ok -> "значение ${o.value}"
|
||||
Outcome.Empty -> "пусто"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe4(Outcome.Ok(1))) // значение 1
|
||||
println(describe4(Outcome.Empty)) // пусто
|
||||
}
|
||||
```
|
||||
`Empty` может быть `object`, а не обобщённым классом, только потому что `Outcome` ковариантен. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Ограничение out: нельзя принимать T
|
||||
Иногда ковариантный класс всё-таки должен принять `T`. Для этого параметр объявляют с более широкой границей:
|
||||
```kotlin
|
||||
class ImmutableBox<out T>(private val value: T) {
|
||||
fun get(): T = value
|
||||
|
||||
fun <R> withDefault(default: R): R where R : Any = default
|
||||
|
||||
fun <U> plus(other: U): List<Any?> = listOf(value, other)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val box: ImmutableBox<String> = ImmutableBox("a")
|
||||
val any: ImmutableBox<Any> = box
|
||||
println(any.get()) // a
|
||||
println(box.plus(1)) // [a, 1]
|
||||
}
|
||||
```
|
||||
В stdlib этот приём виден в `List.plus`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strings: List<String> = listOf("a")
|
||||
val mixed: List<Any> = strings + 1 // plus объявлен как <T> List<T>.plus(element: T)
|
||||
println(mixed) // [a, 1]
|
||||
}
|
||||
```
|
||||
Тип `T` здесь выводится как `Any`, а не как `String`, поэтому ковариантность не нарушается.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `List<? extends T>` в каждой сигнатуре | `List<out E>` один раз в объявлении |
|
||||
> | `List<? super T>` | `MutableList<in E>` или `in` в объявлении |
|
||||
> | `Collection<?>` | `Collection<*>` |
|
||||
> | нет declaration-site variance | `out`/`in` при объявлении класса |
|
||||
> | PECS - правило для программиста | компилятор проверяет сам |
|
||||
>
|
||||
> **Главное отличие - где объявляется вариантность.** Java знает только use-site variance: интерфейс `List<E>` инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard:
|
||||
> ```java
|
||||
> void printAll(Collection<? extends Object> items) { ... }
|
||||
> double sum(List<? extends Number> numbers) { ... }
|
||||
> void addAll(List<? super Integer> dest, List<? extends Integer> src) { ... }
|
||||
> ```
|
||||
> В Kotlin `List<out E>` объявлен один раз в stdlib, и дальше всё работает само:
|
||||
> ```kotlin
|
||||
> fun sum3(numbers: List<Number>) = numbers.sumOf { it.toDouble() }
|
||||
>
|
||||
> fun main() {
|
||||
> println(sum3(listOf(1, 2, 3))) // 6.0 — List<Int> подошёл
|
||||
> }
|
||||
> ```
|
||||
> Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это [проекции типов](Проекции%20типов.md), но нужны они редко.
|
||||
>
|
||||
> **PECS из правила превратился в проверку компилятора.** В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс `out`, и компилятор откажется принять `T` в параметр: ошибка возникает при объявлении, а не у пользователя API.
|
||||
>
|
||||
> **`Comparator` в Kotlin контравариантен автоматически.** В Java `Comparator<T>` инвариантен, и методы вроде `Collections.sort` объявляют `Comparator<? super T>`. В Kotlin stdlib `Comparator` уже `in T`, поэтому `sortedWith(comparator)` принимает компаратор по любому супертипу без дополнительных обозначений.
|
||||
>
|
||||
> **Разделение `List`/`MutableList` делает `out` возможным.** В Java `List` содержит `add`, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы в `MutableList<E>` (инвариантный), и `List<out E>` стал безопасным по построению.
|
||||
>
|
||||
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`out` запрещает `T` в параметрах - включая неочевидные места:**
|
||||
> ```kotlin
|
||||
> class Holder<out T>(private val value: T) {
|
||||
> fun get(): T = value
|
||||
> // fun contains(other: T): Boolean = value == other // ошибка
|
||||
> fun contains(other: Any?): Boolean = value == other // так можно
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Holder("a").contains("a")) // true
|
||||
> }
|
||||
> ```
|
||||
> Расширение границы до `Any?` - стандартный обход, но оно теряет проверку типов: `contains(42)` тоже скомпилируется.
|
||||
>
|
||||
> **`private` функции могут нарушать вариантность:**
|
||||
> ```kotlin
|
||||
> class Mutable<out T>(private var value: T) {
|
||||
> fun get(): T = value
|
||||
> private fun set(v: T) { value = v } // private — проверка не применяется
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Mutable("a").get()) } // a
|
||||
> ```
|
||||
> Компилятор разрешает `T` на входе в `private` членах, потому что снаружи они недоступны. Но `private var value: T` - всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным.
|
||||
>
|
||||
> **`MutableList` инвариантен, и это не ошибка:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings = mutableListOf("a")
|
||||
> // val anys: MutableList<Any> = strings // не скомпилируется — и правильно
|
||||
> val readOnly: List<Any> = strings // а так можно
|
||||
> println(readOnly.size) // 1
|
||||
> }
|
||||
> ```
|
||||
> Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте `List`, а не `MutableList`.
|
||||
>
|
||||
> **Массивы инвариантны в Kotlin и ковариантны в Java:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings = arrayOf("a", "b")
|
||||
> // val anys: Array<Any> = strings // ошибка компиляции
|
||||
> println(strings.size) // 2
|
||||
> }
|
||||
> ```
|
||||
> В Java `String[]` является `Object[]`, поэтому `Object[] o = new String[1]; o[0] = 1;` компилируется и бросает `ArrayStoreException` в рантайме. Kotlin закрыл эту дыру ценой того, что `Array<Any>` не принимает `Array<String>`. Для чтения используйте `Array<out T>`.
|
||||
>
|
||||
> **Вариантность не распространяется на вложенные параметры автоматически:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val nested: List<List<String>> = listOf(listOf("a"))
|
||||
> val widened: List<List<Any>> = nested // работает: оба List ковариантны
|
||||
> println(widened.size) // 1
|
||||
>
|
||||
> val maps: List<MutableList<String>> = listOf(mutableListOf("a"))
|
||||
> // val wide: List<MutableList<Any>> = maps // не работает: MutableList инвариантен
|
||||
> println(maps.size) // 1
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`in` и `out` легко перепутать направление.** Мнемоника: `out` - значение **выходит** из объекта (producer, можно только читать), `in` - значение **входит** в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна.
|
||||
>
|
||||
> **`Map<K, out V>` ковариантен только по значению:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val m: Map<String, Int> = mapOf("a" to 1)
|
||||
> val widened: Map<String, Any> = m // ок — V ковариантен
|
||||
> // val keys: Map<Any, Int> = m // ошибка — K инвариантен
|
||||
> println(widened.size) // 1
|
||||
> }
|
||||
> ```
|
||||
> Ключ инвариантен, потому что он используется на входе в `get(key)`.
|
||||
>
|
||||
> **Ковариантный класс с `var`-полем типа `T` объявить нельзя** - сеттер принимает `T`. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий.
|
||||
|
||||
Далее: [Проекции типов](Проекции%20типов.md)
|
||||
@@ -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)
|
||||
@@ -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<T>"]
|
||||
D -->|"да, по-разному"| G["свой sealed-тип"]
|
||||
```
|
||||
|
||||
Практическое разделение:
|
||||
- **исключение** - для нарушенных предпосылок: неверный аргумент, недопустимое состояние. Это ошибка программиста, её исправляют в коде, а не обрабатывают. Подробнее: [Проверки и контракты](Проверки%20и%20контракты.md)
|
||||
- **`T?`, `Result`, sealed** - для ожидаемых ситуаций: нет записи в БД, сеть недоступна, ввод пользователя некорректен
|
||||
|
||||
## Граница с исключениями
|
||||
`runCatching` удобен на границе: внешний код бросает, ваш возвращает значение.
|
||||
```kotlin
|
||||
class User7(val id: Int, val name: String)
|
||||
|
||||
fun fetchFromDb(id: Int): User7 =
|
||||
if (id == 1) User7(1, "Иван") else throw NoSuchElementException("нет пользователя $id")
|
||||
|
||||
fun findUser(id: Int): User7? = runCatching { fetchFromDb(id) }.getOrNull()
|
||||
|
||||
fun main() {
|
||||
println(findUser(1)?.name) // Иван
|
||||
println(findUser(99)) // null
|
||||
}
|
||||
```
|
||||
Обратное направление - когда API требует исключений, а внутри `Result`:
|
||||
```kotlin
|
||||
fun loadOrThrow(id: Int): String = runCatching {
|
||||
require(id > 0) { "id должен быть положительным" }
|
||||
"данные $id"
|
||||
}.getOrThrow()
|
||||
|
||||
fun main() {
|
||||
println(loadOrThrow(1)) // данные 1
|
||||
try {
|
||||
loadOrThrow(-1)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println(e.message) // id должен быть положительным
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | checked exceptions в сигнатуре | `Result<T>` или sealed-тип |
|
||||
> | `Optional<T>` | `T?` - часть системы типов |
|
||||
> | `Optional.map`/`orElse` | `?.let`/`?:` |
|
||||
> | нет аналога `Result` в JDK | `Result<T>` в stdlib |
|
||||
> | `try`/`catch` для управления | `runCatching` как выражение |
|
||||
>
|
||||
> **`Result` частично возвращает то, что отняло отсутствие checked exceptions.** Сигнатура `fun load(): Result<User>` сообщает, что функция может не справиться, и компилятор не даст использовать значение без разбора. Разница с checked exceptions в том, что это выбор автора функции, а не навязанное правило, и такие значения свободно проходят через `map`, `filter` и другие обобщённые функции.
|
||||
>
|
||||
> **`T?` вместо `Optional<T>`.** `Optional` - обёртка-объект: аллокация, два уровня значений (`null` и пустой `Optional`), неудобство в полях. Nullable-тип в Kotlin существует только при компиляции, а в байт-коде это обычная ссылка - накладных расходов нет:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val name: String? = null
|
||||
> println(name?.uppercase() ?: "<нет>") // <нет>
|
||||
> }
|
||||
> ```
|
||||
> ```java
|
||||
> Optional<String> name = Optional.empty();
|
||||
> System.out.println(name.map(String::toUpperCase).orElse("<нет>"));
|
||||
> ```
|
||||
>
|
||||
> **Sealed-типы вместо иерархии исключений.** В Java разные причины сбоя обычно выражают классами исключений и ловят их по типу. Sealed-тип даёт то же различение, но с проверкой полноты при компиляции и без стоимости стектрейса.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`runCatching` перехватывает всё, включая то, что перехватывать нельзя.** Он ловит `Throwable`, то есть `OutOfMemoryError`, `StackOverflowError` и - самое опасное - `CancellationException` в корутинах:
|
||||
> ```kotlin
|
||||
> // Требует kotlinx-coroutines-core.
|
||||
> // suspend fun broken() {
|
||||
> // runCatching { delay(1000) } // проглотит отмену корутины
|
||||
> // }
|
||||
> //
|
||||
> // suspend fun fixed() = runCatching { delay(1000) }
|
||||
> // .onFailure { if (it is CancellationException) throw it }
|
||||
> ```
|
||||
> В корутинах либо проверяйте тип вручную, либо используйте обычный `try`/`catch` с перехватом конкретных типов. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
|
||||
>
|
||||
> **`Result` нельзя вернуть из функции в публичном API без оговорок.** До Kotlin 1.5 это было запрещено совсем; сейчас разрешено, но тип по-прежнему `value class`, поэтому из Java он виден странно и его нельзя использовать как тип свойства:
|
||||
> ```kotlin
|
||||
> class Holder {
|
||||
> // val r: Result<Int> = Result.success(1) // компилируется, но Result как поле боксируется
|
||||
> private val value: Int = 1
|
||||
> fun get(): Result<Int> = Result.success(value)
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Holder().get().getOrNull()) } // 1
|
||||
> ```
|
||||
>
|
||||
> **`Result` стирает тип ошибки.** Все причины сбоя - `Throwable`, поэтому различать их приходится через `is`, и компилятор полноту не проверяет:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val r = runCatching<Int> { error("сбой") }
|
||||
> val msg = when (val e = r.exceptionOrNull()) {
|
||||
> null -> "успех"
|
||||
> is IllegalStateException -> "состояние: ${e.message}"
|
||||
> else -> "прочее" // else обязателен
|
||||
> }
|
||||
> println(msg) // состояние: сбой
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Создание `failure` всё равно создаёт исключение со стектрейсом** - то есть `Result` не даёт экономии на производительности, если ошибку формирует `runCatching`. Дешёвый вариант - sealed-тип без `Throwable` внутри.
|
||||
>
|
||||
> **Проигнорированный `Result` не вызывает предупреждения:**
|
||||
> ```kotlin
|
||||
> fun save(): Result<Unit> = runCatching { error("не записалось") }
|
||||
>
|
||||
> fun main() {
|
||||
> save() // ошибка потеряна молча
|
||||
> println(save().isFailure) // true — так правильно
|
||||
> }
|
||||
> ```
|
||||
> В отличие от исключения, которое всплывёт само, забытый `Result` ничего не делает. Это основной риск подхода.
|
||||
>
|
||||
> **Цепочка `map` на `Result` может бросить.** `map` не перехватывает исключения из своей лямбды - для этого есть `mapCatching`.
|
||||
>
|
||||
> **`getOrThrow` на чужом `Result` бросает исходное исключение,** а не обёртку, поэтому стектрейс указывает на место первоначального сбоя, а не на `getOrThrow`. Для диагностики это хорошо, для чтения логов - неожиданно.
|
||||
>
|
||||
> **Смешение подходов в одном слое запутывает.** Если часть функций репозитория возвращает `null`, часть `Result`, а часть бросает, клиентский код обрастает тремя видами обработки. Выберите одно соглашение на слой.
|
||||
|
||||
Далее: [Проверки и контракты](Проверки%20и%20контракты.md)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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<out T>"] --> C["Collection<out T>"]
|
||||
C --> L["List<out T>"]
|
||||
C --> S["Set<out T>"]
|
||||
I --> MI["MutableIterable<T>"]
|
||||
MI --> MC["MutableCollection<T>"]
|
||||
C --> MC
|
||||
MC --> ML["MutableList<T>"]
|
||||
L --> ML
|
||||
MC --> MS["MutableSet<T>"]
|
||||
S --> MS
|
||||
M["Map<K, out V>"] --> MM["MutableMap<K, V>"]
|
||||
```
|
||||
|
||||
Два важных факта из этой схемы:
|
||||
- `MutableList` **наследует** `List`, то есть изменяемую коллекцию можно передать туда, где ожидается читаемая. Обратное невозможно.
|
||||
- `Map` не входит в `Collection` - это отдельная иерархия, как и в Java.
|
||||
|
||||
| Интерфейс | Чтение | Изменение | Реализация по умолчанию |
|
||||
|---|---|---|---|
|
||||
| `List<T>` | да | нет | `ArrayList` под капотом |
|
||||
| `MutableList<T>` | да | да | `ArrayList` |
|
||||
| `Set<T>` | да | нет | `LinkedHashSet` |
|
||||
| `MutableSet<T>` | да | да | `LinkedHashSet` |
|
||||
| `Map<K, V>` | да | нет | `LinkedHashMap` |
|
||||
| `MutableMap<K, V>` | да | да | `LinkedHashMap` |
|
||||
|
||||
Обратите внимание: умолчание для `Set` и `Map` - **`Linked`**-версии, то есть порядок вставки сохраняется. В Java `new HashSet<>()` порядок не гарантирует.
|
||||
|
||||
## Создание
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a = listOf(1, 2, 3) // read-only
|
||||
val b = mutableListOf(1, 2, 3) // изменяемый
|
||||
val c = emptyList<Int>() // пустой
|
||||
val d = listOfNotNull(1, null, 3) // null отфильтрованы
|
||||
val e = List(3) { it * it } // по индексу
|
||||
|
||||
println("$a $b $c $d $e") // [1, 2, 3] [1, 2, 3] [] [1, 3] [0, 1, 4]
|
||||
|
||||
val s = setOf(1, 2, 2, 3) // дубликаты убраны
|
||||
val m = mapOf("a" to 1, "b" to 2)
|
||||
println("$s $m") // [1, 2, 3] {a=1, b=2}
|
||||
}
|
||||
```
|
||||
|
||||
Специализированные варианты для разных реализаций:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val sorted = sortedSetOf(3, 1, 2) // TreeSet
|
||||
val sortedMap = sortedMapOf("b" to 2, "a" to 1)
|
||||
val linked = linkedSetOf(3, 1, 2) // LinkedHashSet
|
||||
val hash = hashSetOf(3, 1, 2) // HashSet, порядок не гарантирован
|
||||
val arrayDeque = ArrayDeque(listOf(1, 2, 3))
|
||||
|
||||
println("$sorted $sortedMap $linked") // [1, 2, 3] {a=1, b=2} [3, 1, 2]
|
||||
println(hash.size) // 3
|
||||
arrayDeque.addFirst(0)
|
||||
println(arrayDeque) // [0, 1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
## Билдеры
|
||||
Когда коллекцию нужно собрать по условию, а результат отдать как read-only:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val list = buildList {
|
||||
add(1)
|
||||
if (true) add(2)
|
||||
addAll(listOf(3, 4))
|
||||
}
|
||||
println(list) // [1, 2, 3, 4]
|
||||
|
||||
val map = buildMap {
|
||||
put("a", 1)
|
||||
putAll(mapOf("b" to 2))
|
||||
}
|
||||
println(map) // {a=1, b=2}
|
||||
|
||||
val set = buildSet { repeat(3) { add(it % 2) } }
|
||||
println(set) // [0, 1]
|
||||
}
|
||||
```
|
||||
Внутри лямбды коллекция изменяемая, снаружи возвращается `List<T>`. Это чище, чем `mutableListOf` с последующим приведением.
|
||||
|
||||
## Пустая коллекция и null
|
||||
```kotlin
|
||||
fun main() {
|
||||
val nullable: List<Int>? = null
|
||||
println(nullable?.size ?: 0) // 0
|
||||
println(nullable.orEmpty()) // []
|
||||
println(nullable.isNullOrEmpty()) // true
|
||||
|
||||
val empty = emptyList<Int>()
|
||||
println(empty.firstOrNull()) // null
|
||||
println(empty.sum()) // 0
|
||||
}
|
||||
```
|
||||
Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List<T>?` заставляет каждого клиента писать `?.` или `orEmpty()`, не давая взамен никакой информации - пустой список уже означает «ничего нет».
|
||||
|
||||
## Массив не коллекция
|
||||
```kotlin
|
||||
fun main() {
|
||||
val array = arrayOf(1, 2, 3)
|
||||
// array.filter — есть, потому что Array получает расширения из Iterable
|
||||
println(array.filter { it > 1 }) // [2, 3] — это уже List
|
||||
|
||||
println(array.toList()::class.simpleName) // ArrayList
|
||||
println(listOf(1, 2).toTypedArray().size) // 2
|
||||
}
|
||||
```
|
||||
`Array<T>` - отдельный тип, не входящий в `Collection`. У него фиксированный размер и инвариантность. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [List, Set, Map](List,%20Set,%20Map.md) | три семейства, их гарантии и выбор между ними |
|
||||
| [Изменяемость](Изменяемость.md) | read-only против immutable, защита от изменения |
|
||||
| [Операции](Операции.md) | `map`, `filter`, `groupBy`, `fold` и остальной арсенал |
|
||||
| [Sequence](Sequence.md) | ленивые вычисления и когда они выигрывают |
|
||||
| [Коллекции под капотом](Коллекции%20под%20капотом.md) | что лежит в байт-коде, сложность операций, память |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `new ArrayList<>()` | `mutableListOf()` |
|
||||
> | `List.of(1, 2)` (Java 9+) | `listOf(1, 2)` |
|
||||
> | `Collections.emptyList()` | `emptyList()` |
|
||||
> | изменяемость не в типе | `List` против `MutableList` |
|
||||
> | `UnsupportedOperationException` в рантайме | ошибка компиляции |
|
||||
> | `Stream` для операций | расширения прямо на коллекции |
|
||||
> | `new HashSet<>()` - порядок не гарантирован | `setOf()` - порядок вставки |
|
||||
>
|
||||
> **Разделение на `List` и `MutableList` - главное нововведение.** В Java `List` - один интерфейс с методом `add`, и `List.of(1, 2)` его тоже имеет: вызов просто бросает `UnsupportedOperationException` в рантайме.
|
||||
> ```java
|
||||
> List<Integer> list = List.of(1, 2);
|
||||
> list.add(3); // компилируется, падает в рантайме
|
||||
> ```
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1, 2)
|
||||
> // list.add(3) // не компилируется
|
||||
> println(list.size)
|
||||
> }
|
||||
> ```
|
||||
> Ошибка переехала из рантайма в компиляцию - это главная практическая польза.
|
||||
>
|
||||
> **Операции не требуют `Stream`.** В Java цепочка выглядит как `list.stream().filter(...).map(...).collect(Collectors.toList())`. В Kotlin `filter` и `map` - функции-расширения прямо на `Iterable`, они сразу возвращают `List`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }) // [20, 30]
|
||||
> }
|
||||
> ```
|
||||
> Цена - промежуточный список на каждом шаге. Ленивый аналог `Stream` - это [Sequence](Sequence.md).
|
||||
>
|
||||
> **`setOf` и `mapOf` сохраняют порядок вставки**, потому что под ними `LinkedHashSet` и `LinkedHashMap`. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как `Set`, а не как «упорядоченное множество».
|
||||
>
|
||||
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`List` - это read-only, а не immutable.** Объект под ним может меняться через другую ссылку:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val mutable = mutableListOf(1, 2)
|
||||
> val readOnly: List<Int> = mutable // та же коллекция!
|
||||
> mutable.add(3)
|
||||
> println(readOnly) // [1, 2, 3] — изменился
|
||||
> }
|
||||
> ```
|
||||
> `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md)
|
||||
>
|
||||
> **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает:
|
||||
> ```kotlin
|
||||
> fun giveList(): List<Int> = mutableListOf(1, 2)
|
||||
>
|
||||
> fun main() { println(giveList().size) } // 2
|
||||
> ```
|
||||
> Java-клиент вызовет `add` без всяких ошибок компиляции, и если под ссылкой `ArrayList` - элемент добавится. Для настоящей защиты нужна копия.
|
||||
>
|
||||
> **`listOf()` возвращает разные реализации.** `listOf()` без аргументов - `EmptyList`, `listOf(x)` - `SingletonList`, с несколькими - `Arrays$ArrayList`. То же у `mapOf` и `setOf`: на одном элементе это `SingletonMap`/`SingletonSet`, а не `Linked`-версия. Последний не поддерживает `add` даже через каст:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1, 2)
|
||||
> try {
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> (list as MutableList<Int>).add(3)
|
||||
> } catch (e: UnsupportedOperationException) {
|
||||
> println("каст скомпилировался, но add упал")
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Приводить `List` к `MutableList` нельзя: иногда это сработает, иногда бросит исключение.
|
||||
>
|
||||
> **`Map` не `Collection`** - у него нет `size` через `Collection`-интерфейс, `filter` работает с `Map.Entry`, а `map` возвращает `List`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val m = mapOf("a" to 1, "b" to 2)
|
||||
> println(m.filter { it.value > 1 }) // {b=2} — остался Map
|
||||
> println(m.map { it.key }) // [a, b] — стал List
|
||||
> println(m.mapValues { it.value * 10 }) // {a=10, b=20}
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Операции создают новую коллекцию на каждом шаге.** Цепочка из пяти `map`/`filter` по списку в миллион элементов создаст пять списков. Для длинных цепочек и больших данных нужен [Sequence](Sequence.md).
|
||||
>
|
||||
> **`hashSetOf` и `hashMapOf` не гарантируют порядок.** Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте `setOf`/`mapOf` или сортируйте явно.
|
||||
|
||||
Далее: [List, Set, Map](List,%20Set,%20Map.md)
|
||||