Compare commits

...

33 Commits

Author SHA1 Message Date
Aklivtsov b62e54bbb0 Merge branch 'master' into AK_Additions 2026-10-20 12:55:54 +03:00
Aklivtsov 6853ada348 Kotlin guide done. (20.10.26) 2026-10-20 12:54:25 +03:00
roma-dxunvrs 4d0e404259 add vliw, empty risc and m68k
deploy / Pull and Restart (push) Successful in 16s
2026-10-15 16:39:49 +03:00
Aklivtsov da3ad9dd1a Added kotlin guide. (WIP) (15.10.26) 2026-10-15 00:24:21 +03:00
roma-dxunvrs 005017595b 10.10
deploy / Pull and Restart (push) Successful in 16s
2026-10-10 19:41:44 +03:00
roma-dxunvrs 23a9e06d53 update index
deploy / Pull and Restart (push) Successful in 16s
2026-10-08 21:27:47 +03:00
roma-dxunvrs 8c95d15f6c add deploy guide 2026-10-08 21:00:18 +03:00
roma-dxunvrs 61e293f81b update zsh guide
deploy / Pull and Restart (push) Successful in 17s
2026-10-05 23:57:00 +03:00
roma-dxunvrs 75e45eb930 web add deploy guide
deploy / Pull and Restart (push) Successful in 17s
2026-10-05 22:49:35 +03:00
roma-dxunvrs 12a473f46e web update
deploy / Pull and Restart (push) Successful in 17s
2026-10-04 19:57:51 +03:00
roma-dxunvrs 11432e782f csa update
deploy / Pull and Restart (push) Successful in 16s
2026-10-03 13:42:57 +03:00
roma-dxunvrs fec7f50b3e 02.10.26
deploy / Pull and Restart (push) Successful in 16s
2026-10-02 22:58:53 +03:00
roma-dxunvrs 4886625dac fix
deploy / Pull and Restart (push) Successful in 18s
2026-10-01 20:57:53 +03:00
roma-dxunvrs 977e016822 other: important
deploy / Pull and Restart (push) Successful in 17s
2026-10-01 20:55:51 +03:00
roma-dxunvrs 3764f7dafa js: add table of contents
deploy / Pull and Restart (push) Successful in 17s
2026-10-01 11:32:48 +03:00
roma-dxunvrs ecd928d72d other: add zsh guide
deploy / Pull and Restart (push) Successful in 16s
2026-09-30 20:41:07 +03:00
roma-dxunvrs c0b8ac0043 fix PL, add x86 in CS
deploy / Pull and Restart (push) Successful in 16s
2026-09-29 21:49:51 +03:00
roma 88b307c311 Merge pull request 'ProgLangs added. 29.09.26' (#4) from AK_Additions into master
deploy / Pull and Restart (push) Successful in 17s
Reviewed-on: #4
2026-09-29 21:29:11 +03:00
AK dc82207723 ProgLangs added. 29.09.26 2026-09-29 21:22:14 +03:00
roma-dxunvrs 8ebd7b0b4f ci: Add 2 AM daily schedule to deploy workflow
deploy / Pull and Restart (push) Successful in 1m15s
2026-09-28 11:29:36 +03:00
roma-dxunvrs dd3ac17361 docs: Add RKN IP ban workaround to VPN notes 2026-09-28 11:29:01 +03:00
roma-dxunvrs fc45caa657 24.09.26
deploy / Pull and Restart (push) Successful in 18s
2026-09-24 21:57:22 +03:00
roma-dxunvrs 7637cab7a9 22.09.26
deploy / Pull and Restart (push) Successful in 18s
2026-09-22 22:37:41 +03:00
roma-dxunvrs 0e6e583e6d refactor: wiki links -> md links
deploy / Pull and Restart (push) Successful in 17s
2026-09-22 08:52:23 +03:00
roma-dxunvrs 6421fd3a48 21.09.26
deploy / Pull and Restart (push) Successful in 18s
2026-09-22 00:07:22 +03:00
roma-dxunvrs 08e557da92 Add js
deploy / Pull and Restart (push) Successful in 18s
2026-09-20 13:12:31 +03:00
roma-dxunvrs c90a404c40 Refactor
deploy / Pull and Restart (push) Successful in 18s
2026-09-19 19:20:43 +03:00
roma-dxunvrs d5adddc4f1 Fix index
deploy / Pull and Restart (push) Successful in 17s
2026-09-19 11:25:55 +03:00
roma-dxunvrs 05a67d2998 18.09.26
deploy / Pull and Restart (push) Successful in 17s
2026-09-18 22:52:40 +03:00
roma-dxunvrs 98eeb949fc Fix2 bbi
deploy / Pull and Restart (push) Successful in 17s
2026-09-18 14:01:11 +03:00
roma-dxunvrs 3e4e10da10 Fix bbi
deploy / Pull and Restart (push) Successful in 17s
2026-09-18 13:57:14 +03:00
roma-dxunvrs 8c815df1bc BBI
deploy / Pull and Restart (push) Successful in 18s
2026-09-18 13:54:08 +03:00
roma-dxunvrs fb5f9d4cf2 Add f32a 2026-09-18 13:47:58 +03:00
347 changed files with 37833 additions and 493 deletions
+5
View File
@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- master - master
schedule:
- cron: '0 2 * * *'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -20,7 +22,10 @@ jobs:
key: ${{ secrets.SERVER_SSH_KEY }} key: ${{ secrets.SERVER_SSH_KEY }}
script: | script: |
cd /opt/docs/obsidian 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 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 .. cd ..
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S docker compose down echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S docker compose down
echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S docker compose up -d --build echo "${{ secrets.SERVER_SUDO_PASSWORD }}" | sudo -S docker compose up -d --build
+3
View File
@@ -0,0 +1,3 @@
[submodule "LeterZP"]
path = LeterZP
url = ../../LeterZP/Info.git
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+11 -5
View File
@@ -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 ## Инструменты low-level
- C Language - C Language
- [[Системные паттерны]] - [Системные паттерны](Системные%20паттерны/Системные%20паттерны.md)
## Сети ## Сети
- Компьютерные сети - Компьютерные сети
@@ -0,0 +1,88 @@
- 32-битная стековая архитектура
- Главный принцип - команды не имеют операндов в коде. Все данные неявно берутся с веришны стека, результат кладется обратно в стек.
- Небольшая пасхалка на [вот эту задачу с литкода](../../АиСД/Задачи/LeetCode/Evaluate%20Reverse%20Polish%20Notation.md)
## Регистры, флаги и стеки
- Имеет два независимых аппаратных стека и два регистра-указателя
### Стеки
- `Data Stack` - место где происходят вычисления
- `T` - вершина стека данных
- `S` - второй элемент на стеке данных
- `Return Stack` - стек возврата
- `R` - вершина стека возвратов
### Регистры
- `A` - указатель адреса памяти с поддержкой автоинкремента
- `B` - второй указатель памяти
### Флаги
- `C` - перенос
- `EAM` - режим расширенной арифметики
> [!info] Все регистры, флаги и стеки можно выводить в отчете: `A:dec`, `stack:dec`, `rstack:hex`, `C`, `EAM` и так далее
> [!info] Получение максимальной глубины стека, которая была достигнута на конкретном шаге через: `f32a:data-stack-max` и `f32a:return-stack-max`
## Система команд
- Комментарии - `\`
- Опкод - 1 байт. Если есть операнд, то еще 4 байта
### 1. Операции со стеком
- `dup` - дублировать вершину (`[X] -> [X, X`])
- `drop` - удалить вершину (`[X, Y] -> [X]`)
- `over` - скопировать второй элемент наверх (`[X, Y] -> [X, Y, X]`)
### 2. Операции над памятью
- `<value>` (или `lit <value>`) - положить константу на `Data Stack` (от слова literal)
- `a!` / `b!` - снять значение со стека и записать в регистр `A` / `B`
- `a` - скопировать значение из регистра `A` на стек
- `@p <addr>` - прочитать из адреса на стек
- `!p <addr>` - снять со стека и записать по адресу
- `@` / `!` - чтение/запись по адресу, который лежит в регистре `A`
- `@+` / `!+` - чтеение/запись по адресу в `A` и инкремент `A`
- `@b` / `!b` - чтение/запись по адресу, который лежит в регистре `B`
### 3. Арифметика
- `+` - сумма, снимает два верхних числа, складывает, кладет на вершину стека. Если в режиме `EAM`, то прибавляет еще и флаг переноса. Устанавливает флаг переноса в любом случае
- `+*` - один шаг умножения
- `+/` - один шаг деления
- `2*` - сдвиг влево вершины стека
- `2/` - сдвиг вправо вершины стека
- `inv` - побитовая инверсия вершины стека
- `eam` - снять число с `Data Stack` и записать его в `EAM`
### 4. Побитовые операции
- `and` - побитовое И над `T` и `S` (снимает их)
- `xor` - исключающее ИЛИ
### 5. Управление вычислительным процессом
- `;` - возврат из подпрограммы (снять адрес с `Return Stack` и прыгнуть)
- `<label>` - вызов подпрограммы (адрес возврата кладется на `Return Stack` + прыжок)
- `<label> ;` - прыжок (без записи адреса возврата)
- `next <label>` - цикл for, проверяет вершину стека возврата `R`, если `R != 0`, уменьшает его на 1 и прыгает на метку. Если `R==0`, удаляет `R` и выходит из цикла
- `if <label>` - снимает значение с `Data Stack`, если оно ноль, то прыжок
- `-if <label>` - снимает значение с `Data Stack`, если оно `>=0`. то прыгает
- `halt` - стоп
### 6. Взаимодействие со стеком возврата
- `r>` - снять элемент с `Return Stack` и положить на `Data Stack`
- `>r` - снять элемент с `Data Stack` и положить на `Return Stack`
## Подробнее про умножение и деление
### `+*`
- Как работает умножение чисел в двоичной системе (`A*S`): смотрим на каждый бит числа A, если бит 1, то прибавляем S к результату, если бит 0, то пропускаем, в конце сдвигаем результат
- Как использует эта команда данные
1. Регистр `A` - множитель
2. `S` (второй элемент стека данных) - множимое
3. `T` (первый элемент стека данных) - накапливается ответ, сначала 0
- Теперь эта команда по шагам
1. Процессор смотрит на младший бит `A`
2. Если там `1`, то прибавляет `S` к `T` (`T = T + S`). Если `0` - ничего не прибавляет
3. Сдвигает регистр `A` вправо на 1 бит (`A >> 1`). Тот бит, который мы только что проверили выбрасывается
4. Сдвигает результат `T` вправо на 1 бит (`T >> 1`)
5. Сдвинутый из `T` бит вставляется в старший бит `A` (`A[31]`)
- После 32 повторений такой команды, в регистре `A` и на вершине стека `T` образуется 64-битное число - полный результат умножения. В `A` - младшая половина ответа, а в `T` - старшая
### `+/`
- Деление в двоичной системе: сдвигаем влево и пытаемся вычесть делитель. Если вычитается - пишем 1, если нет - 0
- Как использует данные команда
1. Регистр `A` - делимое
2. `Mem[B]` - делитель
3. `S` - старшая часть числа, здесь будет остаток
4. `T` - кладем 0, здесь будет частное
- Команда по шагам
1. Сдвигаем остаток `S` влево на 1 бит
2. Забираем старший бит из `A` и вставляем в младший бит `S`
3. Сдвигаем `A` влево
4. Сдвигаем `T` влево
5. Если можно вычесть делитель (`S >= Mem[B]`), то ставим в младший бит ответа единицу (`T[0]=1`), если нельзя - бит ответа оставляем нулем
- После 32 повторений на стеке в `T` будет лежать частное, в `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,4 +1,9 @@
## Инструментарий ## Инструментарий
- [[Платформа Wrench]] - [Платформа Wrench](Платформа%20Wrench.md)
## Архитектуры ## Архитектуры
- [[ACC32]] - [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, то просто зануление - для сдвига влево - Если сдвинуть числа больше чем на 32, то просто зануление - для сдвига влево
- Для сдвигов вправо, если исходное число было положительным - зануление - Для сдвигов вправо, если исходное число было положительным - зануление
- Для сдвигов вправо, если иходное число было отрицательным - `-1` - Для сдвигов вправо, если исходное число было отрицательным - `-1`
### Переходы ### Переходы
- В отличие от БЭВМ переходы тут работают иначе. Переходы, которые сравнивают с нулем, учитывают только знак (в бэвм учитывали еще `n+v(mod2)`). Поэтому надо самому реализовывать переходы, которые учитывают специфику доп. кода через `bvz+bltz` например - В отличие от БЭВМ переходы тут работают иначе. Переходы, которые сравнивают с нулем, учитывают только знак (в бэвм учитывали еще `n+v(mod2)`). Поэтому надо самому реализовывать переходы, которые учитывают специфику доп. кода через `bvz+bltz` например
@@ -4,15 +4,15 @@
- Принцип расчета - воплощение таблицы значений в виде, позволяющем быстро находить нужные зависимости - Принцип расчета - воплощение таблицы значений в виде, позволяющем быстро находить нужные зависимости
- Расчет производится путем выставления линейки в требуемое положение и считывания результата - Расчет производится путем выставления линейки в требуемое положение и считывания результата
![[Лог линейка.png]] ![Лог линейка](../../Attachments/Лог%20линейка.png)
### Линейка и операция деления ### Линейка и операция деления
![[Деление на линейке.png]] ![Деление на линейке](../../Attachments/Деление%20на%20линейке.png)
Двигаем делимое напротив делителя, в начале получаем частное Двигаем делимое напротив делителя, в начале получаем частное
### Номограмма и операция умножения ### Номограмма и операция умножения
![[Номограмма.png]] ![Номограмма](../../Attachments/Номограмма.png)
Соединяем две точки, которые хотим перемножить. Пересечение этого отрезка с осью Y - наш результат Соединяем две точки, которые хотим перемножить. Пересечение этого отрезка с осью Y - наш результат
# Табличное исполнение # Табличное исполнение
@@ -26,7 +26,7 @@
- Принципы расчета воплощены в мехнизмах устройства - Принципы расчета воплощены в мехнизмах устройства
- Расчет производится по шагам (алгоритмически), с учетом переноса между разрядами - Расчет производится по шагам (алгоритмически), с учетом переноса между разрядами
![[Арифмометр.png]] ![Арифмометр](../../Attachments/Арифмометр.png)
# Отличие арифмометра от логарифмической линейки # Отличие арифмометра от логарифмической линейки
1. Рост области определения функций: 1. Рост области определения функций:
- для арифмометра: медленный рост сложности устройства, средний рост длительности расчета (зависит от функции) - для арифмометра: медленный рост сложности устройства, средний рост длительности расчета (зависит от функции)
@@ -53,7 +53,7 @@
### Архитектура эффективного решения ### Архитектура эффективного решения
Реконфигурируемый потоковой вычислитель Реконфигурируемый потоковой вычислитель
![[Потоковый вычислитель.png]] ![Потоковый вычислитель](../../Attachments/Потоковый%20вычислитель.png)
1. Расчетная формула представляется в виде графа 1. Расчетная формула представляется в виде графа
2. Каждой вершине графа сопоставляется человек, способный выполнить соответствующую операцию (только одну) 2. Каждой вершине графа сопоставляется человек, способный выполнить соответствующую операцию (только одну)
@@ -63,7 +63,7 @@
6. Для повышения надежности расчетов используются двоирование (две роты, сравниваем результаты, не совпадение - повтор) 6. Для повышения надежности расчетов используются двоирование (две роты, сравниваем результаты, не совпадение - повтор)
### Структура белкового вычислителя ### Структура белкового вычислителя
![[Белковый вычислитель.png]] ![Белковый вычислитель](../../Attachments/Белковый%20вычислитель.png)
### Достоинства ### Достоинства
1. Радикальный рост производительности (параллелизм инструкций) 1. Радикальный рост производительности (параллелизм инструкций)
- Параллельное исполнение независимых операций - Параллельное исполнение независимых операций
@@ -73,11 +73,11 @@
- Минимальная вариативность процесса - Минимальная вариативность процесса
3. Возможность реконфигурации вычислителя под новую задачу 3. Возможность реконфигурации вычислителя под новую задачу
![[Конвейризация расчетов.png]] ![Конвейризация расчетов](../../Attachments/Конвейризация%20расчетов.png)
Цвета - 3 партии входных данных. Цифры - на каком такте будут на данном этапе вычисления Цвета - 3 партии входных данных. Цифры - на каком такте будут на данном этапе вычисления
### Практика ### Практика
На практике все перечисленные механизмы встречаются в современных [[Компьютерные системы|компьютерных системах]], в том числе: На практике все перечисленные механизмы встречаются в современных [компьютерных системах](../Системная%20инженерия/Компьютерные%20системы.md), в том числе:
- В процессорах общего назначения - В процессорах общего назначения
- В специализированных процессорах - В специализированных процессорах
- В распределенных системах - В распределенных системах
@@ -87,7 +87,7 @@
- вертикальные нити - натянуты равномерно - вертикальные нити - натянуты равномерно
- горизонтальные нити разных цветов, дисбаланс петель с одной из сторон формирует узор - горизонтальные нити разных цветов, дисбаланс петель с одной из сторон формирует узор
![[Жаккард.png]] ![Жаккард](../../Attachments/Жаккард.png)
### Проблема создания станка ### Проблема создания станка
Плетение полотна: Плетение полотна:
1. Нити разделяются на верхние и нижние по схеме узора 1. Нити разделяются на верхние и нижние по схеме узора
@@ -98,7 +98,7 @@
- Разделение на верхнюю и нижнюю нить нерегулярно - Разделение на верхнюю и нижнюю нить нерегулярно
- "Гребенка" не работает - "Гребенка" не работает
![[Обычный ткацкий станок.png]] ![Обычный ткацкий станок](../../Attachments/Обычный%20ткацкий%20станок.png)
### Жаккардовый ткацкий станок ### Жаккардовый ткацкий станок
Считается первой программно-управляемой машиной Считается первой программно-управляемой машиной
Элементы решения: Элементы решения:
@@ -107,17 +107,17 @@
- Сильное нажатие и поворот - освободить нить - Сильное нажатие и поворот - освободить нить
- Как автоматизировать регулярное нажатие на кнопочки? - Как автоматизировать регулярное нажатие на кнопочки?
![[Идея крючка 1.png]] ![Идея крючка 1](../../Attachments/Идея%20крючка%201.png)
Идея крючка Идея крючка
![[Идея крючка 2.png]] ![Идея крючка 2](../../Attachments/Идея%20крючка%202.png)
### Программное обеспечение ### Программное обеспечение
Элементы решения: Элементы решения:
- Пластина с отверстиями позволяет разом "нажать" нужные крючки (перфокарта) - Пластина с отверстиями позволяет разом "нажать" нужные крючки (перфокарта)
- Лента из пластин - программа узора - Лента из пластин - программа узора
![[Жаккард программное решение.png]] ![Жаккард программное решение](../../Attachments/Жаккард%20программное%20решение.png)
Структура шага работы станка: Структура шага работы станка:
1. Нажатие пластиной на крючки (разделение верхней и нижней) 1. Нажатие пластиной на крючки (разделение верхней и нижней)
@@ -151,4 +151,4 @@
- Линейный рост скорости поиска от количества городов (до 8 штук) - Линейный рост скорости поиска от количества городов (до 8 штук)
- При росте вычислительной сложности по экспоненте - При росте вычислительной сложности по экспоненте
Как развитие и скачок произошел переход к использованию электричества в сигналах и состояниях. ([[Цифровая схемотехника]]) Как развитие и скачок произошел переход к использованию электричества в сигналах и состояниях. ([Цифровая схемотехника](../Цифровая%20схемотехника/Цифровая%20схемотехника.md))
@@ -0,0 +1,61 @@
1. Развитие [Машины Тьюринга](Модель%20вычислений.md#Машина%20Тьюринга). Отличия:
- Лента заменена на Random-Access Memory (RAM)
- Инструкции и данные объединены
2. Призвана быть максимально простой в реализации и производстве
3. Процедурное программирование и ООП - тот же принцип
![](../../Attachments/Машина%20фон%20Неймана.png)
## Особенности
1. Использование двоичного кодирования
- Встречается и троичное, и двоично-десятичное кодирование
2. Программное управление. Команды выполняются последовательно
- Последовательность и порядок сегодня условны
3. Память компьютера однородно хранит данные и программы
- Однородность спорна
4. Ячейки памяти компьютера имеют адреса. Random-Access Memory
- Адрес не сводится к целому числу сегодня
- Память не является пассивным элементом компьютера
5. Возможность условного перехода
- И перехода по иным причинам
## Окружение
### (Микро)процессор
Цифровая схема, которая выполняет операции с внешним источником данных (обычно с памятью или потоком данных)
### (Микро)контроллер
Микросхема, предназначенная для управления электронными устройствами. Типичный микроконтроллер сочетает функции процессора и периферийных устройств, содержит ОЗУ и/или ПЗУ
### Оперативная память (память)
Обычно часть компьютерной памяти, в которой хранится выполняемый машинный код, а также входные, выходные и промежуточные данные
## Control Unit и DataPath
### Control Unit
Компонент центрального процессора (CPU), управляющей работой процессора. CU часто использует двоичный декодер для преобразования закодированных инструкций в управляющие сигналы, координирующие работу остальных блоков
### DataPath
АЛУ, набор регистров и внутренние шины CPU, по которым данные перемещаются между ними
![](../../Attachments/Control%20Unit%20and%20DataPath.png)
## Виды инструкций
1. Работа с памятью: запись констант, копирование данных (память, регистры, порты I/O)
2. Арифметческие, логические и битовые операции
3. Управляющие операции: переходы, вызов и возврат из подпрограмм
4. Инструкции сопроцессоров: загрузка и выгрузка данных, управление сопроцессором
## Роль машинного слова
Единица данных, которая выбрана естественной для данной архитектуры процессора
1. Может быть адресована в памяти
2. Передается по шине между памятью и процессором за один раз
3. Является стандартной единицей обрабатываемой информации
4. Связанные вопросы: формат инструкции, форматы данных, форматы адресов
## На практике
1. В чистом виде практически не встречаются
2. Влияние на индустрию - трудно переоценить
3. Элементы архитектуры присутствуют в подавляющем большинстве процессоров и средств разработки
4. Структурное программирование, ООП - наследие фон Неймана
5. Ловушка обратной совместимости
## Вариации
1. Память команд и данных: Принстонская и Гарвардская архитектуры
- Типы данных, адресные пространства, машинные слова, теги
2. Принцип работы с данными в DataPath
- Accumulator, Register-to-Register, Register-to-Memory, Memory-to-Memory, Stack Architectures
3. "Мощность" системы команд: CISC, RISC
- Количество операндов (0, 1, 2, ...)
- Кодирование инструкций
- Принципы построения Control Unit
4. Адресация операндов
Кроме того: ввод-вывод, параллелизм, иерархия памяти и тп
@@ -0,0 +1,82 @@
MoC - Model of Computation
1. MoC предоставляет язык для описания моделей процессов/программ/структур
2. MoC определяет возможности вычислительной машины
3. MoC характеризует, как исполняется модель (программа): возможные состояния вычислителя, их последовательность, правила переходов (детерменированные или нет)
4. MoC, обычно, минималистичны относительно реальных вычислителей
## Последовательные модели
Sequential models
Относительно просто реализуются в виде аппаратных схем
### Конечные автоматы
![](../../Attachments/FSM.png)
Несколько наборов состояний системы и условия переходов между ними
#### Автомат с магазинной памятью
![](../../Attachments/Автомат%20с%20магазинной%20памятью.png)
Например, парсеры
#### Достоинства
- Таблица состояний
- Верифицируемость переходов
- Простая реализация в схемотехнике и ПО
### Машина Тьюринга
Включает:
- Неограниченную двустороннюю ленту, разделенную на ячейки
- Устройство управления (головка записи-чтения), которое может находиться в конечном числе состояний
Управляющее устройство может:
- Перемещаться по ленте влево и вправо
- Читать и записывать в ячейки символы конечного алфавита
Машина Тьюринга
1. Не может быть реализована на практике (нет бесконечной ленты)
2. Обладает полнотой по Тьюрингу (можно реализовать любой известный алгоритм)
3. Проблема остановки
4. Данные и управление отделены
5. Ориентирована на реализацию
### Random Access Machine
![](../../Attachments/RAM.png)
## Функциональные модели
Functional models
Представление вычислительного процесса как совокупности символов и правил их преобразований
1. Арифметика
2. Лямбда исчисление (полнота по Тьюрингу)
3. Combinatory logic
4. General recursive functions
5. Abstract rewriting systems
Программа/данные - математический объект
Возможна Тьюринг неполнота
### Лямбда исчисление
#### 4 вида выражений
1. Переменная: $x, y\ \text{и} \ z$
2. Константа: $a,b,c$
3. Коминация (вызов): $s\ t$
4. Абстракция: $\lambda x.s$
#### 3 вида редукций/подстановки
1. Переименование переменных: $\lambda u.u\ v \xrightarrow{\alpha} \lambda w.w\ v$
2. Применение функции (подстановка аргумента): $(\lambda x.s)\ t \xrightarrow{\beta} s[t/x]$
3. Сокращение аргумента: $\lambda u.v\ u \xrightarrow{\eta} v$
## Параллельные модели
Системы, включающие несколько взаимодействующих процессов
- Kahn process networks - однонаправленные каналы с неограниченными буферами (+- как в гошке)
- Actor model - отправка сообщений в "почтовые ящики" (как в Erlang)
- Discrete Event - события в заданные моменты времени (цифровые схемы)
- Multi-Thread Model - общая память (как в джаве)
- Synchronous Data Flow - синхронизированные потоки данных
## Близкие понятия
### Парадигмы программирования
Способ классификации языков программирования на основе их функций. Языки могут относиться к нескольким парадигмам
### Стиль программирования
Это набор правил или рекомендации, используемых при написании исходного кода компьютерной программы
## Применение на практике
1. Computer Science. Формальные модели
2. Дизайн ЯПов
3. Ограничение творческого порыва, контроль сложности
4. Модель-ориентированная инженерия
## Model-Driven Engineering
1. Разработка через моделирование предметной области
2. Автоматическая генерация кода из моделей
3. Повышение уровня абстракции при разработке
4. Сокращение разрыва между требованиями и реализацией
5. Улучшение коммуникации между заинтересованными сторонами
6. Снижение сложности разработки масштабных систем
@@ -0,0 +1,41 @@
## Реле -> Программируемые логические контроллеры
### Проблемы автоматизации релейными схемами
- Плохо масштабируется
- Сложная настройка и поддержка
- Ненадежность сложных схем
- Проблема ведения документации
### ПЛК
- Специальная разновидность электронной вычислительной машины
- Особенности
1. [Управляющие системы](../Системная%20инженерия/Управляющие%20системы.md)
2. Законченные устройства для встроенного использования
3. Специализация ввода-вывода, модульность
4. Выделение инструментальной составляющей
5. Эксплуатация в тяжелых условиях
### Типовое устройство ПЛК
![ПЛК](../../Attachments/ПЛК.png)
- Существуют и полностью программные реализации
- Отделение инструментальной составляющей
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. Формальные свойства, реальное время
![АСУТП](../../Attachments/АСУТП.png)
### Программирование ПЛК (N штук)
- **IEC 61499** - Standard for distributed Automation
- System of Systems
- Разделение управления и данных
- Интероперабельность по данным (возможность обмена данными между разными системами, даже разных фирм)
![ПЛК N](../../Attachments/ПЛК%20N.png)
@@ -0,0 +1,64 @@
## Инорфмационный процессор
- Система (электрическая, механическая или биологическая), которая принимает информацию (последовательность символов или состояний) в одной форме и обрабатывает (преобразует) ее в другую форму
- Например: исходный сигнал в статистику, посредством алгоритмического процесса
- Включает:
1. Ввод
2. Процессор
3. Хранилище
4. Вывод
## Универсальный процессор
Позволяет решать широкий круг задач, настройка которых производится после производства "по месту" или в run-time
### Примечания
1. Универсальность != полнота по Тьюрингу
2. Теоретическая универсальность != практическая универсальность
3. Универсальность противоречит эффективности
4. Современный процессор - совокупность программного и аппаратного обеспечения
### Свойства
1. [Hardware and Software](../Системная%20инженерия/Hardware%20and%20Software.md) (двухэтапное производство)
2. Полнота по Тьюрингу
3. Отсутствие "серьезных" ограничений на "объем" программы
4. Изменяемость ПО
## Виды процессоров
1. Application-Specific Integrated Circuit
2. Coarse-Grained Reconfigurable Arrays
3. Field-Programmable Gate Array
4. Digital Signal Processor
5. Graphics Processing Unit
6. Central Processing Unit
![](../../Attachments/Процессоры%20и%20системы%20команд.png)
## Архитектура процессора
То, как видит компьютер программист. Определена набором команд (язык), местом нахождения операндов (регистры и память) и вычислительными механизмами (кеш, прерывания и тд)
1. Множество разных [архитектур](../Архитектуры/Вычислительные%20архитектуры.md)
2. Чтобы понять архитектуру, нужно знать ее язык
3. Слова в языке компьютера - "инструкции" или "команды"
4. Словарный запас - "система команд"
## Микроархитектура процессора
Соединение простейших цифровых элементов в логические блоки, предназначенные для выполнения команд определенной архитектуры
### Примечания
1. Описывает, как в процессоре расположены и соединены друг с другом регистры, АЛУ, конечные автоматы, блоки памяти, интерфейсы ввода-вывода и тп
2. У каждой архитектуры может быть много микроархитектур, обеспечивающих разное соотношение производительности, цены, сложности, технической эстетики. Они смогут выполнять одни и те же программы
3. Если ISA и микроархитектура отличаются на уровне MoC - обычно требуется уровень виртуализации (ПО, аппаратура, транслятор и тд)
## Система команд (ISA)
- Instruction Set Architecture
- Абстрактная модель процессора, формирующая интерфейс взаимодействия между программным обеспечением и процессором
- Производительность, энергопотребление и задержки часто не рассматриваются
### Что входит
1. Типы данных
2. Модель памяти, система и методы адресации
3. Набор инструкций
4. Механизмы обработки прерываний и исключений
5. Методы ввода и вывода
### Машинное слово
Фрагмент данных фиксированного размера, обрабатываемый как единое целое процессором
### Разнообразие систем команд
1. Действие над памятью/регистрами
- Произвольной сложности и длины (CISC)
- Фиксированного формата (RISC)
2. Действие без указания обрабатываемых данных (стековый)
3. Управляющие сигналы процессора под видом команды (NISC)
4. Группа команд под видом одной команды (VLIW)
5. Действие над шиной данных (TTA)
6. Команды без последовательного исполнения (редукционные)
7. Система команд из одной команды (URISC)
8. Управляющие сигналы процессоров под видом команды (NITTA)
@@ -0,0 +1,82 @@
- **Hardware** производится один раз и дальше не меняется, не корректируется
- **Software** адаптируется под задачу, легко изменяется
- Разделены по времени/специальности: разные дисциплины и разные индустрии
## Как разделить HW/SW
1. Определяем возможности "конфигурирования":
- Заложенные при проектировании - интерфейс задокументирован и стабилен (ISA, API)
- Незадокументированные (нецелевое использование, дыра в безопасности) - используется то, что интерфейсом не гарантировано но работает
2. Фиксируем интерфейс "конфигурирования"
- Набор механизмов
- Модель вычислений
- Интструментальные средства
## Конфигурирование [РЭА](Особенности%20производства%20аппаратуры.md#Монтаж%20радиоэлектронной%20аппаратуры%20(РЭА))
1. Макетные платы
2. Печатные платы: перерезать, навесить дорожку
3. Джамперы
4. Платы расширения
- Вычислительные платы
- Порты ввода/вывода
- Dendy (память программ и графическая память)
## Реконфигурация. Коммутация
### Реконфигурируемые архитектуры
Архитектуры, где задача отображается в структуру вычислителя (пространственно, spatial), а не выполняется потоком инструкций. В отличие от ASIC, это структура может быть перенастроена после изготовления
1. Программируемые логические интеграционные схемы
2. Крупногранулярные реконфигурируемые массивы (CGRA)
3. Выстраивание людей для расчета таблиц
## Программное управление
1. Данные (программы) определяют то, какой прикладной процесс будет разворачиваться
2. Количество уровней исполнения/интерпретации данных ограничено только целесообразностью
**Временные вычисления** - одна и та же схема последовательно проходит через разные состояния во времени
## Аппаратное обеспечение
Электронные и механические части вычислительного устройства, входящие в состав системы. Включает: компьютеры и логические устройства, внешние устройства и диагностическую аппаратуру, энергетическое оборудование
## Программное обеспечение (ПО)
Совокупность программ и программных документов необходимых для эксплуатации. Позволяет аппаратному обеспечению выполнять функции
## Аппаратура и ПО != Hardware и Software
1. Программная/аппаратная составляющая слабо связаны с SW/HW:
- Minix - ОС встроена в Intel ME/CSME
- Микропрограммное обеспечение
2. Часто аппаратурой называют то, что работает как аппаратное обеспечение
- Virutal-Computer, Virtual-Network, Virtual-Volume, etc
- Реконфигурируемые архитектуры (ПЛИС, CGRA)
3. Разделение на SW/HW зависит в большей степени от способа использования
- Hardware - тяжело/долго/дорого поменять
- Software - легко/быстро/дешево поменять
## Плюсы и минусы ПО/SW
### Часть плюсов
1. Быстрый цикл разработки, CI/CD и тд
2. Меняем у пользователя
3. Пользователь - Beta-тестер
### Часть минусов
1. Высокая сложность программ
2. Безопасность
3. Культура разработки. Верификация
## Типовое HW/SW проектирование
### Проектируем аппаратуру
- Система команд и вычислительные механизмы
- Интерфейсы управления аппаратурой
- Цифровая схематехника, система команд
### Пишем программу
- Прикладная задача
- Язык и библиотеки
- Система команд и вычислительные механизмы (виртуальная память, прерывания)
- Интерфейсы управления аппаратурой
### Проблемы
1. Излишне шаблонное проектирование
2. Высокие интеграционные риски
3. Последовательная разработка (сроки)
## Hardware/Software CoDesign
Мы проектируем систему
1. Разработка интерфейса HW/SW (системы команд)
2. Разработка моделей HW/SW, для получения обратной связи
3. Разработка HW/SW, верификация моделей
4. Заменая моделей на HW/SW
![](../../Attachments/CoDesign%20HW-SW.png)
В итоге: сокращение цикла разработки
## Как выбирать инструменты для разработки Software
1. От доступного Hardware и инструментальных средств
2. От доступной команды разработки
3. От прикладной задачи
- От области (библиотеки)
- От модели вычислений (принципов организации вычислительного процесса: параллелизм, конвейризация, оптимизации и тд)
@@ -5,7 +5,7 @@
3. Кластерные и облачные вычисления 3. Кластерные и облачные вычисления
## История ## История
![[История инф систем.png]] ![История инф систем](../../Attachments/История%20инф%20систем.png)
1. Mainframes - большие компьютерные системы, колоды перфокарт. Пакетный режим, долго считает 1. Mainframes - большие компьютерные системы, колоды перфокарт. Пакетный режим, долго считает
2. Minis - миниЭВМ, компьютеры значительно меньше, эксклюзивный доступ к компьютеру, человек логиниться уже под своим юзером, а не кучу людей на одно вычисление 2. Minis - миниЭВМ, компьютеры значительно меньше, эксклюзивный доступ к компьютеру, человек логиниться уже под своим юзером, а не кучу людей на одно вычисление
@@ -10,8 +10,8 @@
- станок - станок
- и т.п. - и т.п.
## Классификация компьютерных систем ## Классификация компьютерных систем
1. [[Информационные системы]] 1. [Информационные системы](Информационные%20системы.md)
2. [[Управляющие системы]] 2. [Управляющие системы](Управляющие%20системы.md)
Про [[Уровни компьютерных систем]] Про [Уровни компьютерных систем](../Системная%20инженерия/Уровни%20компьютерных%20систем.md)
История: [[Докомпьютерные вычисления]] История: [Докомпьютерные вычисления](Докомпьютерные%20вычисления.md)
@@ -0,0 +1,120 @@
Задача производства: превратить документацию в изделие
## Монтаж радиоэлектронной аппаратуры (РЭА)
### Ключевые тенденции
1. Рост уровня интеграции
2. Снижение затрат на производство изделия
3. Усложнение производственной цепочки
### Навесной монтаж
Способ монтажа электронных схем, при котором расположенные на изолирующем шасси радиоэлементы соединяются друг с другом проводами или непосредственно выводами
#### Достоинства
1. Простота производства
2. Простота подготовки производства
3. Гибкость
#### Недостатки
1. Сложность автоматизации, дорого в серии
2. Надежность
3. Плотность ограничена прямотой рук монтажника
На макетной плате
![Макетная плата](../../Attachments/Макетная%20плата.png)
### Монтаж на печатную плату
Печатная плата - пластина из диэлектрика, на поверхности и/или в объеме которой сформированы электропроводящие цепи электронной схемы. Электрическое и механическое соединение компонентов
Различают:
- одно- и двухслойные платы
- многослойные платы
## Виды монтажа компонентов
- Выводной (в отверстие)
- Поверхностный
- Встроенный
## Поверхностный монтаж (SMD) и Surface-mount technology (SMT)
![SMT](../../Attachments/SMT.png)
### Виды SMD компонентов
![Виды SMD](../../Attachments/Виды%20SMD.png)
### Встроенные компоненты
![Встроенные комопненты](../../Attachments/Встроенные%20комопненты.png)
### Достоинства
1. Автоматизация
2. Плотность размещения компонентов
3. Размеры
4. Стоимость в серии
### Недостатки
1. Необходимость подготовки производства
2. Удлинение производственной цепочки
3. Сложность внесения изменений
### Гибкость
1. Джампер (by design)
2. "Перерезать дорожку"
3. "Навесная дорожка"
### Производственная цепочка
![Произв цепочка SMD](../../Attachments/Произв%20цепочка%20SMD.png)
## Интегральные схемы. Кремниевое произодство
![Кремний 1](../../Attachments/Кремний%201.png)
### Кремниевый песок
Начало производственной цепочки
![Кремний песок](../../Attachments/Кремний%20песок.png)
### Выращиваем монокристалл кремния (буля
![Буля](../../Attachments/Буля.png)
### Производим подложку будущих чипов (Wafer)
![Wafer](../../Attachments/Wafer.png)
### Фотолитография и "море" транзисторов
"свет-шаблон-фоторезист"
1. На кремниевую подложку наносят материал для рисунка
2. Наносится фоторезист
3. Экспонирование через фотошаблон
4. Удаление отработанного фоторезиста
![Фотолитография](../../Attachments/Фотолитография.png)
### Interconnect
![Кремний 2](../../Attachments/Кремний%202.png)
### Структура чипа в разрезе
![Кремний 3](../../Attachments/Кремний%203.png)
### Извлечение будущих чипов
![Кремний 4](../../Attachments/Кремний%204.png)
### Корпус и контакты
![Кремний 5](../../Attachments/Кремний%205.png)
### Современный степпер (засветка фотошаблона)
![Кремний 6](../../Attachments/Кремний%206.png)
### Надежность
Производственный брак неизбежен: не все транзисторы на пластине работают корректно
**Чип биннинг** - один и тот же кристалл продается под разными марками: дефектные или нестабильные блоки блокируются программно или лазером
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. Система как совокупность частей ## 1. Система как совокупность частей
Система состоит из подсистем, которые в свою очередь тоже являются системами, и так далее - иерархия систем Система состоит из подсистем, которые в свою очередь тоже являются системами, и так далее - иерархия систем
![[Иерархия систем.png]] ![Иерархия систем](../../Attachments/Иерархия%20систем.png)
### Структура системы ### Структура системы
![[Структура системы.png]] ![Структура системы](../../Attachments/Структура%20системы.png)
Одна система при разных взглядах. При разных взглядах на систему что-то может и совпадать. В зависимости от нужд от системы - она меняется. Одна система при разных взглядах. При разных взглядах на систему что-то может и совпадать. В зависимости от нужд от системы - она меняется.
Например: дом с точки зрения сантехника, электрика и строителя Например: дом с точки зрения сантехника, электрика и строителя
@@ -55,6 +55,6 @@
5. Рассмотрения ее обеспечивающих систем 5. Рассмотрения ее обеспечивающих систем
## Фундаментальная проблема систем ## Фундаментальная проблема систем
Множественность точек зрения на систему. Систему может разрушить проблема коммуникации при разработке Множественность точек зрения на систему. Систему может разрушить проблема коммуникации при разработке
Формальный способ зафиксировать эти точки зрения и передать их другим - [[Понятие архитектуры|архитектура]] Формальный способ зафиксировать эти точки зрения и передать их другим - [архитектура](Понятие%20архитектуры.md)
## Применение в Computer Science ## Применение в Computer Science
- [[Компьютерные системы]] - [Компьютерные системы](Компьютерные%20системы.md)
@@ -17,7 +17,7 @@
Пример, где один код может быть как ИС, так и УС - видеокодек ffmpeg ( 1 сценарий - конвертация видео на сервере - классика ИС, 2 сценарий - воспроизведение на устройстве пользователя - УС) Пример, где один код может быть как ИС, так и УС - видеокодек ffmpeg ( 1 сценарий - конвертация видео на сервере - классика ИС, 2 сценарий - воспроизведение на устройстве пользователя - УС)
## Эволюция ## Эволюция
![[Эволюция УС.png]] ![Эволюция УС](../../Attachments/Эволюция%20УС.png)
1. Информационная УС - объект управления + кабели (тот же мейнфрейм, только без входного пакета данных, данные были всегда одни и те же) 1. Информационная УС - объект управления + кабели (тот же мейнфрейм, только без входного пакета данных, данные были всегда одни и те же)
2. Встроенные системы - большая интеграция, ну и размеры стали меньше 2. Встроенные системы - большая интеграция, ну и размеры стали меньше
@@ -22,4 +22,7 @@
- *Языки* (Languages) - на чем описываем/пишем - *Языки* (Languages) - на чем описываем/пишем
- *Типы* (Types) - предопределенные платформой абстракции и структуры данных - *Типы* (Types) - предопределенные платформой абстракции и структуры данных
- *Экземпляры* (Instances) - готовые, предопределенные платформой объекты и компоненты - *Экземпляры* (Instances) - готовые, предопределенные платформой объекты и компоненты
- *Шаблоны* (Patterns) - связывают языки, типы и экземпляры, задают повторяемые решения (про [[Системные паттерны]]) - *Шаблоны* (Patterns) - связывают языки, типы и экземпляры, задают повторяемые решения (про [Системные паттерны](../Системные%20паттерны/Системные%20паттерны.md))
## Принципы формирования уровней (платформ)
- **Принцип развития иерархически систем Седова** - закономерности и механизмы, по которым развиваются сложные иерархические системы. Ключевой принцип: ограничение разнообразия на нижележащем уровне для получения разнообразия на вышележащем уровне
- **Метасистемный подход** - концепция, предложенная Валентином Турчиным, описывающая процесс перехода системы на новый уровень организации, становясь метасистемой. В этом процессе несколько систем интегрируются в одну более сложную, обладающую новыми свойствами
@@ -0,0 +1,48 @@
## Кодирование
### Двоичное кодирование
![Сигналы](../../Attachments/Сигналы.png)
#### Достоинства
1. Надежно и помехоустойчиво
2. Простая арифметика
3. Диапазоны и точность наращиваются разрядностью (слева и справа соотвественно)
4. Погрешности "by design", а не "by implementation"
#### Недостатки
1. Нечитаемое представление
2. Простые десятичные дроби записываются в виде бесконечных двоичных дробей
3. Дискретное кодирование сигналов (точность)
## Машинное слово. Представления данных
- Машинное слово - единица данных, естественная для обработки вычислителем
- Примеры: сложение, пересылка и т.п.
- Целые числа
- Позиционные системы счисления
- Дополнительный код
- Big & Little endian
- Дроби
- С фиксированной точкой
- С плавающей точкой
- Перечисления
- Двоично-десятичное кодирование
- Символы (старые кодировки)
- Тегированные данные
![Little & Big endian](../../Attachments/Little%20&%20Big%20endian.png)
### Классификация типов по версии IBM
![IBM types](../../Attachments/IBM%20types.png)
## Код Грея и BCD
### Код Грея
Две "соседние" кодовые комбинации различаются только в одном двоичном разряде
### BCD
Форма записи рациональных чисел, когда каждый десятичный разряд числа записывается в виде его четырехбитного двоичного кода
![Gray and BCD](../../Attachments/Gray%20and%20BCD.png)
### Base64 и Base58
### Base64
Стандарт кодирования двоичных данных при помощи только 64 символов ASCII. Алфавит кодирования содержит латинские символы A-Z, a-z, цифры 0-9 (всего 62 знака) и 2 дополнительных символа
![Base64](../../Attachments/Base64.png)
### Base58
Модификация Base64 для избежания неоднозначных символов (0, I, O, |). В конце сообщения - хешсумма (checksum)
@@ -2,4 +2,5 @@
- Сторожевые таймеры - микроконтроллеры, OC/гипервизоры, оркестраторы - Сторожевые таймеры - микроконтроллеры, OC/гипервизоры, оркестраторы
- Кеши и когерентность - CPU, OC (page cache), диски, CDN, браузер - Кеши и когерентность - CPU, OC (page cache), диски, CDN, браузер
- Прерывания и события - аппартные IRQ, сигналы OC, event loop, message queue - Прерывания и события - аппартные 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]] ![Реле 1](../../../Attachments/Реле%201.png)
## Управление двигателем ## Управление двигателем
Классическая схема включения электродвигателя с аварийной остановкой Классическая схема включения электродвигателя с аварийной остановкой
![[Реле двигатель.png]] ![Реле двигатель](../../../Attachments/Реле%20двигатель.png)
![[Реле двигатель 2.png]] ![Реле двигатель 2](../../../Attachments/Реле%20двигатель%202.png)
## Практика применения реле ## Практика применения реле
1. Когда-то использовали, чтобы делать компьютеры 1. Когда-то использовали, чтобы делать компьютеры
2. Непосредственное применение, силовые шкафы управления. Высокий ток, неблагоприятная среда эксплуатации 2. Непосредственное применение, силовые шкафы управления. Высокий ток, неблагоприятная среда эксплуатации
@@ -0,0 +1,28 @@
### Комбинационная схема
Схема, составленная из набора логических элементов, в совокупности реализующая заданную таблицу истинности
### Простейшие элементы комбинационных схем
![Комб схемы](../../../Attachments/Комб%20схемы.png)
### КС: Возможности
Реализация функций - отображение множества A на множество B
A и B конечны (представимы конечной последовательностью бит) -> возможно построить комбинационную схему
### КС: Подход к построению
1. Через таблицу истинности (черный ящик)
2. Через алгоритмизацию (белый ящик, этапность/структура расчета) - логарифмическая линейка и арифмометр
#### Через таблицу истинности
1. Формирование таблицы истинности
2. Запись таблицы истинности в каноническую форму (ДНФ и КНФ), отображаемую в комбинационную схему
3. Минимизация булевой функции (сокращаем размер схемы)
Пример: шифратор (encoder, 4-to-2)
![Encoder](../../../Attachments/Encoder.png)
#### Через алгоритмизацию
Схема строится на основе понимания функции, взаимосвязи входа и выхода. Творческий процесс. Схема может быть каскадной
![Сумматоры](../../../Attachments/Сумматоры.png)
### Свойства комбинационных схем
1. Возможность **установления стабильного состояния** при корректном входе (нет циклов)
2. **Задержка** установления состояния после изменения входных значений
3. **Накопление ошибки** в физическом процессе, что может привести к ошибке на логическом уровне (буферы)
4. **Параллелизм уровня бит** (узлы с параллельным включением работают параллельно)
5. **Уровень линии**, не сигнал (как и в реле)
@@ -0,0 +1,5 @@
## Условный оператор. Мультиплексор
Схемотехника не может не работать. Уровень не может не быть
На уровне комбинационных схем без состояния: спекулятивные вычисления и выбор нужного результата (мультиплексор является классом [комбинационных схем](Комбинационные%20схемы.md))
![Мультиплексор](../../../Attachments/Мультиплексор.png)
@@ -1,7 +1,7 @@
Асинхронный триггер, который сохраняет свое предыдущее состояние при неактивном состоянии обоих входов и изменяет свой состояние при подаче на один из его входов активного уровня Асинхронный триггер, который сохраняет свое предыдущее состояние при неактивном состоянии обоих входов и изменяет свой состояние при подаче на один из его входов активного уровня
![[RS-триггер.png]] ![RS-триггер](../../../Attachments/RS-триггер.png)
Реализация на [[Электрическое реле|реле]]: Реализация на [реле](../Базовые%20элементы/Электрическое%20реле.md):
![[RS-триггер на реле.png]] ![RS-триггер на реле](../../../Attachments/RS-триггер%20на%20реле.png)
@@ -0,0 +1,40 @@
### 1. Конвейрное исполнение
- Количество стадий - количество паралельных задач
- Больше "разбиений" - ниже задержка схемы, выше частота
Пример: сравнение 4 чисел:
![Многотакт](../../../Attachments/Многотакт.png)
### 2. Spatial and Temporal Computation
- Сложение 16-битных чисел:
- Сумматор на 16 бит и 1 такт
- Сумматор на 8 бит и 2 такта + система управления
![Spatial Computation](../../../Attachments/Spatial%20Computation.png)
Сумматор на 8 бит и 2 така + система управления
![Сумматор 8 бит 2 такта](../../../Attachments/Сумматор%208%20бит%202%20такта.png)
### 3. Синхронная схемотехника
- Меньше гонок, проще синхронизация
- Частота по самой медленной комбинационной схеме
- Дискредитация входных сигналов по времени
- Синхронность - в заданном диапазоне
![Синхронная схемотехника](../../../Attachments/Синхронная%20схемотехника.png)
### 4. Программное управление
## Пример схемы с состоянием
![Схема с состоянием](../../../Attachments/Схема%20с%20состоянием.png)
### Реализация схемы на Verilog
![Схема 1](../../../Attachments/Схема%201.png)
### Схема детальная с состоянием
![Схема 2](../../../Attachments/Схема%202.png)
- Так как память используется только для чтения, она выродилась в комбинационную схему
- Смысл цветов линий неизвестен...
### Временная диаграмма схемы с состоянием
![Схема 3](../../../Attachments/Схема%203.png)
@@ -0,0 +1,15 @@
## Состояние в комбинационной схеме
Триггер - класс электронных устройств, обладающих способностью длительно находиться в одном из двух устойчивых состояний и чередовать их под воздействием внешних сигналов
Триггер позволяет:
1. Зафиксировать состояние линии на длительное время
2. Ограничить распространение сигнала по схеме
- Разорвать цикл
- Сократить задержку в комбинационной схеме
### Виды триггеров
1. Условие изменения состояния
- По фронту
- По уровню
2. Интерфейс
- D-триггер - запоминает состояние входа и выдает его на выход
- [RS-триггер](RS-триггер.md)
- И другие
@@ -1,8 +1,15 @@
## Коммутация и базовые элементы ## Коммутация и базовые элементы
- [[Базовая логика]] - [Базовая логика](Базовые%20элементы/Базовая%20логика.md)
- [[Логические вентили]] - [Логические вентили](Базовые%20элементы/Логические%20вентили.md)
- [[Электрическое реле]] - [Электрическое реле](Базовые%20элементы/Электрическое%20реле.md)
## Память и состояние
- [[RS-триггер]]
## Комбинационные схемы ## Комбинационные схемы
- [Комбинационные схемы](Комбинационные%20схемы/Комбинационные%20схемы.md)
- [Мультиплексор](Комбинационные%20схемы/Мультиплексор.md)
## Схемы с памятью
- [Многотактовые схемы](Схемы%20с%20памятью/Многотактовые%20схемы.md)
- [Триггеры](Схемы%20с%20памятью/Триггеры.md)
## Ключевые отличия схемотехники от программирования
1. Все процессы между регистрами всегда происходят параллельно. Не читайте код как алгоритм, рисуйте схему
2. Передача сигнала - физический аналоговый процесс. Есть питание и контакт - есть передача. Сигнал не может не идти
3. Нет понятия "система остановилась". Она всегда работает, если есть питание
4. Таблица истинности неполна - результат будет слуйчайным, но будет (и возможно воспроизводимым)
+145
View File
@@ -0,0 +1,145 @@
---
title: Kotlin
description: Полное пособие по Kotlin — синтаксис, ООП, корутины, инструментарий
tags:
- kotlin
---
Статически типизированный язык от JetBrains, работающий на [JVM](../Программирование%20на%20Java/JVM/JVM.md), а также компилируемый в JavaScript, WebAssembly и нативный код.
- Первый релиз - 2011, стабильная 1.0 - 2016
- Официальный язык разработки для Android с 2019
- Полная двусторонняя совместимость с Java: один проект может содержать оба языка
- Текущая версия - 2.4.x, компилятор K2 по умолчанию
Главные отличия от Java: [null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) на уровне типов, [корутины](Корутины/Корутины.md) вместо потоков, выражения вместо инструкций, отсутствие проверяемых исключений, в 2-3 раза меньше кода на ту же задачу.
## Маршрут
Последовательность для изучения с нуля. Каждая заметка заканчивается ссылкой `Далее:`, так что можно просто идти по цепочке.
1. [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md) - начало
2. [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) - ядро системы типов
3. [ООП](ООП/ООП.md) - классы и объекты
4. [Коллекции](Коллекции/Коллекции.md) - структуры данных
5. [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md) - функциональный стиль
6. [Корутины](Корутины/Корутины.md) - асинхронность
## Синтаксис и типы
### Базовый синтаксис
- [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md)
- [Типы данных](Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
- [Переменные](Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
- [Операторы и выражения](Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md)
- [Условные конструкции](Синтаксис%20и%20типы/Базовый%20синтаксис/Условные%20конструкции.md)
- [Циклы и диапазоны](Синтаксис%20и%20типы/Базовый%20синтаксис/Циклы%20и%20диапазоны.md)
- [Строковые шаблоны и вывод](Синтаксис%20и%20типы/Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md)
- [Пакеты и импорты](Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
### Null-безопасность
- [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
- [Операторы null-безопасности](Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
- [Умные приведения](Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
- [Платформенные типы](Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
### Работа с данными
- [Строки](Синтаксис%20и%20типы/Работа%20с%20данными/Строки.md)
- [Char](Синтаксис%20и%20типы/Работа%20с%20данными/Char.md)
- [Числа и переполнение](Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
- [Массивы](Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
- [Функции](Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
- [Лямбды и функциональные типы](Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
- [Деструктуризация](Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
- [Приведение типов](Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md)
### Базовые типы ООП
- [Any, Unit, Nothing](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
- [Data class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
- [Enum class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md)
- [Sealed class и interface](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
- [Value class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
- [Object и companion object](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
- [Typealias](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Typealias.md)
- [equals, hashCode и toString](Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
## ООП
- [ООП](ООП/ООП.md)
- [Классы и конструкторы](ООП/Классы%20и%20конструкторы.md)
- [Свойства](ООП/Свойства.md)
- [Инициализация](ООП/Инициализация.md)
- [Модификаторы видимости](ООП/Модификаторы%20видимости.md)
- [Наследование](ООП/Наследование.md)
- [Переопределение](ООП/Переопределение.md)
- [Абстрактные классы](ООП/Абстрактные%20классы.md)
- [Интерфейсы](ООП/Интерфейсы.md)
- [Делегирование](ООП/Делегирование.md)
- [Вложенные и внутренние классы](ООП/Вложенные%20и%20внутренние%20классы.md)
- [Полиморфизм](ООП/Полиморфизм.md)
## Исключения
- [Система исключений](Исключения/Система%20исключений.md)
- [Result и runCatching](Исключения/Result%20и%20runCatching.md)
- [Проверки и контракты](Исключения/Проверки%20и%20контракты.md)
## Коллекции
- [Коллекции](Коллекции/Коллекции.md)
- [List, Set, Map](Коллекции/List,%20Set,%20Map.md)
- [Изменяемость](Коллекции/Изменяемость.md)
- [Операции](Коллекции/Операции.md)
- [Sequence](Коллекции/Sequence.md)
- [Коллекции под капотом](Коллекции/Коллекции%20под%20капотом.md)
## Дженерики
- [Параметрический полиморфизм](Дженерики/Параметрический%20полиморфизм.md)
- [Вариантность](Дженерики/Вариантность.md)
- [Проекции типов](Дженерики/Проекции%20типов.md)
- [Reified и inline](Дженерики/Reified%20и%20inline.md)
## Функциональный Kotlin
- [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
- [Inline-функции](Функциональный%20Kotlin/Inline-функции.md)
- [Функции-расширения](Функциональный%20Kotlin/Функции-расширения.md)
- [Scope-функции](Функциональный%20Kotlin/Scope-функции.md)
- [Инфиксные и операторные функции](Функциональный%20Kotlin/Инфиксные%20и%20операторные%20функции.md)
- [Receivers и context parameters](Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
- [DSL](Функциональный%20Kotlin/DSL.md)
- [Паттерны проектирования](Функциональный%20Kotlin/Паттерны%20проектирования.md)
## Корутины
- [Корутины](Корутины/Корутины.md)
- [Suspend-функции](Корутины/Suspend-функции.md)
- [Структурированная конкурентность](Корутины/Структурированная%20конкурентность.md)
- [Билдеры корутин](Корутины/Билдеры%20корутин.md)
- [Диспетчеры и контекст](Корутины/Диспетчеры%20и%20контекст.md)
- [Отмена и таймауты](Корутины/Отмена%20и%20таймауты.md)
- [Исключения в корутинах](Корутины/Исключения%20в%20корутинах.md)
- [Flow](Корутины/Flow.md)
- [StateFlow и SharedFlow](Корутины/StateFlow%20и%20SharedFlow.md)
- [Channels](Корутины/Channels.md)
- [Разделяемое состояние](Корутины/Разделяемое%20состояние.md)
- [Корутины против потоков](Корутины/Корутины%20против%20потоков.md)
## Взаимодействие с Java
- [Взаимодействие с Java](Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
- [Java из Kotlin](Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
- [Kotlin из Java](Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
- [Kotlin в байт-коде](Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
## Инструментарий
- [Инструментарий](Инструментарий/Инструментарий.md)
- [kotlinc](Инструментарий/kotlinc.md)
- [Gradle и Kotlin DSL](Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
- [Структура проекта](Инструментарий/Структура%20проекта.md)
- [Компилятор K2](Инструментарий/Компилятор%20K2.md)
- [KSP](Инструментарий/KSP.md)
- [Линтеры и формат](Инструментарий/Линтеры%20и%20формат.md)
## Тестирование
- [Тестирование](Тестирование/Тестирование.md)
- [kotlin.test и JUnit 5](Тестирование/kotlin.test%20и%20JUnit%205.md)
- [Kotest](Тестирование/Kotest.md)
- [MockK](Тестирование/MockK.md)
- [Тестирование корутин](Тестирование/Тестирование%20корутин.md)
## Рефлексия
- [Рефлексия](Рефлексия/Рефлексия.md)
- [Аннотации](Рефлексия/Аннотации.md)
@@ -0,0 +1,385 @@
---
title: Java из Kotlin
description: Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи
tags:
- kotlin
- java-interop
---
Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова.
```kotlin
import java.io.File
import java.util.ArrayList
fun main() {
val list = ArrayList<String>()
list.add("первый")
list.add("второй")
println(list.size) // 2
println(list.joinToString()) // первый, второй
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText("содержимое")
println(tmp.readText()) // содержимое
tmp.delete()
}
```
Обратите внимание: `joinToString` и `writeText` - это Kotlin-расширения, работающие на Java-типах. Stdlib добавляет сотни таких функций к `java.util` и `java.io`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Платформенные типы
Главная особенность: тип из Java записывается как `String!` - компилятор не знает, может ли там быть `null`.
```kotlin
fun main() {
// System.getProperty возвращает String! — может быть null
val asNonNull: String = System.getProperty("java.version") // компилируется
val asNullable: String? = System.getProperty("нет.такого") // тоже компилируется
println(asNonNull.isNotEmpty()) // true
println(asNullable?.length) // null
}
```
Компилятор разрешает оба варианта, поэтому ответственность на вас. Если Java вернёт `null` там, где объявлен ненулевой тип, проверка сработает в точке присваивания:
```kotlin
fun main() {
val error = runCatching {
val value: String = System.getProperty("отсутствует")
value.length
}.exceptionOrNull()
println(error?.javaClass?.simpleName) // NullPointerException
}
```
Сообщение будет вида «Parameter specified as non-null is null» или «null cannot be cast to non-null type» - проверка вставляется компилятором. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
**Аннотации в Java решают проблему.** Если Java-код аннотирован, платформенных типов не возникает:
```java
// Java
@Nullable public String findName() { return null; } // в Kotlin: String?
@NotNull public String getId() { return "1"; } // в Kotlin: String
public String plain() { return null; } // в Kotlin: String!
```
Поддерживаются `org.jetbrains.annotations`, JSR-305 (`javax.annotation`), `androidx.annotation`, Lombok, Eclipse и Checker Framework. Это единственный способ получить настоящую null-безопасность на границе с Java, поэтому в смешанных проектах аннотировать Java-код стоит.
## Синтетические свойства
Java-геттеры и сеттеры видны как свойства Kotlin:
```kotlin
import java.util.Calendar
fun main() {
val cal = Calendar.getInstance()
cal.timeInMillis = 0L // вместо setTimeInMillis(0)
println(cal.timeInMillis) // 0
val sb = StringBuilder("текст")
println(sb.length) // 5 — вместо length()
}
```
Правила распознавания:
| Java-метод | Свойство в Kotlin |
|---|---|
| `getValue()` | `value` (только чтение) |
| `getValue()` + `setValue(T)` | `value` (чтение и запись) |
| `isReady()` | `isReady` (имя сохраняется) |
| `setOnly(T)` без геттера | свойства нет, только метод |
| `getValue(int index)` | свойства нет - есть параметр |
```kotlin
fun main() {
val thread = Thread.currentThread()
println(thread.name.isNotEmpty()) // true — getName()
println(thread.isDaemon) // false — isDaemon() сохраняет префикс
println(thread.priority) // 5 — getPriority()
}
```
Синтетические свойства доступны только для Java-методов. Kotlin-функция с именем `getFoo()` свойством не становится.
## SAM-конверсия
Java-интерфейс с одним абстрактным методом принимает лямбду:
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
executor.submit { println("как Runnable") } // лямбда -> Runnable
val future = executor.submit<String> { "как Callable" } // лямбда -> Callable
println(future.get())
val items = mutableListOf("гамма", "альфа", "бета")
items.sortWith { a, b -> a.compareTo(b) } // Comparator
println(items)
executor.shutdown()
// как Runnable
// как Callable
// [альфа, бета, гамма]
}
```
Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно:
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
val task = Runnable { println("явная SAM-конструкция") }
executor.submit(task)
executor.shutdown()
Thread.sleep(50)
// явная SAM-конструкция
}
```
Важная тонкость: SAM-конверсия работает для **Java**-интерфейсов автоматически. Для Kotlin-интерфейса нужно объявить его как `fun interface`:
```kotlin
fun interface Validator {
fun validate(input: String): Boolean
}
fun check(v: Validator, s: String) = v.validate(s)
fun main() {
println(check({ it.isNotEmpty() }, "текст")) // true
}
```
Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Массивы
Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные:
```kotlin
fun main() {
val ints: IntArray = intArrayOf(1, 2, 3) // int[]
val strings: Array<String> = arrayOf("a", "b") // String[]
val boxed: Array<Int> = arrayOf(1, 2) // Integer[]
println(ints::class.simpleName) // IntArray
println(strings::class.simpleName) // Array
println(ints.sum()) // 6
println(strings.joinToString()) // a, b
println(boxed.size) // 2
}
```
`IntArray` компилируется в `int[]` без упаковки, а `Array<Int>` - в `Integer[]`. Это важно для производительности и для совместимости: Java-метод, принимающий `int[]`, не примет `Array<Int>`.
Передача массива в varargs требует spread-оператора:
```kotlin
fun main() {
val parts = arrayOf("a", "b", "c")
println(String.format("%s-%s-%s", *parts)) // a-b-c
val numbers = intArrayOf(3, 1, 2)
println(listOf(*numbers.toTypedArray()).sorted()) // [1, 2, 3]
}
```
`*` распаковывает массив в отдельные аргументы - без него массив передался бы как один элемент.
Статические методы класса `java.lang.String` через Kotlin-овский `String` недоступны - `String` в Kotlin это `kotlin.String`, у него нет `join`:
```kotlin
fun main() {
val parts = listOf("x", "y")
println(java.lang.String.join("/", parts)) // x/y
println(parts.joinToString("/")) // x/y — идиоматично
}
```
## Статические члены
```kotlin
fun main() {
println(Integer.MAX_VALUE) // 2147483647
println(Integer.parseInt("42")) // 42
println(Math.max(3, 7)) // 7
val list = java.util.Collections.emptyList<String>()
println(list.size) // 0
}
```
Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: `"42".toInt()` вместо `Integer.parseInt`, `maxOf(3, 7)` вместо `Math.max`, `emptyList()` вместо `Collections.emptyList()`.
## Java-ключевые слова как идентификаторы
Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки:
```kotlin
class JavaLike {
fun `is`(value: Int) = value > 0 // в Kotlin `is` — ключевое слово
fun `in`(value: Int) = value
fun `object`() = "объект"
}
fun main() {
val j = JavaLike()
println(j.`is`(5)) // true
println(j.`in`(1)) // 1
println(j.`object`()) // объект
}
```
Чаще всего это встречается с методами `is`, `in`, `object`, `when`, `val`, `var`, `fun`, `typealias`. Backticks работают и для имён с пробелами - этим пользуются в названиях тестов. Подробнее: [kotlin.test и JUnit 5](../Тестирование/kotlin.test%20и%20JUnit%205.md)
## Checked exceptions исчезают
```kotlin
import java.io.File
fun main() {
// Java-метод объявляет throws IOException, но Kotlin этого не требует
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText("данные")
println(tmp.readText())
tmp.delete()
// данные
}
```
В Kotlin нет проверяемых исключений: `try/catch` вокруг `createTempFile` не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
## Наследование от Java-класса
```kotlin
import java.io.IOException
class CustomException(message: String, cause: Throwable? = null) :
IOException(message, cause)
abstract class BaseTask : Runnable {
abstract fun execute()
override fun run() {
try {
execute()
} catch (e: Exception) {
println("ошибка: ${e.message}")
}
}
}
class FailingTask : BaseTask() {
override fun execute() = throw CustomException("задача упала")
}
fun main() {
FailingTask().run() // ошибка: задача упала
}
```
Наследование работает в обе стороны без ограничений: Kotlin-класс может расширять Java-класс, реализовывать Java-интерфейс и быть предком Java-класса.
## Java-типы, которые заменяют на Kotlin-аналоги
| Java | Предпочтительно в Kotlin | Почему |
|---|---|---|
| `java.util.ArrayList` | `mutableListOf()` | короче, тот же класс внутри |
| `Integer.parseInt(s)` | `s.toInt()` | расширение, читается слева направо |
| `String.valueOf(x)` | `x.toString()` | то же |
| `Math.max(a, b)` | `maxOf(a, b)` | работает с любым `Comparable` |
| `Collections.sort(list)` | `list.sort()` | расширение |
| `Optional<T>` | `T?` | nullable-тип нативен |
| `new String[]{...}` | `arrayOf(...)` | короче |
| `StringBuilder` | интерполяция или `buildString` | читаемее |
```kotlin
fun main() {
val fromJava = java.util.ArrayList<String>().apply { add("a") }
val idiomatic = mutableListOf("a")
println("тот же класс: ${fromJava::class == idiomatic::class}") // тот же класс: true
println(buildString {
append("собрано ")
append("через buildString")
})
// собрано через buildString
}
```
`mutableListOf()` возвращает именно `java.util.ArrayList` - разница только в синтаксисе.
## Optional
```kotlin
import java.util.Optional
fun main() {
val present: Optional<String> = Optional.of("значение")
val empty: Optional<String> = Optional.empty()
println(present.orElse("по умолчанию")) // значение
println(empty.orElse("по умолчанию")) // по умолчанию
// перевод в идиоматичный Kotlin
val asNullable: String? = present.orElse(null)
println(asNullable?.length) // 8
println(empty.getOrNull()?.length) // null
}
fun <T> Optional<T>.getOrNull(): T? = orElse(null)
```
`Optional` в Kotlin-коде избыточен: его роль выполняет `T?`, который не создаёт обёртку и поддержан синтаксисом (`?.`, `?:`, `let`). При получении `Optional` из Java-API его сразу переводят в nullable.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `getName()` | `name` (синтетическое свойство) |
> | `isReady()` | `isReady` |
> | `new Runnable() { ... }` | `{ ... }` (SAM) |
> | `int[]` | `IntArray` |
> | `Integer[]` | `Array<Int>` |
> | `method(array)` для varargs | `method(*array)` |
> | `throws IOException` обязателен | не требуется |
> | `Optional<T>` | `T?` |
>
> **Платформенные типы - компромисс, которого в Java нет.** В чистом Kotlin компилятор гарантирует отсутствие NPE. На границе с Java гарантия исчезает: тип `String!` означает «компилятор не знает». Альтернативой было бы считать все Java-типы nullable - тогда Java-код требовал бы `?.` и `!!` на каждом шагу и стал бы неудобен. Авторы Kotlin выбрали практичность: ответственность за null на границе лежит на программисте.
>
> **Синтетические свойства работают только для Java.** Это важная асимметрия: Java-метод `getName()` в Kotlin доступен и как `getName()`, и как `.name`, а Kotlin-функция `getName()` - только как функция. Причина в том, что в Kotlin для этого есть настоящие свойства, и превращать функции в свойства означало бы двусмысленность.
>
> **SAM-конверсия автоматическая только для Java-интерфейсов.** В Java любой интерфейс с одним методом принимает лямбду. В Kotlin - нет: нужен `fun interface`, иначе требуется `object : Interface { }`. Причина - в Kotlin есть функциональные типы (`(String) -> Boolean`), которые покрывают большинство случаев, и SAM-интерфейсы нужны реже.
>
> **Массивы различаются сильнее, чем кажется.** В Java `int[]` и `Integer[]` - разные типы, но оба «массивы». В Kotlin это `IntArray` и `Array<Int>`, причём `Array<Int>` ковариантен по-другому: `Array<String>` не является `Array<Any>`, тогда как в Java `String[]` является `Object[]` - что и породило `ArrayStoreException`. Kotlin эту дыру закрыл, сделав `Array<T>` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.md)
>
> **Отсутствие проверяемых исключений - сознательное решение.** Опыт Java показал, что `throws` чаще приводит к `catch (Exception e) { }` и обёрткам в `RuntimeException`, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный `throws`). Цена - потеря документирующей роли и необходимость `@Throws` при вызове из Java.
>
> Сравнить: [Массивы в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Платформенный тип даёт NPE в неожиданном месте:**
> ```kotlin
> fun main() {
> val props = System.getProperties()
> val error = runCatching {
> val missing: String = props.getProperty("нет.такого")
> missing.uppercase()
> }.exceptionOrNull()
> println("NPE при присваивании: ${error is NullPointerException}")
> // NPE при присваивании: true
> }
> ```
> Исключение возникает не там, где значение используется, а там, где оно присваивается ненулевому типу. Стектрейс указывает на строку присваивания, а не на Java-метод.
>
> **`!!` на платформенном типе бессмысленен.** `javaMethod()!!` не добавляет безопасности - проверка и так вставляется. Правильно - `?:` с осмысленным значением по умолчанию или явный тип `String?`.
>
> **Java-аннотации nullability обязательны для настоящей безопасности.** Без них компилятор Kotlin не знает ничего, и весь Java-API остаётся зоной риска. Это основная причина добавлять `@Nullable`/`@NotNull` в унаследованный Java-код при миграции.
>
> **Неизменяемая Kotlin-коллекция, переданная в Java, может быть изменена:**
> ```kotlin
> fun main() {
> val readOnly = listOf("a")
> val error = runCatching { (readOnly as MutableList<String>).add("b") }
> .exceptionOrNull()
> println(error?.javaClass?.simpleName) // UnsupportedOperationException
> }
> ```
> Java-код увидит `java.util.List` с методом `add()` и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через `toMutableList()`, если Java-код изменяет коллекцию.
>
> **`Array<Int>` и `IntArray` не взаимозаменяемы.** Java-метод с параметром `int[]` требует `IntArray`; `arrayOf(1, 2)` даёт `Array<Int>` (то есть `Integer[]`) и не подойдёт. Конвертация - `toIntArray()`, и она копирует данные.
>
> **Забытый spread-оператор меняет смысл:**
> ```kotlin
> fun main() {
> val parts = arrayOf("a", "b")
> println(listOf(parts).size) // 1 — массив как один элемент
> println(listOf(*parts).size) // 2 — распакованный
> }
> ```
> Компилятор не всегда предупреждает - `listOf(parts)` корректен, просто создаёт `List<Array<String>>`.
>
> **`getClass()` вызывается как `javaClass`.** Метод `Object.getClass()` в Kotlin недоступен напрямую: он конфликтует с `::class`. Используйте `javaClass` для `java.lang.Class` и `::class` для `KClass`.
>
> **Статические вложенные Java-классы требуют полного пути.** `Map.Entry` в Kotlin - это `Map.Entry`, но `java.util.AbstractMap.SimpleEntry` нужно импортировать явно; обращение через внешний класс (`AbstractMap.SimpleEntry`) работает только после импорта внешнего.
>
> **Lombok-геттеры видны, но не всегда.** Lombok генерирует методы на этапе аннотационной обработки Java, и Kotlin-компилятор, работающий первым, их не видит. Нужен плагин `kotlin("plugin.lombok")` и `lombok.config` с `lombok.addLombokGeneratedAnnotation = true`.
>
> **Java-дженерики с wildcards переводятся в проекции.** `List<? extends Number>` становится `List<out Number>`, `List<? super Integer>` - `List<in Integer>`. Raw-тип `List` без параметра становится `List<*>`, и элементы из него имеют тип `Any?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md)
>
> **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации.
>
> **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом.
Далее: [Kotlin из Java](Kotlin%20из%20Java.md)
@@ -0,0 +1,434 @@
---
title: Kotlin в байт-коде
description: Во что компилируются свойства, лямбды, data class, inline, suspend, value class и null-проверки
tags:
- kotlin
- java-interop
- jvm
---
Понимание представления Kotlin в байт-коде отвечает на три практических вопроса: почему API выглядит из Java именно так, откуда берутся накладные расходы и что происходит при `NullPointerException` внутри сгенерированного кода.
Инструмент один - `javap`:
```
kotlinc Demo.kt -d out # компиляция в .class
javap -p -cp out DemoKt # сигнатуры, включая приватные
javap -c -p -cp out DemoKt # с байт-кодом
javap -v -p -cp out DemoKt # с аннотациями и константным пулом
```
```kotlin
fun add(a: Int, b: Int) = a + b
fun main() {
println(add(2, 3)) // 5
}
```
Эта функция компилируется в `public static final int add(int, int)` внутри класса `DemoKt`. Никакой обёртки нет - сигнатура такая же, как у Java-метода.
## Где живёт код
| Конструкция Kotlin | Класс в байт-коде |
|---|---|
| `class Foo` | `Foo` |
| функции верхнего уровня в `Demo.kt` | `DemoKt` |
| `object Bar` | `Bar` с полем `INSTANCE` |
| `companion object` в `Foo` | `Foo$Companion` + поле `Foo.Companion` |
| вложенный класс | `Foo$Inner` |
| анонимный объект | `Foo$method$1` |
| лямбда | `invokedynamic`, без отдельного класса |
| `enum class Color` | `Color extends java.lang.Enum` |
```kotlin
object Registry {
private val items = mutableListOf<String>()
fun add(item: String) { items += item }
fun size() = items.size
}
fun main() {
Registry.add("a")
println(Registry.size()) // 1
}
```
`object` становится классом с приватным конструктором, статическим полем `INSTANCE` и статическим инициализатором. Это классический singleton, и его потокобезопасность обеспечена гарантиями JVM на загрузку класса.
## Свойства
Свойство - это всегда приватное поле плюс аксессоры:
```kotlin
class Holder {
var mutable: String = "a"
val immutable: Int = 1
private var hidden = 2
lateinit var late: String
fun touch() = hidden++
}
fun main() {
val h = Holder()
h.mutable = "b"
println("${h.mutable} ${h.immutable} ${h.touch()}") // b 1 2
}
```
В байт-коде:
```
private java.lang.String mutable;
private final int immutable;
private int hidden;
public java.lang.String late; // lateinit — поле публично!
public final java.lang.String getMutable();
public final void setMutable(java.lang.String);
public final int getImmutable();
public final java.lang.String getLate();
public final void setLate(java.lang.String);
```
Три наблюдения:
**`val` даёт `final` поле и только геттер.** Это не константа - значение может вычисляться в конструкторе.
**`lateinit var` делает поле публичным.** Компилятор не может сделать его приватным, потому что геттер должен различать «не инициализировано» и «значение `null`» - для этого нужна прямая работа с полем, а `lateinit` исключает nullable-тип. Побочный эффект: поле видно из Java напрямую.
**Приватное свойство аксессоров не получает** - доступ идёт к полю, если нет кастомного геттера.
Свойство с кастомным геттером поля не создаёт вообще:
```kotlin
class Rect(val w: Int, val h: Int) {
val area: Int get() = w * h
}
fun main() {
println(Rect(3, 4).area) // 12
}
```
`area` - только метод `getArea()`, считающий значение каждый раз. Подробнее: [Свойства](../ООП/Свойства.md)
## Null-проверки
Для каждого ненулевого параметра публичной функции компилятор вставляет проверку:
```kotlin
fun nn(s: String) = s.length
fun nullable(s: String?) = s?.length
fun main() {
println(nn("текст")) // 5
println(nullable(null)) // null
}
```
Байт-код `nn`:
```
0: aload_0
1: ldc #9 // String s
3: invokestatic #15 // Intrinsics.checkNotNullParameter:(Object;String)V
6: aload_0
7: invokevirtual #21 // String.length:()I
10: ireturn
```
`checkNotNullParameter` бросает `NullPointerException` с сообщением «Parameter specified as non-null is null» - это и есть защита границы от Java-кода. Для `nullable` проверки нет, зато есть ветвление и упаковка в `Integer`:
```
public static final java.lang.Integer nullable(java.lang.String);
```
Возвращаемый тип `Int?` стал `Integer` - nullable-примитив всегда упакован.
Информация о nullability хранится в аннотациях:
```
public static final int nn(java.lang.String); @NotNull на параметре
public static final java.lang.Integer nullable(...); @Nullable на методе и параметре
```
Это `org.jetbrains.annotations.@NotNull`/`@Nullable` с `RuntimeInvisible`-удержанием. Их читают Java-IDE и другие JVM-языки. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
```mermaid
graph LR
A["String в Kotlin"] --> B["java.lang.String<br/>+ @NotNull<br/>+ checkNotNullParameter"]
C["String? в Kotlin"] --> D["java.lang.String<br/>+ @Nullable<br/>без проверки"]
E["Int в Kotlin"] --> F["int"]
G["Int? в Kotlin"] --> H["java.lang.Integer"]
```
## data class
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(1, 2)
println(p) // Point(x=1, y=2)
println(p.copy(y = 5)) // Point(x=1, y=5)
println(p == Point(1, 2)) // true
}
```
Генерируется:
```
private final int x;
private final int y;
public Point(int, int);
public final int getX();
public final int getY();
public final int component1();
public final int component2();
public final Point copy(int, int);
public static Point copy$default(Point, int, int, int, java.lang.Object);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
```
`component1()`/`component2()` нужны для деструктуризации - компилятор вызывает их по позиции. `copy$default` реализует значения по умолчанию: четвёртый параметр `int` - битовая маска переданных аргументов, пятый `Object` - технический. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
## Значения по умолчанию
```kotlin
class Service {
fun call(url: String, retries: Int = 3, timeout: Long = 1000L) =
"$url r=$retries t=$timeout"
}
fun main() {
println(Service().call("/api")) // /api r=3 t=1000
}
```
Два метода вместо одного:
```
public final java.lang.String call(java.lang.String, int, long);
public static java.lang.String call$default(Service, java.lang.String, int, long, int, java.lang.Object);
```
`$default` получает битовую маску: бит `i` установлен, если аргумент `i` **не** передан. Внутри он подставляет значение по умолчанию для каждого отсутствующего аргумента и вызывает основной метод. Отсюда два следствия: вызов со значениями по умолчанию стоит на один вызов дороже, а из Java `$default` недоступен (он синтетический и требует знания маски).
## Лямбды и функциональные типы
```kotlin
fun noCapture(): () -> Int = { 42 }
fun withCapture(n: Int): () -> Int = { n }
fun main() {
println(noCapture()()) // 42
println(withCapture(7)()) // 7
}
```
Функциональный тип `() -> Int` - это интерфейс `kotlin.jvm.functions.Function0<Integer>`. Их 23: `Function0` ... `Function22`.
```
public static final kotlin.jvm.functions.Function0<java.lang.Integer> noCapture();
0: invokedynamic #81, 0 // InvokeDynamic #0:invoke:()Lkotlin/jvm/functions/Function0;
```
Начиная с K2 лямбды компилируются через `invokedynamic` (как в Java), а не в отдельные классы `Foo$lambda$1`. Это уменьшает размер артефакта и ускоряет загрузку, но означает, что имя класса лямбды генерируется в рантайме - в стектрейсах появляются `$$Lambda$...`.
Важная деталь: `Function0<Integer>` работает с **упакованным** `Integer`. Лямбда, возвращающая `Int`, упаковывает результат на каждом вызове - если это горячий цикл, нужна `inline`-функция. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## inline
```kotlin
inline fun measure(block: () -> Unit) {
block()
}
fun useInline() {
measure { println("тело") }
}
fun main() {
useInline() // тело
}
```
Байт-код `useInline` не содержит вызова `measure` вообще:
```
public static final void useInline();
0: iconst_0
1: istore_0
2: iconst_0
3: istore_1
4: ldc #49 // String тело
6: getstatic #55 // System.out
9: swap
10: invokevirtual #61 // println
13: nop
14: nop
15: return
```
Тело лямбды подставлено прямо в метод: ни объекта `Function0`, ни вызова по ссылке. Это основа `forEach`, `let`, `map` и остальных операций stdlib - они не создают объектов. Цена - код дублируется в каждой точке вызова. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
Сама `measure` всё равно существует как метод (для Java-клиентов и рефлексии):
```
public static final void measure(kotlin.jvm.functions.Function0<kotlin.Unit>);
```
## suspend
```kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
suspend fun fetch(id: Int): String {
delay(10)
return "ответ $id"
}
fun main() = runBlocking {
println(fetch(1)) // ответ 1
}
```
Сигнатура меняется радикально:
```
public static final java.lang.Object fetch(int, kotlin.coroutines.Continuation<? super java.lang.String>);
```
Три изменения: добавлен параметр `Continuation`, тип возврата стал `Object` (вместо `String`), и само тело превратилось в конечный автомат. Функция может вернуть либо результат, либо специальный маркер `COROUTINE_SUSPENDED`, означающий «я приостановилась, результат придёт через `Continuation`».
Конечный автомат - это анонимный класс с полем `label`, хранящим номер точки приостановки, и полями для всех локальных переменных, живущих через приостановку. При возобновлении функция вызывается снова, читает `label` и прыгает в нужную ветку `switch`. Это CPS-преобразование (continuation-passing style), выполняемое компилятором.
Отсюда свойства корутин: приостановка не блокирует поток (нет никакого потока - есть объект состояния), стектрейсы короткие, а Java-код вызвать `suspend`-функцию практически не может. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md)
## value class
```kotlin
@JvmInline
value class UserId(val value: Int)
fun process(id: UserId) = "обработан ${id.value}"
fun main() {
println(process(UserId(42))) // обработан 42
}
```
Класс `UserId` в байт-коде существует, но в сигнатурах заменяется на обёрнутый тип:
```
public static final java.lang.String process-4ZD5Yi0(int);
```
Параметр стал `int` - обёртки в рантайме нет. Суффикс `-4ZD5Yi0` (хеш от типов) нужен, чтобы `process(UserId)` и `process(OrderId)` не конфликтовали: после стирания у обеих сигнатура `process(int)`.
Обёртка всё-таки создаётся в трёх случаях: когда value-класс используется как дженерик-параметр (`List<UserId>`), как nullable (`UserId?`) и при обращении через супертип. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
## Делегированные свойства
```kotlin
class Config {
val expensive: String by lazy { "вычислено" }
}
fun main() {
println(Config().expensive) // вычислено
}
```
```
private final kotlin.Lazy v$delegate;
public final java.lang.String getV();
```
Поле хранит не значение, а делегат. Геттер вызывает `delegate.getValue()`. Поэтому каждое делегированное свойство - это дополнительный объект и один лишний вызов на чтение. Для `by lazy` с дефолтным режимом внутри ещё и `synchronized`-блок при первом доступе. Подробнее: [Делегирование](../ООП/Делегирование.md)
## Дженерики и вариантность
```kotlin
class Box<T : Comparable<T>>(val value: T) {
fun max(other: T) = if (value > other) value else other
}
fun sizes(l: List<out Number>, m: MutableList<in Int>) = l.size + m.size
fun main() {
println(Box(3).max(7)) // 7
println(sizes(listOf(1, 2), mutableListOf(3))) // 3
}
```
```
public final class Box<T extends java.lang.Comparable<? super T>>
public static final int sizes(java.util.List<? extends java.lang.Number>, java.util.List<? super java.lang.Integer>);
```
Проекции Kotlin становятся wildcards Java: `out T` → `? extends T`, `in T` → `? super T`. Дженерики стираются так же, как в Java - в рантайме `Box<Int>` и `Box<String>` неразличимы. Обойти это позволяет только `reified` в `inline`-функциях, где тип подставляется при компиляции. Подробнее: [Вариантность](../Дженерики/Вариантность.md), [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## Nothing и Unit
```kotlin
fun fail(): Nothing = throw IllegalStateException("всегда падает")
fun act(): Unit = println("действие")
fun main() {
act() // действие
println(runCatching { fail() }.isFailure) // true
}
```
`Unit` как возвращаемый тип становится `void`. `Nothing` - `java.lang.Void` (значение которого всегда `null`, потому что функция никогда не возвращается нормально). В обоих случаях Kotlin-компилятор знает больше, чем байт-код: `Nothing` делает код после вызова недостижимым, но Java-компилятор этого не увидит. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
## Метаданные
Каждый Kotlin-класс несёт аннотацию `kotlin.Metadata` с закодированной информацией о nullable-типах, значениях по умолчанию, свойствах, `suspend`, вариантности, `inline`, `sealed` и прочем. Именно её читает Kotlin-компилятор, подключая скомпилированную библиотеку: `.class`-файла недостаточно, чтобы восстановить Kotlin-API.
Практические следствия:
**Обфускация ломает Kotlin-interop.** ProGuard/R8 должны сохранять `kotlin.Metadata`, иначе библиотека перестанет быть пригодной для Kotlin-клиентов (но останется пригодной для Java).
**Рефлексия Kotlin требует `kotlin-reflect`.** `KClass.memberProperties` читает метаданные, и для этого нужна отдельная зависимость весом ~2 МБ. Java-рефлексия (`javaClass.declaredFields`) работает без неё, но видит только байт-код. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
**Java-библиотека не знает про Kotlin-типы.** Jackson, Hibernate и подобные видят только `.class`: конструктор со значениями по умолчанию, nullable-поле как обычное. Для корректной работы нужны модули-адаптеры (`jackson-module-kotlin`), которые читают `kotlin.Metadata`.
## Сводная таблица
| Kotlin | Байт-код |
|---|---|
| `fun f()` на верхнем уровне | `static final` в `ФайлKt` |
| `object O` | класс + `static final O INSTANCE` |
| `companion object` | класс `Cls$Companion` + поле |
| `val x: T` | `private final T x` + `getX()` |
| `var x: T` | `private T x` + `getX()`/`setX()` |
| `lateinit var x` | `public T x` (поле публично) |
| `val x get() = ...` | только `getX()`, поля нет |
| `x: String` (параметр) | `String` + `@NotNull` + `checkNotNullParameter` |
| `x: String?` | `String` + `@Nullable` |
| `Int` | `int` |
| `Int?` | `java.lang.Integer` |
| `() -> Int` | `Function0<Integer>` |
| лямбда | `invokedynamic` |
| `inline fun` | тело подставлено + метод для Java |
| `suspend fun f(): T` | `Object f(Continuation)` + автомат |
| `@JvmInline value class` | обёрнутый тип + суффикс в имени |
| `by lazy` | поле `x$delegate: Lazy` |
| `out T` / `in T` | `? extends T` / `? super T` |
| `Unit` | `void` |
| `Nothing` | `java.lang.Void` |
| `internal fun f()` | `public final f$module()` |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | исходник ≈ байт-код | есть зазор, есть метаданные |
> | поля публичны, если так написано | поля всегда приватны (кроме `lateinit`) |
> | геттер пишется руками | генерируется |
> | лямбда → `invokedynamic` | то же (с K2) |
> | нет значений по умолчанию | синтетический `$default` |
> | нет `inline` | тело подставляется |
> | `async` через библиотеку | `suspend` → конечный автомат |
> | `int` и `Integer` в коде | `Int` и `Int?` |
>
> **Главное отличие - наличие метаданных.** В Java `.class` полностью описывает API: что видно в сигнатурах, то и есть. В Kotlin часть системы типов (nullability, значения по умолчанию, `suspend`, `inline`, `sealed`, вариантность) существует только в аннотации `kotlin.Metadata`. Это значит, что Kotlin-компилятор и Java-компилятор видят **разные** API одной библиотеки: первый богаче. Отсюда и необходимость сохранять метаданные при обфускации, и зависимость `kotlin-reflect`, и адаптеры для Jackson.
>
> **Проверки на null стоят вызова метода на каждый параметр.** В Java такой проверки нет вообще - `NullPointerException` возникает при первом использовании. Kotlin переносит сбой на границу функции, что делает диагностику проще (понятно, какой параметр виноват), но добавляет `invokestatic` на каждый ненулевой параметр публичной функции. В приватных и `inline`-функциях проверок нет: там компилятор уже гарантировал тип.
>
> **`inline` - возможность, которой в Java нет на уровне языка.** JIT в Java тоже встраивает вызовы, но это решение рантайма, непредсказуемое и не применимое к лямбдам с захватом. Kotlin встраивает на этапе компиляции, гарантированно и вместе с телом лямбды. Это и позволяет `collection.map { }` не создавать объектов, и даёт `reified`, и делает возможными `return` из лямбды. Цена - рост байт-кода и невозможность изменить тело без перекомпиляции клиентов.
>
> **`suspend` реализован компилятором, а не рантаймом.** В Java асинхронность требует либо потоков, либо `CompletableFuture` с цепочками колбэков, либо виртуальных потоков (Loom), которые меняют JVM. Kotlin делает CPS-преобразование на этапе компиляции и работает на любой JVM 8+. Следствие: корутины не требуют поддержки платформы, но и не интегрируются с Java-кодом.
>
> **Value-классы ближе к будущим Project Valhalla, чем к Java сегодня.** В Java любая обёртка над `int` - это объект в куче. Kotlin убирает обёртку на этапе компиляции, заменяя тип в сигнатурах. Цена - манглирование имён и обёртка в дженериках, где стирание не позволяет обойтись примитивом.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
> [!warning] Подводные камни
> **`lateinit var` создаёт публичное поле.** Это нарушает инкапсуляцию неожиданным образом: Java-код и рефлексия получают прямой доступ в обход геттера, минуя проверку инициализации. Если это важно, используйте `var x: T? = null` с приватным сеттером.
>
> **Nullable-примитив всегда упакован:**
> ```kotlin
> fun main() {
> val plain: Int = 1
> val nullable: Int? = 1
> println(plain == nullable) // true
> println(nullable!!::class.java.name) // java.lang.Integer
> val a: Int? = 1000
> val b: Int? = 1000
> println("identity: ${a === b}") // identity: false
> }
> ```
> `Int?` это `Integer`, поэтому `===` на значениях вне кеша `-128..127` даёт `false`. В горячем коде `Int?` в коллекциях и лямбдах создаёт объекты. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
>
> **Значения по умолчанию стоят лишнего вызова.** `call("/api")` идёт через `call$default`, который разбирает битовую маску и вызывает `call`. Для горячего пути с миллионами вызовов разница измерима - там пишут явные перегрузки.
>
> **`inline` раздувает байт-код.** Большая `inline`-функция, вызванная в сотне мест, копируется сто раз. Компилятор предупреждает про «expected performance impact... insignificant» для функций без лямбда-параметров - это намёк, что `inline` там не нужен.
>
> **Изменение тела `inline`-функции требует перекомпиляции клиентов.** Старое тело уже вкомпилировано в их байт-код. Для публичных библиотек это серьёзное ABI-ограничение: `inline` в публичном API фиксирует реализацию.
>
> **Делегированное свойство - это объект и вызов.** `by lazy` на каждом из сотни свойств даёт сотню объектов `Lazy` и сотню косвенных вызовов. Для простых случаев дешевле `private var x: T? = null` с проверкой в геттере.
>
> **Стектрейс корутины не показывает путь вызова.** Конечный автомат возобновляется из диспетчера, поэтому в стектрейсе видна только текущая точка приостановки. Для отладки включают `-Dkotlinx.coroutines.debug` или зависимость `kotlinx-coroutines-debug`. Подробнее: [Корутины](../Корутины/Корутины.md)
>
> **Обфускация без правил для `kotlin.Metadata` ломает библиотеку для Kotlin-клиентов.** Код продолжит работать, но исчезнет nullability, пропадут значения по умолчанию, `kotlin-reflect` перестанет видеть свойства. В правилах R8 нужно `-keep class kotlin.Metadata`.
>
> **`javap` не показывает Kotlin-API.** Он читает байт-код, не метаданные: `suspend`-функция выглядит как метод с `Continuation`, а `data class` - как обычный класс с кучей методов. Чтобы увидеть Kotlin-вид скомпилированного кода, нужен `kotlinp` или IDE («Show Kotlin Bytecode» → «Decompile»).
>
> **Синтетические методы видны рефлексии.** `component1()`, `copy$default`, `access$...`, `Foo$Companion` - всё это попадёт в `getDeclaredMethods()`. Код, перебирающий методы класса (сериализаторы, валидаторы), должен фильтровать по `isSynthetic()`.
>
> **`internal` не защищает.** Манглированное имя `helper$module` публично. Java-код в том же артефакте вызовет его без препятствий, как и любая рефлексия.
>
> **Проверки `checkNotNullParameter` можно отключить, но не стоит.** Флаг `-Xno-param-assertions` убирает их, давая небольшой выигрыш. Цена - `NullPointerException` будет возникать глубоко внутри вместо границы функции, и диагностика станет заметно хуже.
Далее: [Инструментарий](../Инструментарий/Инструментарий.md)
@@ -0,0 +1,525 @@
---
title: Kotlin из Java
description: Аннотации @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws и то, как конструкции Kotlin выглядят из Java
tags:
- kotlin
- java-interop
---
Обратное направление сложнее: в Kotlin есть конструкции, которых нет в JVM, и компилятор представляет их по-своему. Без аннотаций ваш API окажется неудобным для Java-кода.
```kotlin
class Greeter(val name: String) {
fun greet(greeting: String = "Привет") = "$greeting, $name!"
}
fun main() {
println(Greeter("Мир").greet()) // Привет, Мир!
}
```
Из Java этот же класс выглядит так:
```java
// Java
Greeter g = new Greeter("Мир");
System.out.println(g.greet("Привет")); // параметр обязателен!
// g.greet(); // не скомпилируется
```
Значение по умолчанию существует только для Kotlin: в байт-коде есть `greet(String)` и синтетический `greet$default(Greeter, String, int, Object)`, вызывать который вручную нельзя.
## Карта проблем и аннотаций
| Конструкция Kotlin | Из Java без аннотации | Аннотация |
|---|---|---|
| значение по умолчанию | один метод со всеми параметрами | `@JvmOverloads` |
| `companion object` | `Cls.Companion.method()` | `@JvmStatic` |
| `object` | `Singleton.INSTANCE.method()` | `@JvmStatic` |
| `val` в `companion object` | `Cls.Companion.getX()` | `@JvmField` |
| `const val` | уже поле - ничего не нужно | - |
| функция верхнего уровня | `FileNameKt.fn()` | `@JvmName` на файле |
| свойство | `getX()` / `setX()` | `@get:JvmName` |
| `suspend fun` | лишний параметр `Continuation` | не решается |
| `value class` | манглированное имя | не решается |
| исключение | не видно в `throws` | `@Throws` |
| перегрузки с дженериками | конфликт стирания | `@JvmName` |
```mermaid
graph TD
A["Kotlin API"] --> B{"Вызывается из Java?"}
B -->|"нет"| C["аннотации не нужны"]
B -->|"да"| D["проверить каждый пункт"]
D --> E["значения по умолчанию<br/>@JvmOverloads"]
D --> F["companion / object<br/>@JvmStatic"]
D --> G["константы<br/>@JvmField или const"]
D --> H["исключения<br/>@Throws"]
D --> I["имена<br/>@JvmName"]
```
## @JvmOverloads
Генерирует перегрузку для каждого значения по умолчанию:
```kotlin
class Greeter(val name: String) {
@JvmOverloads
fun greet(greeting: String = "Привет", punct: String = "!") = "$greeting, $name$punct"
}
fun main() {
val g = Greeter("Мир")
println(g.greet()) // Привет, Мир!
println(g.greet("Здравствуй")) // Здравствуй, Мир!
println(g.greet("Hi", ".")) // Hi, Мир.
}
```
В байт-коде появляются три метода:
```
public final String greet(String, String) // полный
public final String greet(String) // punct по умолчанию
public final String greet() // оба по умолчанию
```
Перегрузки создаются **справа налево**: параметры отбрасываются с конца. Поэтому `greet(punct = ".")` из Java невозможно - такой перегрузки нет. Это ограничение влияет на порядок параметров: самые часто переопределяемые ставьте первыми.
Для конструктора аннотация работает так же:
```kotlin
class Config @JvmOverloads constructor(
val host: String = "localhost",
val port: Int = 8080,
) {
override fun toString() = "$host:$port"
}
fun main() {
println(Config()) // localhost:8080
println(Config("example.com")) // example.com:8080
println(Config("example.com", 443)) // example.com:443
}
```
## @JvmStatic
Без неё члены `companion object` доступны только через `.Companion`:
```kotlin
class Repository private constructor(val id: String) {
companion object {
@JvmStatic
fun create(id: String) = Repository(id)
fun createNoStatic(id: String) = Repository(id)
}
}
fun main() {
println(Repository.create("1").id) // 1
println(Repository.createNoStatic("2").id) // 2
}
```
Из Kotlin обе формы одинаковы. Из Java - нет:
```java
// Java
Repository.create("1"); // работает: статический метод
Repository.Companion.createNoStatic("2"); // без @JvmStatic только так
```
То же для `object`:
```kotlin
object Registry {
private val items = mutableListOf<String>()
@JvmStatic
fun register(item: String) { items += item }
fun size() = items.size
}
fun main() {
Registry.register("a")
println(Registry.size()) // 1
}
```
Из Java: `Registry.register("a")` против `Registry.INSTANCE.size()`.
## @JvmField и const
Свойство в `companion object` компилируется в приватное поле с геттером на `Companion`. Три варианта:
```kotlin
class Limits {
companion object {
const val MAX = 100 // примитив/String, compile-time
@JvmField val DEFAULT = "значение" // поле, доступно напрямую
val COMPUTED = "вычисляется".uppercase() // только через геттер
}
}
fun main() {
println(Limits.MAX) // 100
println(Limits.DEFAULT) // значение
println(Limits.COMPUTED) // ВЫЧИСЛЯЕТСЯ
}
```
Из Java:
```java
// Java
Limits.MAX; // public static final int
Limits.DEFAULT; // public static final String
Limits.Companion.getCOMPUTED(); // только геттер
```
`const val` требует примитив или `String` и инициализацию константным выражением - зато встраивается в место вызова (и потому меняет ABI при изменении значения). `@JvmField` работает с любым типом, но несовместим с `private`, `open`, `override`, `lateinit` и делегированными свойствами.
## @JvmName
Задаёт имя в байт-коде. Три точки применения.
**Файл.** Функции верхнего уровня попадают в класс `ИмяФайлаKt`:
```kotlin
@file:JvmName("StringUtils")
fun String.slugify() = lowercase().replace(" ", "-")
fun main() {
println("Привет Мир".slugify()) // привет-мир
}
```
Без аннотации Java-код обращался бы к `SlugifyKt.slugify(s)`, с ней - `StringUtils.slugify(s)`. Расширения становятся статическими методами с получателем первым параметром.
**Аксессор свойства:**
```kotlin
class Account(private val raw: String) {
@get:JvmName("fetchBalance")
val balance: String get() = raw
}
fun main() {
println(Account("1000").balance) // 1000
}
```
Из Java метод называется `fetchBalance()`, а не `getBalance()`.
**Обход конфликта стирания.** Две функции, различающиеся только параметром дженерика, в байт-коде имеют одинаковую сигнатуру:
```kotlin
@JvmName("sumInts")
fun List<Int>.sumAll() = sum()
@JvmName("sumDoubles")
fun List<Double>.sumAll() = sum()
fun main() {
println(listOf(1, 2, 3).sumAll()) // 6
println(listOf(1.5, 2.5).sumAll()) // 4.0
}
```
Без `@JvmName` компилятор откажется: «platform declaration clash». Это одна из немногих ситуаций, когда аннотация нужна даже для чисто Kotlin-кода. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## @Throws
В Kotlin нет проверяемых исключений, поэтому в байт-коде нет `throws`:
```kotlin
import java.io.IOException
class Loader {
@Throws(IOException::class)
fun load(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "данные из $path"
}
fun loadNoThrows(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "данные из $path"
}
}
fun main() {
val loader = Loader()
println(loader.load("/tmp/file")) // данные из /tmp/file
println(runCatching { loader.loadNoThrows("") }.isFailure) // true
}
```
Из Java разница принципиальна:
```java
// Java
try { loader.load(""); }
catch (IOException e) { } // компилируется
try { loader.loadNoThrows(""); }
catch (IOException e) { } // ОШИБКА: exception is never thrown
```
Java-компилятор запрещает ловить проверяемое исключение, которое метод не объявляет. Исключение всё равно вылетит в рантайме - просто обработать его можно только через `catch (Exception e)`.
## Как выглядят остальные конструкции
### Nullability
```kotlin
class Validator {
fun check(input: String): Boolean = input.isNotEmpty()
fun find(key: String): String? = if (key == "a") "найдено" else null
}
fun main() {
val v = Validator()
println(v.check("текст")) // true
println(v.find("b")) // null
}
```
`check(String)` получает аннотацию `@NotNull` на параметре и проверку `Intrinsics.checkNotNullParameter` в начале тела. Java-код, передавший `null`, получит `NullPointerException` с сообщением «Parameter specified as non-null is null» - сразу, а не при первом использовании. `find` возвращает `@Nullable String`, и IDE предупредит Java-код.
### Свойства
```kotlin
class User {
var name: String = "аноним"
val id: Int = 1
var internalState: String = "скрыто"
private set
}
fun main() {
val u = User()
u.name = "Иван"
println("${u.name} ${u.id} ${u.internalState}") // Иван 1 скрыто
}
```
Из Java: `getName()`/`setName()`, `getId()` без сеттера, `getInternalState()` без сеттера (приватный сеттер в байт-коде приватен). Само поле всегда приватно - прямого доступа из Java нет.
### Функции-расширения
```kotlin
fun List<String>.firstOrDefault(default: String) = firstOrNull() ?: default
fun main() {
println(listOf("a").firstOrDefault("нет")) // a
println(emptyList<String>().firstOrDefault("нет")) // нет
}
```
Из Java это статический метод с получателем первым аргументом: `FileNameKt.firstOrDefault(list, "нет")`. Никакого синтаксического сахара - расширение существует только в Kotlin.
### suspend-функции
```kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
suspend fun fetch(id: Int): String {
delay(10)
return "ответ для $id"
}
fun main() = runBlocking {
println(fetch(1)) // ответ для 1
}
```
В байт-коде сигнатура становится `Object fetch(int, Continuation)`, возвращая либо результат, либо маркер `COROUTINE_SUSPENDED`. Вызвать такое из Java практически невозможно. Для Java-клиентов делают обёртку:
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture
class Api(private val scope: CoroutineScope) {
suspend fun fetch(id: Int): String {
delay(10)
return "ответ для $id"
}
// для Java: CompletableFuture вместо suspend
fun fetchAsync(id: Int): CompletableFuture<String> =
scope.future { fetch(id) }
}
fun main() {
val scope = CoroutineScope(Dispatchers.Default)
println(Api(scope).fetchAsync(7).get()) // ответ для 7
}
```
Нужна зависимость `kotlinx-coroutines-jdk8`. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md)
### value class
```kotlin
@JvmInline
value class UserId(val value: Int)
fun process(id: UserId) = "обработан ${id.value}"
fun main() {
println(process(UserId(42))) // обработан 42
}
```
В байт-коде `process` получает имя `process-4ZD5Yi0(int)` - суффикс нужен, чтобы отличать перегрузки с разными value-классами над одним примитивом. Вызвать такое имя из Java нельзя: дефис недопустим в идентификаторе. Для Java-API добавляют обычную перегрузку:
```kotlin
@JvmInline
value class Money(val cents: Long)
fun charge(amount: Money) = "списано ${amount.cents}"
@JvmName("chargeCents")
fun charge(cents: Long) = charge(Money(cents))
fun main() {
println(charge(Money(500))) // списано 500
println(charge(500L)) // списано 500
}
```
Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
### internal
```kotlin
class Service {
internal fun helper() = "вспомогательное"
fun public() = helper()
}
fun main() {
println(Service().public()) // вспомогательное
}
```
`internal` компилируется в `public` с манглированным именем вида `helper$main` (суффикс - имя модуля). Java-код технически может вызвать `helper$main()`. Это не изоляция, а соглашение, которое соблюдает только Kotlin-компилятор. Подробнее: [Модификаторы видимости](../ООП/Модификаторы%20видимости.md)
### data class
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(1, 2)
println(p) // Point(x=1, y=2)
println(p.copy(y = 5)) // Point(x=1, y=5)
val (x, y) = p
println("$x $y") // 1 2
}
```
Из Java доступны `getX()`, `getY()`, `equals`, `hashCode`, `toString`, `component1()`, `component2()` и `copy(int, int)`. Но `copy` с одним аргументом недоступна - это те же значения по умолчанию, и `@JvmOverloads` на сгенерированном `copy` поставить нельзя. Деструктуризации в Java нет. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
### sealed
```kotlin
sealed interface Result
data class Ok(val value: String) : Result
data class Err(val message: String) : Result
fun describe(r: Result) = when (r) {
is Ok -> "успех: ${r.value}"
is Err -> "ошибка: ${r.message}"
}
fun main() {
println(describe(Ok("данные"))) // успех: данные
println(describe(Err("сбой"))) // ошибка: сбой
}
```
Из Java `sealed interface` Kotlin - обычный интерфейс (при JVM-таргете 17+ может компилироваться в настоящий `sealed`). Исчерпывающего `switch` Java не получит - гарантия полноты обеспечивается только Kotlin-компилятором. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Чеклист публичного API для Java-клиентов
1. `@JvmOverloads` на всех функциях со значениями по умолчанию.
2. `@JvmStatic` на членах `companion object` и `object`.
3. `const val` или `@JvmField` для констант.
4. `@Throws` там, где Java-код должен ловить проверяемое исключение.
5. `@file:JvmName` для файлов с функциями верхнего уровня.
6. Нет `suspend` в публичном API - только обёртки (`CompletableFuture`, колбэки).
7. Нет `value class` в сигнатурах - или перегрузка с `@JvmName`.
8. Возвращать `List`, а не `MutableList`, и отдавать копию, если Java может изменить.
9. Nullable-типы объявлять явно - Java-IDE прочитает аннотации.
10. Проверить результат через `javap` на реальном классе.
```kotlin
// пример API, готового к вызову из Java
@file:JvmName("UserApi")
class UserService @JvmOverloads constructor(
private val baseUrl: String = "https://api.example.com",
) {
@Throws(IllegalArgumentException::class)
@JvmOverloads
fun find(id: String, includeDeleted: Boolean = false): String {
require(id.isNotBlank()) { "id не может быть пустым" }
return "$baseUrl/users/$id?deleted=$includeDeleted"
}
companion object {
const val DEFAULT_TIMEOUT_MS = 5_000L
@JvmStatic
fun default() = UserService()
}
}
fun main() {
println(UserService.default().find("42"))
println(UserService.DEFAULT_TIMEOUT_MS)
// https://api.example.com/users/42?deleted=false
// 5000
}
```
## Проверка результата
Единственный надёжный способ увидеть реальный API - посмотреть байт-код:
```
kotlinc Api.kt -d out
javap -cp out com.example.UserApi
javap -cp out 'com.example.UserService$Companion'
```
`javap` покажет точные сигнатуры, включая синтетические методы `$default` и манглированные имена. Это стоит делать перед релизом библиотеки. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
> [!info] Отличия от Java
> | Java | Kotlin из Java |
> |---|---|
> | `static` метод | `@JvmStatic` в `companion object` |
> | `public static final` поле | `const val` или `@JvmField` |
> | перегрузки пишутся руками | `@JvmOverloads` генерирует |
> | `throws` в сигнатуре | `@Throws` |
> | имя класса = имя файла | `@file:JvmName` |
> | утилитный класс со `static` | функции верхнего уровня |
> | геттер пишется руками | генерируется из свойства |
>
> **Аннотации `@Jvm*` - это не костыли, а явное управление ABI.** В Java представление в байт-коде однозначно: что написано, то и скомпилируется. В Kotlin между исходником и байт-кодом есть зазор, и аннотации дают над ним контроль. Плата за выразительность языка: `@JvmOverloads` существует потому, что в JVM нет значений по умолчанию, а не из-за недоработки.
>
> **Значения по умолчанию в Kotlin устроены иначе, чем перегрузки в Java.** В Java N необязательных параметров требуют N перегрузок, каждую с делегированием. В Kotlin - один метод и синтетический `$default`, принимающий битовую маску переданных аргументов. Отсюда и ограничение `@JvmOverloads`: перегрузки генерируются только отбрасыванием с конца, потому что произвольные комбинации дали бы экспоненциальное число методов.
>
> **`companion object` - настоящий объект, а не пространство имён.** В Java `static` - модификатор: метод принадлежит классу. В Kotlin companion - отдельный экземпляр класса `Cls$Companion`, у него есть тип, он может реализовывать интерфейсы и быть получателем расширений. Поэтому без `@JvmStatic` вызов идёт через `Cls.Companion`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
>
> **Отсутствие проверяемых исключений нельзя «включить» обратно.** `@Throws` только добавляет запись в атрибут `Exceptions` метода - сам Kotlin-компилятор её не проверяет. Фактически это документация для Java-компилятора: без неё Java не разрешит `catch`, с ней - потребует обработки.
>
> **`suspend` принципиально несовместим с Java.** Корутины реализованы через CPS-преобразование на этапе компиляции Kotlin: функция превращается в конечный автомат с параметром `Continuation`. Java-компилятор этого преобразования не делает, поэтому написать Java-вызывающий код для `suspend`-функции нельзя - только обёртки. Это самая жёсткая граница interop. Подробнее: [Корутины против потоков](../Корутины/Корутины%20против%20потоков.md)
>
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `fun f(a: Int = 1, b: Int = 2, c: Int = 3)` появятся `f(a,b,c)`, `f(a,b)`, `f(a)`, `f()` - но не `f(c)` и не `f(a,c)`. Из Java нельзя передать только третий параметр. Ставьте чаще переопределяемые параметры раньше.
>
> **Добавление `@JvmStatic` или `@JvmOverloads` позже ломает совместимость.** `@JvmStatic` не удаляет метод с `Companion`, но меняет рекомендуемую точку входа; снятие аннотации удаляет статический метод и роняет скомпилированный Java-код с `NoSuchMethodError`. Решайте до релиза.
>
> **Изменение `const val` требует перекомпиляции клиентов.** Значение встраивается в место вызова, поэтому Java-код, собранный против старой версии, продолжит использовать старое число. Для значений, которые могут меняться, лучше `@JvmField val`.
>
> **`@JvmField` несовместим с половиной модификаторов:**
> ```kotlin
> class Limits {
> companion object {
> @JvmField val OK = "работает"
> // @JvmField private val BAD = "" // ошибка: не может быть private
> // @JvmField lateinit var LATE: String // ошибка: не для lateinit
> }
> }
>
> fun main() {
> println(Limits.OK) // работает
> }
> ```
> Запрещено для `private`, `open`, `override`, `const`, `lateinit` и делегированных свойств.
>
> **`copy()` у `data class` из Java требует все аргументы.** `@JvmOverloads` на сгенерированный `copy` поставить нельзя. Если Java-коду нужна частичная копия, пишите свой метод или билдер.
>
> **Манглированные имена value-классов недоступны из Java.** `fun takeId(id: UserId)` компилируется в `takeId-4ZD5Yi0(int)`; дефис делает имя невызываемым в Java. Это проявится только при попытке интеграции - Kotlin-компилятор не предупредит.
>
> **`internal` виден из Java.** `internalFun$main()` - публичный метод в байт-коде. Если нужна настоящая закрытость для Java-кода в том же проекте, используйте `private` или выносите в отдельный модуль.
>
> **`List<T>` из Kotlin в Java имеет `add()`.** Тип стирается до `java.util.List`, и Java-код может вызвать мутирующий метод. Падение будет в рантайме с `UnsupportedOperationException`. Для публичного API возвращайте `Collections.unmodifiableList(...)` или копию.
>
> **Платформенные объявления конфликтуют при стирании:**
> ```kotlin
> @JvmName("countStrings")
> fun List<String>.count2() = size
>
> @JvmName("countNumbers")
> fun List<Int>.count2() = size
>
> fun main() {
> println(listOf("a").count2()) // 1
> println(listOf(1, 2).count2()) // 2
> }
> ```
> Без `@JvmName` будет ошибка компиляции «platform declaration clash» - обе функции дают `count2(List)`. Это проявляется и в чисто Kotlin-проекте.
>
> **`@file:JvmName` должна стоять до `package`.** Файловые аннотации идут самой первой строкой файла, иначе компилятор их не примет.
>
> **`@get:JvmName` на свойстве в интерфейсе или `open` классе запрещена.** Переименование аксессора сломало бы переопределение, поэтому компилятор его не допускает для `open`/`override` членов.
>
> **Делегированные свойства из Java выглядят как обычные.** `by lazy` даёт `getX()`, но поле хранит `Lazy<T>` под именем `x$delegate`. Прямого доступа к значению нет, и `@JvmField` не применить. Подробнее: [Делегирование](../ООП/Делегирование.md)
>
> **Расширения не переопределяются и не видны как методы.** `fun Foo.bar()` из Java - это `FileKt.bar(foo)`. Java-код не найдёт `foo.bar()`, а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса.
Далее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
@@ -0,0 +1,202 @@
---
title: Взаимодействие с Java
description: Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел
tags:
- kotlin
- java-interop
---
Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен.
```kotlin
import java.time.LocalDate
import java.util.UUID
fun main() {
val id = UUID.randomUUID()
val today = LocalDate.of(2026, 1, 15)
println(id.toString().length) // 36
println(today.plusDays(30)) // 2026-02-14
println(today.dayOfWeek) // THURSDAY
}
```
Это обычный Java API, вызванный как родной Kotlin-код: никаких обёрток, конвертеров и объявлений внешних типов.
## Что происходит без усилий
| Что | Как работает |
|---|---|
| вызов Java-метода из Kotlin | как обычный метод |
| геттеры/сеттеры Java | как свойства Kotlin |
| Java-коллекции | как `MutableList`, `MutableMap` и т.д. |
| наследование от Java-класса | как от Kotlin-класса |
| реализация Java-интерфейса | SAM-конверсия из лямбды |
| Java-аннотации | применяются к Kotlin-коду |
| смешанная компиляция в одном модуле | поддерживается Gradle-плагином |
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
executor.submit { println("Runnable из лямбды") } // SAM-конверсия
executor.shutdown()
val list = java.util.ArrayList<String>()
list.add("элемент") // как MutableList
println(list.size) // 1
val builder = StringBuilder()
builder.append("a").append("b")
println(builder.length) // 2
// Runnable из лямбды
}
```
## Где возникает трение
Три системы типов Kotlin не имеют соответствия в Java, и именно на этих границах появляются сложности.
**1. Null-безопасность.** Java не различает `String` и `String?`, поэтому любое значение из Java - платформенный тип `String!`:
```kotlin
fun main() {
// Java-метод вернул null, а Kotlin считал тип ненулевым
val value: String = System.getProperty("несуществующее.свойство")
?: "значение по умолчанию"
println(value) // значение по умолчанию
}
```
Без `?:` здесь был бы `NullPointerException` в момент присваивания. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
**2. Изменяемость коллекций.** В Kotlin `List` и `MutableList` - разные типы, в Java это один `java.util.List`:
```kotlin
fun main() {
val immutable: List<String> = listOf("a", "b")
// передача в Java-метод, который может вызвать add()
val asJava: java.util.List<String> = immutable as java.util.List<String>
val error = runCatching { asJava.add("c") }.exceptionOrNull()
println("неизменяемый список отверг изменение: ${error != null}") // неизменяемый список отверг изменение: true
}
```
Java-код не видит разницы и может попытаться изменить список, который Kotlin считает неизменяемым. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
**3. Возможности Kotlin, которых нет в Java.** Значения по умолчанию, функции-расширения, `suspend`, value-классы, `object` - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации `@JvmStatic`, `@JvmOverloads`, `@JvmName` и другие. Подробнее: [Kotlin из Java](Kotlin%20из%20Java.md)
## Асимметрия направлений
Направления не равноценны - и это главное, что нужно понять про interop.
```mermaid
graph LR
A["Java-код"] -->|"почти без трения:<br/>только null-безопасность"| B["Kotlin-код"]
B -->|"требует аннотаций:<br/>@JvmStatic, @JvmOverloads,<br/>@JvmName, @JvmField"| A
```
**Java из Kotlin** работает почти прозрачно: язык создавался именно для этого. Единственная серьёзная проблема - платформенные типы, то есть риск `NullPointerException` там, где компилятор уверен в ненулевом значении.
**Kotlin из Java** требует внимания: без аннотаций ваш API окажется неудобным - значения по умолчанию превратятся в один метод с лишними параметрами, `companion object` потребует обращения через `.Companion`, а `suspend`-функция получит параметр `Continuation`.
Практический вывод: если ваш Kotlin-код вызывается из Java, его публичный API нужно проектировать осознанно. Если Kotlin-код вызывается только из Kotlin, аннотации не нужны вообще.
## Карта раздела
| Заметка | О чём |
|---|---|
| [Java из Kotlin](Java%20из%20Kotlin.md) | платформенные типы, SAM, массивы, статика, синтетические свойства |
| [Kotlin из Java](Kotlin%20из%20Java.md) | `@JvmStatic`, `@JvmOverloads`, `@JvmName`, `@JvmField`, `@Throws` |
| [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) | во что компилируются свойства, лямбды, `data class`, `suspend` |
## Смешанный проект
Gradle-плагин Kotlin компилирует Java и Kotlin вместе, разрешая циклические зависимости между языками:
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
java
}
kotlin {
jvmToolchain(21)
}
// Kotlin и Java в одном модуле:
// src/main/kotlin/ и src/main/java/ — компилируются совместно
```
Порядок компиляции: сначала Kotlin видит Java-исходники (через стабы), затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java-класс может наследоваться от Kotlin-класса, который реализует Java-интерфейс. Подробнее: [Структура проекта](../Инструментарий/Структура%20проекта.md)
## Стратегия миграции
Типичный путь для существующего Java-проекта:
1. **Новый код на Kotlin, старый не трогать.** Interop позволяет это без прослоек.
2. **Конвертировать по файлу**, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей.
3. **Проверять результат автоконвертера.** IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable, `!!` вместо `?.`, классы вместо `data class`.
4. **Публичный API, вызываемый из Java, аннотировать** сразу - иначе придётся менять его потом.
5. **Тесты конвертировать последними или не конвертировать** - они и так работают.
```kotlin
// типичный результат автоконвертера — работает, но это не идиоматичный Kotlin
class UserDtoConverted {
var name: String? = null
var age: Int? = null
fun getDisplayName(): String {
return if (name != null) name!! else "аноним"
}
}
// идиоматичная версия
data class UserDto(val name: String?, val age: Int?) {
val displayName: String get() = name ?: "аноним"
}
fun main() {
println(UserDtoConverted().getDisplayName()) // аноним
println(UserDto(null, null).displayName) // аноним
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | один язык, одна система типов | две системы, граница между ними |
> | `String` может быть `null` | `String` и `String?` различаются |
> | `List` один тип | `List` и `MutableList` |
> | `int` и `Integer` различаются | `Int` и `Int?` |
> | статические методы | `companion object` + `@JvmStatic` |
> | нет аналога | платформенные типы `String!` |
> | checked exceptions обязательны | проверяемых исключений нет |
>
> **Interop работает, потому что Kotlin компилируется в тот же байт-код.** Kotlin-класс - это обычный класс JVM: у него те же модификаторы доступа, то же наследование, тот же формат `.class`. Никакой прослойки, маршалинга или рефлексии в runtime нет, поэтому вызов Kotlin-метода из Java стоит ровно столько же, сколько вызов Java-метода. Это принципиально отличает ситуацию от interop между языками разных платформ (JNI, JNA), где границу приходится пересекать с накладными расходами.
>
> **Цена - необходимость представлять конструкции Kotlin средствами JVM.** В байт-коде нет ни nullable-типов, ни значений по умолчанию, ни функций-расширений. Kotlin эмулирует их: nullable-информацию хранит в аннотациях и метаданных, значения по умолчанию - в синтетическом методе `greet$default`, расширения - в статических методах. Из Kotlin это видно как родные конструкции (компилятор читает метаданные), из Java - как реализация. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
>
> **Проверяемые исключения - односторонняя потеря.** В Kotlin их нет: `throws IOException` из Java-метода не обязывает ни к `try/catch`, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без `@Throws`. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
>
> **Null-безопасность Kotlin не распространяется на Java-код.** Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации `@Nullable`/`@NotNull` в Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
> [!warning] Подводные камни
> **Платформенные типы отключают проверки компилятора.** Значение из Java имеет тип `String!`, который компилятор позволяет использовать и как `String`, и как `String?`. Ошибка проявится как `NullPointerException` в рантайме, в точке присваивания:
> ```kotlin
> fun main() {
> val error = runCatching {
> val value: String = System.getProperty("нет.такого") // вернёт null
> value.length
> }.exceptionOrNull()
> println("NPE от платформенного типа: ${error is NullPointerException}")
> // NPE от платформенного типа: true
> }
> ```
>
> **Автоконвертер даёт работающий, но плохой код.** Типичные следы: `!!` вместо `?.`, `var x: T? = null` вместо конструктора, геттеры вместо свойств, обычные классы вместо `data class`. После конвертации файл нужно переписывать вручную, иначе Kotlin-проект унаследует все недостатки Java-кода и не получит преимуществ.
>
> **Java-код может нарушить неизменяемость Kotlin-коллекции.** `List<String>` из Kotlin приходит в Java как `java.util.List`, у которого есть `add()`. Вызов упадёт с `UnsupportedOperationException` в рантайме, а компилятор об этом не предупредит. Передавайте в Java копию (`toList()`) или `Collections.unmodifiableList`.
>
> **Kotlin-код без аннотаций неудобен из Java.** Значения по умолчанию, `companion object`, `object`, `suspend`-функции и функции верхнего уровня выглядят из Java не так, как задумано. Если API публичный, это нужно решать до релиза - добавление `@JvmStatic` позже меняет сигнатуры и ломает совместимость.
>
> **`internal` в Kotlin из Java доступен.** Он компилируется в `public` с манглированным именем (`internalFun$main`), поэтому Java-код в том же проекте технически может его вызвать. Это не защита, а соглашение.
>
> **Проверяемые исключения из Kotlin не видны Java.** Kotlin-метод, бросающий `IOException`, из Java выглядит как не бросающий ничего - `catch (IOException e)` вокруг него не скомпилируется («exception is never thrown»). Нужна аннотация `@Throws`.
>
> **Смешанная компиляция требует правильного плагина.** В Gradle нужен `kotlin("jvm")` и, при наличии Java-исходников, корректно настроенный `sourceSets`. Без этого Java-код не увидит Kotlin-классы, а ошибка выглядит как «cannot find symbol».
>
> **Generics Java стираются и в Kotlin.** `List<String>` и `List<Int>` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
Далее: [Java из Kotlin](Java%20из%20Kotlin.md)
@@ -0,0 +1,359 @@
---
title: Reified и inline
description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет
tags:
- kotlin
- generics
---
На JVM параметры типа стираются: в рантайме `List<Int>` и `List<String>` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`.
```kotlin
inline fun <reified T> isOfType(value: Any): Boolean = value is T
fun main() {
println(isOfType<String>("текст")) // true
println(isOfType<Int>("текст")) // false
println(isOfType<List<*>>(listOf(1))) // true
}
```
Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно.
## Что делает компилятор
```kotlin
inline fun <reified T> nameOf(): String = T::class.simpleName ?: "?"
fun main() {
println(nameOf<String>()) // String
println(nameOf<Int>()) // Int
}
```
В байт-коде вызова `nameOf<String>()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего.
Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически.
## Главный случай: парсинг и десериализация
Задача, ради которой `reified` чаще всего и используют:
```kotlin
inline fun <reified T> parseAs(raw: String): T? = when (T::class) {
Int::class -> raw.toIntOrNull() as T?
Long::class -> raw.toLongOrNull() as T?
Double::class -> raw.toDoubleOrNull() as T?
Boolean::class -> raw.toBooleanStrictOrNull() as T?
String::class -> raw as T
else -> null
}
fun main() {
println(parseAs<Int>("42")) // 42
println(parseAs<Int>("abc")) // null
println(parseAs<Double>("1.5")) // 1.5
println(parseAs<Boolean>("true")) // true
println(parseAs<String>("текст")) // текст
}
```
В Java такая функция потребовала бы параметр `Class<T> type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun <reified T> readValue(json: String): T`, и тип выводится из контекста:
```kotlin
// С kotlinx.serialization или Jackson Kotlin module:
// val user: User = mapper.readValue(json) // тип берётся из объявления переменной
```
## filterIsInstance и прочее в stdlib
```kotlin
fun main() {
val mixed: List<Any> = listOf(1, "a", 2.0, 3, "b")
println(mixed.filterIsInstance<Int>()) // [1, 3]
println(mixed.filterIsInstance<String>()) // [a, b]
println(mixed.filterIsInstance<Number>()) // [1, 2.0, 3]
val target = mutableListOf<Int>()
mixed.filterIsInstanceTo(target)
println(target) // [1, 3]
}
```
`filterIsInstance` объявлена как `inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>` - без `reified` поэлементная проверка типа была бы невозможна.
Другие `reified`-функции stdlib:
```kotlin
fun main() {
val array = arrayOf(1, 2, 3) // arrayOf — reified
println(listOf("a", "b").toTypedArray().size) // 2 — тоже reified
println(array.javaClass.simpleName) // Integer[]
val typed: Array<String?> = arrayOfNulls(2)
println(typed.size) // 2
println(emptyArray<Int>().size) // 0
}
```
`toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array<T>` в рантайме есть настоящий тип элементов.
## Безопасное приведение
```kotlin
inline fun <reified T> Any?.asOrNull(): T? = this as? T
fun main() {
val value: Any = "текст"
println(value.asOrNull<String>()?.length) // 5
println(value.asOrNull<Int>()) // null — без исключения
}
```
Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции.
Разбор `Map<String, Any>` - типичная задача при работе с конфигами и JSON:
```kotlin
inline fun <reified T> Map<String, Any>.getTyped(key: String): T? = this[key] as? T
fun main() {
val config = mapOf<String, Any>("port" to 8080, "host" to "localhost", "tls" to true)
println(config.getTyped<Int>("port")) // 8080
println(config.getTyped<String>("host")) // localhost
println(config.getTyped<Int>("host")) // null — тип не тот
println(config.getTyped<Int>("нет")) // null — ключа нет
}
```
## reified и Java-рефлексия
```kotlin
inline fun <reified T : Any> newInstance(): T = T::class.java.getDeclaredConstructor().newInstance()
class Service2 {
override fun toString() = "сервис создан"
}
fun main() {
val service: Service2 = newInstance()
println(service) // сервис создан
}
```
`T::class` даёт `KClass<T>`, `T::class.java` - `Class<T>`. Это мост к любому Java API, требующему `Class`:
```kotlin
inline fun <reified T> typeName(): String = T::class.java.name
fun main() {
println(typeName<String>()) // java.lang.String
println(typeName<Int>()) // java.lang.Integer — не int!
println(typeName<List<String>>()) // java.util.List
println(Int::class.java.name) // int — а так примитив
}
```
Две детали из этого примера:
- `T::class` знает только стёртый тип: `List<String>` и `List<Int>` дадут одинаковый результат.
- Параметр типа всегда представлен ссылочным типом, поэтому `T = Int` даёт `java.lang.Integer`, хотя написанный напрямую `Int::class.java` возвращает примитивный `int`.
## Что происходит с inline
`reified` - побочный эффект `inline`, а у `inline` есть своя основная задача: убрать объект лямбды.
```kotlin
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
fun main() {
val time = measure { (1..1000).sum() }
println(time > 0) // true
}
```
Тело `block` подставляется прямо в `main` - объекта `Function0` не создаётся. Подробнее об этом и о `noinline`/`crossinline`: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
Для `reified` важно только одно следствие inline: **функция перестаёт существовать как метод**, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку.
## Ограничения reified
```kotlin
inline fun <reified T> demo(): String {
val cls = T::class // можно
val arr = arrayOfNulls<T>(1) // можно
// val instance = T() // нельзя: нет конструктора в типе
// val companion = T.Companion // нельзя
return "${cls.simpleName} ${arr.size}"
}
fun main() {
println(demo<String>()) // String 1
}
```
| Можно с `reified T` | Нельзя |
|---|---|
| `value is T`, `value as? T` | `T()` - создать экземпляр |
| `T::class`, `T::class.java` | обращаться к статическим членам `T` |
| `arrayOfNulls<T>()`, `arrayOf<T>()` | различать `List<Int>` и `List<String>` |
| передать `T` в другую `reified`-функцию | вызвать из Java |
| вывести `T` из типа переменной | передать тип как значение в рантайме |
Создание экземпляра требует рефлексии:
```kotlin
inline fun <reified T : Any> createOrNull(): T? = runCatching {
T::class.java.getDeclaredConstructor().newInstance()
}.getOrNull()
class WithDefault { override fun toString() = "создан" }
class WithoutDefault(val x: Int)
fun main() {
println(createOrNull<WithDefault>()) // создан
println(createOrNull<WithoutDefault>()) // null — нет пустого конструктора
}
```
## Проброс reified дальше
```kotlin
inline fun <reified T> logType(value: Any) {
println("${T::class.simpleName}: ${check2<T>(value)}")
}
inline fun <reified T> check2(value: Any): Boolean = value is T
fun main() {
logType<String>("текст") // String: true
logType<Int>("текст") // Int: false
}
```
`reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет:
```kotlin
fun <T> notReified(value: Any): Boolean = value != null // T здесь стёрт
inline fun <reified T> caller(value: Any): Boolean = notReified<T>(value)
fun main() {
println(caller<String>("текст")) // true — но проверки типа внутри нет
}
```
## Альтернатива без reified
Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно:
```kotlin
fun <T : Any> parseWith(raw: String, type: kotlin.reflect.KClass<T>): T? = when (type) {
Int::class -> raw.toIntOrNull() as T?
String::class -> raw as T
else -> null
}
fun main() {
println(parseWith("42", Int::class)) // 42
println(parseWith("текст", String::class)) // текст
}
```
Это ровно Java-подход с `Class<T>`. Он многословнее в вызове, зато функция остаётся обычным методом.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `<T> T parse(String s, Class<T> type)` | `inline fun <reified T> parse(s: String): T` |
> | `parse(json, User.class)` | `parse<User>(json)` или `val u: User = parse(json)` |
> | `x instanceof T` - невозможно | `x is T` с `reified` |
> | `new T[]` - запрещён | `arrayOf<T>()` с `reified` |
> | `T.class` - невозможно | `T::class` с `reified` |
> | нет inline-функций | `inline` + подстановка на этапе компиляции |
>
> **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java:
> ```java
> User user = mapper.readValue(json, User.class);
> List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
> ```
> Kotlin-модуль того же Jackson:
> ```kotlin
> // val user: User = mapper.readValue(json)
> // val users: List<User> = mapper.readValue(json)
> ```
> Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной.
>
> **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа:
> ```kotlin
> inline fun <reified T> describeType(): String = T::class.simpleName ?: "?"
>
> fun main() {
> println(describeType<List<String>>()) // List — параметр потерян
> println(describeType<List<Int>>()) // List — то же самое
> }
> ```
> Для полного типа с аргументами нужен `typeOf<T>()` из `kotlin.reflect`, и он требует зависимости `kotlin-reflect`.
>
> **Inline-функций в Java нет.** JVM инлайнит методы на уровне JIT в рантайме, но это оптимизация, невидимая системе типов. Kotlin инлайнит на этапе компиляции, что даёт две вещи сразу: отсутствие объекта лямбды и `reified`. Цена - рост размера байт-кода и невозможность вызвать такую функцию из Java.
>
> **`filterIsInstance` аналога в Java не имеет.** Эквивалент - `stream().filter(X.class::isInstance).map(X.class::cast)`, где класс указывается дважды.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **`reified` без `inline` не компилируется** - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать `KClass<T>` вручную.
>
> **Тип всё равно стёрт внутри аргументов:**
> ```kotlin
> inline fun <reified T> isList(value: Any): Boolean = value is T
>
> fun main() {
> println(isList<List<String>>(listOf(1, 2))) // true — хотя это List<Int>!
> }
> ```
> `is List<String>` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа.
>
> **`as T` внутри reified-функции не всегда проверяется:**
> ```kotlin
> inline fun <reified T> forceCast(value: Any): T = value as T
>
> fun main() {
> val list: List<String> = forceCast(listOf(1, 2)) // прошло!
> println(list.size) // 2
> try {
> println(list[0].length)
> } catch (e: ClassCastException) {
> println("упало только при обращении к элементу")
> }
> }
> ```
> Каст к `List<String>` сводится к касту к `List`, который успешен. Ошибка всплывает позже.
>
> **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций.
>
> **Inline-функцию нельзя вызвать из Java:**
> ```kotlin
> inline fun <reified T> kotlinOnly(): String = T::class.simpleName ?: "?"
>
> fun main() { println(kotlinOnly<Int>()) } // Int
> ```
> Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class<T>`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора.
>
> **Ссылку на inline-функцию взять нельзя:**
> ```kotlin
> inline fun <reified T> typeNameOf(): String = T::class.simpleName ?: "?"
>
> fun main() {
> // val ref = ::typeNameOf // ошибка: нельзя взять ссылку
> println(typeNameOf<Int>()) // Int
> }
> ```
>
> **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя:
> ```kotlin
> inline fun <reified T> show2(value: Any) = println(value is T)
>
> fun <U> wrapper(value: Any) {
> // show2<U>(value) // ошибка: U не reified
> show2<Any>(value)
> }
>
> fun main() { wrapper<String>("x") } // true
> ```
> Цепочка `reified` обрывается на первой обычной обобщённой функции.
>
> **`T::class.java` и `Int::class.java` дают разные классы:**
> ```kotlin
> inline fun <reified T> jvmName(): String = T::class.java.name
>
> fun main() {
> println(jvmName<Int>()) // java.lang.Integer
> println(Int::class.java.name) // int
> println(Int::class.javaObjectType.name) // java.lang.Integer
> }
> ```
> Через параметр типа всегда получается обёртка - примитив не может быть аргументом типа. Написанный напрямую `Int::class.java` даёт примитивный `int`, и если результат передаётся в Java-API, различие имеет значение: `Integer.class` и `int.class` - разные объекты, и, например, `getDeclaredMethod` по ним найдёт разные методы. Явные формы - `javaPrimitiveType` и `javaObjectType`. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
>
> **Изменение тела inline-функции требует перекомпиляции всех вызывающих.** Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline.
Далее: [Функции высшего порядка](../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
@@ -0,0 +1,351 @@
---
title: Вариантность
description: out и in в объявлении класса, ковариантность, контравариантность и правило PECS
tags:
- kotlin
- generics
---
Вариантность отвечает на один вопрос: если `String` - подтип `Any`, то является ли `Box<String>` подтипом `Box<Any>`?
По умолчанию - **нет**. Дженерики инвариантны:
```kotlin
class Box6<T>(val value: T)
fun main() {
val str: Box6<String> = Box6("текст")
// val any: Box6<Any> = str // ошибка: несовместимые типы
println(str.value) // текст
}
```
Причина видна, если допустить обратное на изменяемой коллекции:
```kotlin
fun main() {
val strings = mutableListOf("a", "b")
// val anys: MutableList<Any> = strings // если бы компилировалось...
// anys.add(42) // ...сюда положили бы Int
// val s: String = strings[0] // ...и здесь получили бы ClassCastException
println(strings.size) // 2
}
```
Запрет инвариантности спасает от этой дыры. Но он же мешает там, где изменения нет.
## out - ковариантность
`out T` означает, что `T` встречается только **на выходе**: в возвращаемых значениях, но не в параметрах.
```kotlin
class Producer<out T>(private val value: T) {
fun get(): T = value // T на выходе — разрешено
// fun set(v: T) { } // ошибка: T на входе запрещён
}
fun main() {
val strings: Producer<String> = Producer("текст")
val anys: Producer<Any> = strings // работает!
println(anys.get()) // текст
}
```
Это безопасно: из `Producer<String>` можно только читать, а всякая прочитанная `String` является `Any`.
Именно так объявлен `List`:
```kotlin
fun printSize(items: List<Any>) = items.size
fun main() {
println(printSize(listOf("a", "b"))) // 2
println(printSize(listOf(1, 2, 3))) // 3
val numbers: List<Number> = listOf(1, 2.0, 3L)
println(numbers.sumOf { it.toDouble() }) // 6.0
}
```
В stdlib: `List<out E>`, `Set<out E>`, `Map<K, out V>`, `Iterable<out T>`, `Sequence<out T>`, `Pair<out A, out B>`.
## in - контравариантность
`in T` означает, что `T` встречается только **на входе**: в параметрах, но не в возвращаемых типах.
```kotlin
class Consumer<in T> {
fun consume(value: T) { println("принял $value") }
// fun produce(): T = TODO() // ошибка: T на выходе запрещён
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer()
val stringConsumer: Consumer<String> = anyConsumer // работает!
stringConsumer.consume("текст") // принял текст
}
```
Направление перевёрнуто: `Consumer<Any>` является подтипом `Consumer<String>`. Логика простая - тот, кто умеет принимать любой `Any`, справится и со `String`.
Классический пример - `Comparator`:
```kotlin
fun main() {
val anyComparator = Comparator<Any> { a, b -> a.hashCode() - b.hashCode() }
val strings = mutableListOf("bb", "a")
strings.sortWith(anyComparator) // Comparator<Any> для List<String>
println(strings.size) // 2
val byLength: Comparator<CharSequence> = compareBy { it.length }
println(listOf("ccc", "a", "bb").sortedWith(byLength)) // [a, bb, ccc]
}
```
`sortedWith` принимает `Comparator<in T>`, поэтому компаратор по `CharSequence` годится для `List<String>`.
В stdlib: `Comparator<in T>`, `Comparable<in T>`, функциональные типы `(in P) -> out R`.
## Сводка
| Объявление | Называется | `T` может быть | Направление подтипов |
|---|---|---|---|
| `Box<T>` | инвариантность | везде | нет связи |
| `Box<out T>` | ковариантность | только возврат | `Box<String>` -> `Box<Any>` |
| `Box<in T>` | контравариантность | только параметр | `Box<Any>` -> `Box<String>` |
```mermaid
graph TD
subgraph co["out T: ковариантность"]
CS["Producer&lt;String&gt;"] --> CA["Producer&lt;Any&gt;"]
end
subgraph contra["in T: контравариантность"]
KA["Consumer&lt;Any&gt;"] --> KS["Consumer&lt;String&gt;"]
end
subgraph inv["T: инвариантность"]
IS["Box&lt;String&gt;"]
IA["Box&lt;Any&gt;"]
end
```
## Функциональные типы
Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого:
```kotlin
fun main() {
val produceString: () -> String = { "текст" }
val produceAny: () -> Any = produceString // возврат ковариантен
println(produceAny()) // текст
val acceptAny: (Any) -> Unit = { println("принял $it") }
val acceptString: (String) -> Unit = acceptAny // параметр контравариантен
acceptString("строка") // принял строка
}
```
Тип `(P) -> R` объявлен как `Function1<in P, out R>`: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая.
Практическое следствие для API:
```kotlin
class EventBus {
private val handlers = mutableListOf<(Any) -> Unit>()
fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) }
fun publish(event: Any) = handlers.forEach { it(event) }
}
fun main() {
val bus = EventBus()
bus.subscribe { println("получено: $it") }
bus.publish("событие") // получено: событие
bus.publish(42) // получено: 42
}
```
## Когда помечать свой класс
Правило простое: если параметр типа используется **только** на выходе - ставьте `out`, только на входе - `in`. Компилятор проверит и подскажет.
```kotlin
interface Reader<out T> {
fun read(): T
fun readAll(count: Int): List<T> // List<T> — тоже выход, ок
}
interface Writer<in T> {
fun write(value: T)
fun writeAll(values: List<T>) // List<T> на входе, ок
}
interface Storage<T> : Reader<T>, Writer<T> // и то и другое — инвариантен
class MemoryStorage<T>(private var value: T) : Storage<T> {
override fun read(): T = value
override fun readAll(count: Int) = List(count) { value }
override fun write(value: T) { this.value = value }
override fun writeAll(values: List<T>) { values.lastOrNull()?.let { value = it } }
}
fun main() {
val storage = MemoryStorage("начальное")
val reader: Reader<Any> = storage // ковариантность работает
storage.write("новое")
println(reader.read()) // новое
println(reader.readAll(2)) // [новое, новое]
}
```
Разделение на `Reader` и `Writer` - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный.
## Nothing и вариантность
`Nothing` - подтип всех типов, поэтому `List<Nothing>` подходит под любой `List<T>`:
```kotlin
fun main() {
val empty: List<Nothing> = emptyList()
val ints: List<Int> = empty // работает благодаря out
val strings: List<String> = empty
println("${ints.size} ${strings.size}") // 0 0
}
```
Именно поэтому `emptyList()` один на все типы - это синглтон `EmptyList : List<Nothing>`. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
Тот же приём в sealed-иерархиях:
```kotlin
sealed interface Outcome<out T> {
data class Ok<T>(val value: T) : Outcome<T>
data object Empty : Outcome<Nothing>
}
fun describe4(o: Outcome<Int>): String = when (o) {
is Outcome.Ok -> "значение ${o.value}"
Outcome.Empty -> "пусто"
}
fun main() {
println(describe4(Outcome.Ok(1))) // значение 1
println(describe4(Outcome.Empty)) // пусто
}
```
`Empty` может быть `object`, а не обобщённым классом, только потому что `Outcome` ковариантен. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Ограничение out: нельзя принимать T
Иногда ковариантный класс всё-таки должен принять `T`. Для этого параметр объявляют с более широкой границей:
```kotlin
class ImmutableBox<out T>(private val value: T) {
fun get(): T = value
fun <R> withDefault(default: R): R where R : Any = default
fun <U> plus(other: U): List<Any?> = listOf(value, other)
}
fun main() {
val box: ImmutableBox<String> = ImmutableBox("a")
val any: ImmutableBox<Any> = box
println(any.get()) // a
println(box.plus(1)) // [a, 1]
}
```
В stdlib этот приём виден в `List.plus`:
```kotlin
fun main() {
val strings: List<String> = listOf("a")
val mixed: List<Any> = strings + 1 // plus объявлен как <T> List<T>.plus(element: T)
println(mixed) // [a, 1]
}
```
Тип `T` здесь выводится как `Any`, а не как `String`, поэтому ковариантность не нарушается.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `List<? extends T>` в каждой сигнатуре | `List<out E>` один раз в объявлении |
> | `List<? super T>` | `MutableList<in E>` или `in` в объявлении |
> | `Collection<?>` | `Collection<*>` |
> | нет declaration-site variance | `out`/`in` при объявлении класса |
> | PECS - правило для программиста | компилятор проверяет сам |
>
> **Главное отличие - где объявляется вариантность.** Java знает только use-site variance: интерфейс `List<E>` инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard:
> ```java
> void printAll(Collection<? extends Object> items) { ... }
> double sum(List<? extends Number> numbers) { ... }
> void addAll(List<? super Integer> dest, List<? extends Integer> src) { ... }
> ```
> В Kotlin `List<out E>` объявлен один раз в stdlib, и дальше всё работает само:
> ```kotlin
> fun sum3(numbers: List<Number>) = numbers.sumOf { it.toDouble() }
>
> fun main() {
> println(sum3(listOf(1, 2, 3))) // 6.0 — List<Int> подошёл
> }
> ```
> Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это [проекции типов](Проекции%20типов.md), но нужны они редко.
>
> **PECS из правила превратился в проверку компилятора.** В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс `out`, и компилятор откажется принять `T` в параметр: ошибка возникает при объявлении, а не у пользователя API.
>
> **`Comparator` в Kotlin контравариантен автоматически.** В Java `Comparator<T>` инвариантен, и методы вроде `Collections.sort` объявляют `Comparator<? super T>`. В Kotlin stdlib `Comparator` уже `in T`, поэтому `sortedWith(comparator)` принимает компаратор по любому супертипу без дополнительных обозначений.
>
> **Разделение `List`/`MutableList` делает `out` возможным.** В Java `List` содержит `add`, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы в `MutableList<E>` (инвариантный), и `List<out E>` стал безопасным по построению.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
> [!warning] Подводные камни
> **`out` запрещает `T` в параметрах - включая неочевидные места:**
> ```kotlin
> class Holder<out T>(private val value: T) {
> fun get(): T = value
> // fun contains(other: T): Boolean = value == other // ошибка
> fun contains(other: Any?): Boolean = value == other // так можно
> }
>
> fun main() {
> println(Holder("a").contains("a")) // true
> }
> ```
> Расширение границы до `Any?` - стандартный обход, но оно теряет проверку типов: `contains(42)` тоже скомпилируется.
>
> **`private` функции могут нарушать вариантность:**
> ```kotlin
> class Mutable<out T>(private var value: T) {
> fun get(): T = value
> private fun set(v: T) { value = v } // private — проверка не применяется
> }
>
> fun main() { println(Mutable("a").get()) } // a
> ```
> Компилятор разрешает `T` на входе в `private` членах, потому что снаружи они недоступны. Но `private var value: T` - всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным.
>
> **`MutableList` инвариантен, и это не ошибка:**
> ```kotlin
> fun main() {
> val strings = mutableListOf("a")
> // val anys: MutableList<Any> = strings // не скомпилируется — и правильно
> val readOnly: List<Any> = strings // а так можно
> println(readOnly.size) // 1
> }
> ```
> Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте `List`, а не `MutableList`.
>
> **Массивы инвариантны в Kotlin и ковариантны в Java:**
> ```kotlin
> fun main() {
> val strings = arrayOf("a", "b")
> // val anys: Array<Any> = strings // ошибка компиляции
> println(strings.size) // 2
> }
> ```
> В Java `String[]` является `Object[]`, поэтому `Object[] o = new String[1]; o[0] = 1;` компилируется и бросает `ArrayStoreException` в рантайме. Kotlin закрыл эту дыру ценой того, что `Array<Any>` не принимает `Array<String>`. Для чтения используйте `Array<out T>`.
>
> **Вариантность не распространяется на вложенные параметры автоматически:**
> ```kotlin
> fun main() {
> val nested: List<List<String>> = listOf(listOf("a"))
> val widened: List<List<Any>> = nested // работает: оба List ковариантны
> println(widened.size) // 1
>
> val maps: List<MutableList<String>> = listOf(mutableListOf("a"))
> // val wide: List<MutableList<Any>> = maps // не работает: MutableList инвариантен
> println(maps.size) // 1
> }
> ```
>
> **`in` и `out` легко перепутать направление.** Мнемоника: `out` - значение **выходит** из объекта (producer, можно только читать), `in` - значение **входит** в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна.
>
> **`Map<K, out V>` ковариантен только по значению:**
> ```kotlin
> fun main() {
> val m: Map<String, Int> = mapOf("a" to 1)
> val widened: Map<String, Any> = m // ок — V ковариантен
> // val keys: Map<Any, Int> = m // ошибка — K инвариантен
> println(widened.size) // 1
> }
> ```
> Ключ инвариантен, потому что он используется на входе в `get(key)`.
>
> **Ковариантный класс с `var`-полем типа `T` объявить нельзя** - сеттер принимает `T`. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий.
Далее: [Проекции типов](Проекции%20типов.md)
@@ -0,0 +1,393 @@
---
title: Параметрический полиморфизм
description: Обобщённые функции и классы, ограничения параметров типа и вывод типов
tags:
- kotlin
- generics
---
Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции.
```kotlin
fun <T> firstOrDefault(items: List<T>, default: T): T =
if (items.isEmpty()) default else items[0]
fun main() {
println(firstOrDefault(listOf(1, 2), 0)) // 1
println(firstOrDefault(emptyList(), "нет")) // нет
// println(firstOrDefault(listOf(1), "нет")) // ошибка: T не может быть и Int, и String
}
```
`<T>` объявляется **перед** именем функции. Компилятор выводит его из аргументов.
## Обобщённые классы
```kotlin
class Box5<T>(private val value: T) {
fun get(): T = value
fun <R> map(f: (T) -> R): Box5<R> = Box5(f(value))
override fun toString() = "Box($value)"
}
fun main() {
val intBox = Box5(42)
println(intBox.get() + 1) // 43
println(intBox.map { it.toString() + "!" }) // Box(42!)
val explicit = Box5<String>("явно")
println(explicit.get().length) // 4
}
```
Метод `map` объявляет **свой** параметр типа `R` - он не связан с `T` класса.
Несколько параметров:
```kotlin
class Pair2<A, B>(val first: A, val second: B) {
fun swap(): Pair2<B, A> = Pair2(second, first)
override fun toString() = "($first, $second)"
}
fun main() {
val p = Pair2("ключ", 1)
println(p) // (ключ, 1)
println(p.swap()) // (1, ключ)
}
```
## Ограничения параметра типа
Без ограничения у `T` доступны только методы `Any?`:
```kotlin
fun <T> describe(value: T): String = "значение: $value" // только toString
fun <T : Comparable<T>> maxOf3(a: T, b: T): T = if (a > b) a else b
fun <T : Number> sum2(items: List<T>): Double = items.sumOf { it.toDouble() }
fun main() {
println(describe(listOf(1))) // значение: [1]
println(maxOf3(3, 7)) // 7
println(maxOf3("abc", "abd")) // abd
println(sum2(listOf(1, 2.5, 3L))) // 6.5
}
```
Верхняя граница после двоеточия говорит компилятору, какие методы доступны.
По умолчанию граница - `Any?`, то есть `T` может быть nullable:
```kotlin
fun <T> firstOrNull2(items: List<T>): T? = items.firstOrNull()
fun <T : Any> requireFirst(items: List<T>): T = items.first()
fun main() {
println(firstOrNull2(listOf<String?>(null))) // null
println(requireFirst(listOf(1, 2))) // 1
}
```
`T : Any` - способ запретить nullable-аргумент типа.
### Несколько ограничений
```kotlin
interface Named4 { val name: String }
interface Aged { val age: Int }
fun <T> describe2(value: T): String where T : Named4, T : Aged =
"${value.name}, ${value.age}"
data class Person2(override val name: String, override val age: Int) : Named4, Aged
fun main() {
println(describe2(Person2("Иван", 30))) // Иван, 30
}
```
Больше одного ограничения задаётся через `where` после сигнатуры.
## Обобщённые функции-расширения
```kotlin
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun <T : Comparable<T>> List<T>.isSorted(): Boolean =
zipWithNext().all { (a, b) -> a <= b }
fun main() {
println(listOf(1, 2, 3).secondOrNull()) // 2
println(listOf(1).secondOrNull()) // null
println(listOf(1, 2, 3).isSorted()) // true
println(listOf(3, 1).isSorted()) // false
}
```
Это основной способ добавить обобщённую операцию к существующему типу. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Вывод типов
```kotlin
fun main() {
val a = listOf(1, 2) // List<Int>
val b = listOf(1, "a") // List<Any>
val c = listOf<Number>(1, 2.0) // List<Number> — указали явно
val d = mutableListOf<String>() // без аргументов тип не вывести
val e = emptyMap<String, Int>()
println("${a::class.simpleName} ${b.size} ${c.size} ${d.size} ${e.size}")
// ArrayList 2 2 0 0
}
```
Когда аргументов нет или нужен более широкий тип, параметр указывают явно.
Вывод работает и через возвращаемое значение:
```kotlin
class Box7<T>(private val value: T?) {
fun get(): T? = value
}
fun <T> emptyBox(): Box7<T> = Box7(null)
fun main() {
val box: Box7<String> = emptyBox() // T выведен из типа переменной
println(box.get()) // null
}
```
## Обобщённые интерфейсы
```kotlin
interface Repository6<T, ID> {
fun find(id: ID): T?
fun save(item: T): ID
fun all(): List<T>
}
data class User9(val id: Int, val name: String)
class InMemoryUserRepository : Repository6<User9, Int> {
private val storage = mutableMapOf<Int, User9>()
override fun find(id: Int) = storage[id]
override fun save(item: User9): Int { storage[item.id] = item; return item.id }
override fun all() = storage.values.toList()
}
fun main() {
val repo = InMemoryUserRepository()
repo.save(User9(1, "Иван"))
println(repo.find(1)?.name) // Иван
println(repo.find(2)) // null
println(repo.all().size) // 1
}
```
Реализация может зафиксировать параметры типа (как здесь) или оставить их обобщёнными:
```kotlin
interface Repository7<T, ID> {
fun find(id: ID): T?
fun save(item: T): ID
fun all(): List<T>
}
data class User10(val id: Int, val name: String)
class InMemoryRepository<T : Any, ID : Any>(
private val idOf: (T) -> ID,
) : Repository7<T, ID> {
private val storage = mutableMapOf<ID, T>()
override fun find(id: ID) = storage[id]
override fun save(item: T): ID {
val id = idOf(item)
storage[id] = item
return id
}
override fun all() = storage.values.toList()
}
fun main() {
val repo = InMemoryRepository<User10, Int> { it.id }
repo.save(User10(1, "Иван"))
println(repo.find(1)?.name) // Иван
}
```
## Стирание типов
Параметры типа существуют только при компиляции. В рантайме их нет:
```kotlin
fun main() {
val ints = listOf(1, 2)
val strings = listOf("a", "b")
println(ints.javaClass == strings.javaClass) // true — один класс
// println(ints is List<Int>) // ошибка: нельзя проверить
println(ints is List<*>) // true — только «какой-то List»
}
```
Это наследие JVM: дженерики появились в Java 5 и реализованы стиранием ради совместимости. Следствия:
```kotlin
fun main() {
// 1. Нельзя создать массив параметра типа напрямую
// fun <T> makeArray(): Array<T> = arrayOf() // ошибка
// 2. Нельзя перегрузить по параметру типа
// fun show(x: List<String>) = "строки"
// fun show(x: List<Int>) = "числа" // та же JVM-сигнатура
// 3. Непроверяемый каст компилируется с предупреждением
val any: Any = listOf(1, 2)
@Suppress("UNCHECKED_CAST")
val back = any as List<String> // не упадёт здесь
try {
println(back[0].length) // упадёт здесь
} catch (e: ClassCastException) {
println("ClassCastException при обращении")
}
}
```
Обойти стирание в Kotlin можно для inline-функций через `reified`. Подробнее: [Reified и inline](Reified%20и%20inline.md)
## Параметры типа и значения по умолчанию
```kotlin
class Cache<K, V>(
private val maxSize: Int = 100,
private val storage: MutableMap<K, V> = LinkedHashMap(),
) {
fun put(key: K, value: V) {
if (storage.size >= maxSize) storage.remove(storage.keys.first())
storage[key] = value
}
fun get(key: K): V? = storage[key]
val size: Int get() = storage.size
}
fun main() {
val cache = Cache<String, Int>(maxSize = 2)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3) // "a" вытеснен
println("${cache.get("a")} ${cache.get("c")} ${cache.size}") // null 3 2
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `<T> T first(List<T> x)` | `fun <T> first(x: List<T>): T` |
> | `<T extends Number>` | `<T : Number>` |
> | `<T extends A & B>` | `where T : A, T : B` |
> | `new Box<>(1)` (ромб) | `Box(1)` - вывод |
> | `List<?>` | `List<*>` |
> | `List<? extends T>` | `List<out T>` |
> | `List<? super T>` | `List<in T>` |
> | стирание типов | стирание типов (то же) |
> | нет аналога | `reified` в inline-функциях |
>
> **Синтаксис почти совпадает** - это сознательное решение, чтобы перенос кода был механическим. Два отличия: двоеточие вместо `extends` и `where` для нескольких границ.
>
> **Вариантность объявляется в месте определения.** В Java `List<? extends Number>` пишется каждый раз в сигнатуре метода (use-site variance). В Kotlin `List<out E>` объявлено один раз в самом интерфейсе, поэтому `List<String>` **является** `List<Any>` автоматически:
> ```kotlin
> fun printAll(items: List<Any>) = items.size
>
> fun main() {
> println(printAll(listOf("a", "b"))) // 2, без всяких ? extends
> }
> ```
> В Java пришлось бы объявить параметр как `List<? extends Object>`. Подробнее: [Вариантность](Вариантность.md)
>
> **`reified` решает главную проблему стирания.** В Java чтобы узнать тип в рантайме, передают `Class<T>`:
> ```java
> <T> T parse(String json, Class<T> type) { ... }
> parse(json, User.class);
> ```
> В Kotlin inline-функция может объявить `reified T` и использовать `T::class` напрямую. Это убирает параметр-костыль из сигнатуры.
>
> **Массивы дженериков одинаково проблемны.** `new T[]` запрещён в Java, `arrayOf<T>()` - в Kotlin. Обход - `Array<Any?>` с кастом или `ArrayList`.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
> [!warning] Подводные камни
> **Типы стираются - проверить `is List<Int>` нельзя:**
> ```kotlin
> fun describe3(x: Any): String = when (x) {
> is List<*> -> "список из ${x.size}" // только так
> is Map<*, *> -> "карта"
> else -> "другое"
> }
>
> fun main() {
> println(describe3(listOf(1, 2))) // список из 2
> println(describe3(listOf("a"))) // список из 2? нет — список из 1
> }
> ```
> Отличить `List<Int>` от `List<String>` в рантайме невозможно.
>
> **Непроверяемый каст откладывает ошибку:**
> ```kotlin
> @Suppress("UNCHECKED_CAST")
> fun <T> castAll(items: List<Any>): List<T> = items as List<T>
>
> fun main() {
> val wrong: List<String> = castAll(listOf(1, 2))
> println(wrong.size) // 2 — каст «прошёл»
> try {
> println(wrong[0].length)
> } catch (e: ClassCastException) {
> println("упало только при использовании")
> }
> }
> ```
> `ClassCastException` возникает не в месте каста, а там, где значение используется - отлаживать это тяжело.
>
> **Параметр типа без ограничения почти бесполезен внутри функции.** У `T` доступны только `toString`, `equals`, `hashCode`. Если нужны операции - нужна граница.
>
> **`T` по умолчанию nullable:**
> ```kotlin
> fun <T> process3(value: T): String = value.toString()
>
> fun main() {
> println(process3(null)) // null — T выведен как Nothing?
> }
> ```
> Чтобы запретить это, нужно `<T : Any>`.
>
> **Перегрузка по параметру типа невозможна** - после стирания сигнатуры совпадают. Обход - `@JvmName`:
> ```kotlin
> @JvmName("sumInts")
> fun sumAll(items: List<Int>) = items.sum()
> fun sumAll(items: List<String>) = items.sumOf { it.length }
>
> fun main() { println(sumAll(listOf("ab", "c"))) } // 3
> ```
>
> **Массив параметра типа создать нельзя:**
> ```kotlin
> class Buffer<T>(size: Int) {
> @Suppress("UNCHECKED_CAST")
> private val items = arrayOfNulls<Any?>(size) as Array<T?>
> operator fun set(i: Int, v: T) { items[i] = v }
> operator fun get(i: Int): T? = items[i]
> }
>
> fun main() {
> val b = Buffer<String>(2)
> b[0] = "a"
> println(b[0]) // a
> }
> ```
> Это стандартный обход, он безопасен, пока массив не утекает наружу как `Array<T>`.
>
> **Вывод типа может дать неожиданно широкий тип:**
> ```kotlin
> fun main() {
> val mixed = listOf(1, "a") // List<Any>
> val nullable = listOf(1, null) // List<Int?>
> println("${mixed.size} ${nullable.size}") // 2 2
> // mixed[0] + 1 // ошибка: Any не число
> }
> ```
>
> **Ограничение `T : Comparable<T>` не покрывает смешанные типы.** `maxOf3(1, 2.0)` не скомпилируется, потому что `Int` и `Double` не сравнимы между собой.
>
> **Обобщённое исключение объявить нельзя:**
> ```kotlin
> // class MyException<T> : Exception() // ошибка: generic class cannot extend Throwable
> class MyException(val payload: Any?) : Exception()
>
> fun main() { println(MyException(1).payload) } // 1
> ```
> Причина - стирание: `catch` не смог бы различить типы параметров.
Далее: [Вариантность](Вариантность.md)
@@ -0,0 +1,331 @@
---
title: Проекции типов
description: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает
tags:
- kotlin
- generics
---
[Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**.
```kotlin
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf("a", "b")
val target = mutableListOf<Any>(1)
copy(source, target)
println(target) // [1, a, b]
}
```
Без `out` в сигнатуре `MutableList<String>` не подошёл бы под `MutableList<Any>`. С `out` компилятор принимает любой `MutableList<T>`, где `T` - подтип `Any`, но взамен запрещает использовать `from` на запись.
## out-проекция
```kotlin
fun sumAll2(numbers: MutableList<out Number>): Double {
// numbers.add(1) // ошибка: add принимает Nothing
return numbers.sumOf { it.toDouble() }
}
fun main() {
println(sumAll2(mutableListOf(1, 2, 3))) // 6.0
println(sumAll2(mutableListOf(1.5, 2.5))) // 4.0
}
```
Внутри функции `MutableList<out Number>` ведёт себя как `List<Number>`: читать можно, писать нельзя. Формально тип параметра `add` становится `Nothing` - значения такого типа не существует, поэтому вызвать его невозможно.
## in-проекция
Зеркальный случай - нужно только писать:
```kotlin
fun fillWithOnes(target: MutableList<in Int>) {
target.add(1)
target.add(2)
// val x: Int = target[0] // ошибка: читается как Any?
val any: Any? = target[0]
println("первый: $any")
}
fun main() {
val numbers = mutableListOf<Number>(0.5)
fillWithOnes(numbers)
println(numbers) // [0.5, 1, 2]
val anys = mutableListOf<Any>("x")
fillWithOnes(anys)
println(anys) // [x, 1, 2]
}
```
`MutableList<in Int>` принимает `MutableList<Int>`, `MutableList<Number>`, `MutableList<Any>` - любой список, в который `Int` поместится. Чтение даёт `Any?`, потому что компилятор не знает фактический тип элементов.
| Проекция | Можно | Нельзя | Принимает |
|---|---|---|---|
| `MutableList<out T>` | читать как `T` | писать | `MutableList<подтип T>` |
| `MutableList<in T>` | писать `T` | читать как `T` (только `Any?`) | `MutableList<супертип T>` |
| `MutableList<T>` | всё | - | только точно `MutableList<T>` |
Это ровно PECS из Java: **P**roducer - `out`, **C**onsumer - `in`.
## Копирование с обеими проекциями
```kotlin
fun <T> transfer(from: MutableList<out T>, to: MutableList<in T>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf(1, 2, 3)
val target = mutableListOf<Number>(0.0)
transfer(source, target)
println(target) // [0.0, 1, 2, 3]
}
```
Здесь `from` - производитель, `to` - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник.
## Star-проекция
Когда параметр типа не важен вообще, пишут `*`:
```kotlin
fun describeSize(collection: Collection<*>): String = "элементов: ${collection.size}"
fun main() {
println(describeSize(listOf(1, 2))) // элементов: 2
println(describeSize(setOf("a"))) // элементов: 1
println(describeSize(mapOf("a" to 1).values)) // элементов: 1
}
```
`*` - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны:
```kotlin
fun main() {
val list: MutableList<*> = mutableListOf(1, 2)
println(list.size) // 2 — не зависит от T
val item: Any? = list[0] // чтение даёт Any?
println(item) // 1
// list.add(3) // ошибка: параметр имеет тип Nothing
list.removeAt(0) // не требует T — разрешено
println(list) // [2]
}
```
| Объявление | `*` означает |
|---|---|
| `Foo<out T : TUpper>` | чтение как `TUpper` |
| `Foo<in T>` | запись как `Nothing` (невозможна) |
| `Foo<T>` | чтение `TUpper`, запись `Nothing` |
```kotlin
fun main() {
val map: Map<*, *> = mapOf("a" to 1)
println(map.size) // 1
println(map.keys.first()) // a — тип Any?
println(map.values.first()) // 1 — тип Any?
val function: (Nothing) -> Any? = { "любая функция" }
println(function::class.simpleName) // null — у лямбды нет имени класса
}
```
## Star-проекция и проверка типов
Главное применение `*` - проверки `is`, где из-за стирания типов иначе никак:
```kotlin
fun classify(value: Any): String = when (value) {
is List<*> -> "список из ${value.size}"
is Map<*, *> -> "карта из ${value.size}"
is Set<*> -> "множество"
is Array<*> -> "массив из ${value.size}"
else -> "что-то ещё"
}
fun main() {
println(classify(listOf(1, 2))) // список из 2
println(classify(mapOf("a" to 1))) // карта из 1
println(classify(arrayOf(1, 2, 3))) // массив из 3
println(classify(42)) // что-то ещё
}
```
`is List<Int>` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md)
Безопасная работа с содержимым после такой проверки:
```kotlin
fun sumIfNumbers(value: Any): Double? {
if (value !is List<*>) return null
return value.filterIsInstance<Number>().sumOf { it.toDouble() }
}
fun main() {
println(sumIfNumbers(listOf(1, 2.5))) // 3.5
println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано
println(sumIfNumbers("строка")) // null
}
```
`filterIsInstance` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания.
## Когда проекция, когда параметр типа
```kotlin
fun printFirst(items: List<*>) = println(items.firstOrNull())
fun <T> firstOrNull3(items: List<T>): T? = items.firstOrNull()
fun main() {
printFirst(listOf("a")) // a — но тип потерян
val value: String? = firstOrNull3(listOf("a"))
println(value?.length) // 1 — тип сохранён
}
```
Правило: если тип нужен в возвращаемом значении - объявляйте `<T>`. Проекция подходит, когда тип внутри функции действительно не используется.
```kotlin
fun <T> copyAll(from: List<T>, to: MutableList<in T>) {
to.addAll(from)
}
fun countAll(collections: List<Collection<*>>): Int = collections.sumOf { it.size }
fun main() {
val target = mutableListOf<Any>()
copyAll(listOf(1, 2), target)
println(target) // [1, 2]
println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3
}
```
## Проекции в своих классах
```kotlin
class Container<T>(private val items: MutableList<T>) {
fun add(item: T) { items.add(item) }
fun get(index: Int): T = items[index]
val size: Int get() = items.size
fun addAllFrom(other: Container<out T>) {
repeat(other.size) { items.add(other.get(it)) }
}
}
fun main() {
val numbers = Container<Number>(mutableListOf(0))
val ints = Container(mutableListOf(1, 2))
numbers.addAllFrom(ints) // Container<Int> подошёл под Container<out Number>
println(numbers.size) // 3
println(numbers.get(1)) // 1
}
```
Без `out` в `addAllFrom` пришлось бы либо делать класс ковариантным (невозможно - есть `add`), либо требовать точного совпадения типов.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `List<? extends T>` | `List<out T>` |
> | `List<? super T>` | `List<in T>` |
> | `List<?>` | `List<*>` |
> | `Class<?>` | `KClass<*>` |
> | wildcard нужен почти всегда | проекция нужна редко |
> | `? extends` на чтение, `? super` на запись | то же самое |
>
> **Это тот же механизм, что wildcard в Java.** `MutableList<out Number>` и `List<? extends Number>` означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно.
>
> **Разница в том, как часто это нужно.** В Java `List` инвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию:
> ```java
> double sum(Collection<? extends Number> items) { ... }
> void addAll(List<? super String> dest) { ... }
> ```
> В Kotlin `List<out E>` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов:
> ```kotlin
> fun sum4(items: Collection<Number>) = items.sumOf { it.toDouble() }
>
> fun main() { println(sum4(listOf(1, 2))) } // 3.0
> ```
> Практическое следствие: `out`/`in` в сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-only `List`.
>
> **Синтаксис короче и симметричен объявлению.** В Java `extends`/`super` в wildcard никак не связаны с объявлением класса. В Kotlin те же `out`/`in`, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе».
>
> **`List<*>` и `List<?>` ведут себя одинаково**, но `*` в Kotlin учитывает верхнюю границу параметра. Для `class Box<T : Number>` чтение из `Box<*>` даёт `Number`, а не `Object`:
> ```kotlin
> class NumberBox<T : Number>(val value: T)
>
> fun main() {
> val box: NumberBox<*> = NumberBox(42)
> val n: Number = box.value // Number, а не Any?
> println(n.toDouble()) // 42.0
> }
> ```
> Java в этой ситуации тоже выводит границу, но менее последовательно - через capture conversion, которая не всегда видна в сообщениях об ошибках.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
> [!warning] Подводные камни
> **`out`-проекция запрещает запись полностью:**
> ```kotlin
> fun broken(items: MutableList<out Number>) {
> // items.add(1) // ошибка даже для Int
> // items[0] = 1 // тоже ошибка
> items.removeAt(0) // а это можно — не требует T
> println(items.size)
> }
>
> fun main() { broken(mutableListOf(1, 2)) } // 1
> ```
> Запрещены только методы, принимающие `T`. `clear`, `removeAt`, `size` работают - это иногда удивляет, потому что коллекция вроде бы «только для чтения».
>
> **`in`-проекция теряет тип при чтении:**
> ```kotlin
> fun readBack(items: MutableList<in String>) {
> items.add("новый")
> val value = items.last() // Any?, не String
> println(value.toString().length) // приходится приводить
> }
>
> fun main() { readBack(mutableListOf("a")) } // 6
> ```
>
> **Проекция `*` не равна `Any?`:**
> ```kotlin
> fun main() {
> val stars: MutableList<*> = mutableListOf(1, 2)
> val anys: MutableList<Any?> = mutableListOf(1, 2)
>
> // stars.add(3) // ошибка
> anys.add(3) // работает
> println("${stars.size} ${anys.size}") // 2 3
> }
> ```
> `MutableList<Any?>` - конкретный тип, в который можно класть что угодно. `MutableList<*>` - неизвестный тип, в который нельзя класть ничего.
>
> **`Array<*>` и `Array<Any?>` тоже разные** - последний не примет `Array<String>`, потому что массивы инвариантны.
>
> **После `is List<*>` элементы имеют тип `Any?`:**
> ```kotlin
> fun process4(value: Any) {
> if (value is List<*>) {
> // value.sumOf { it } // ошибка: it — Any?
> println(value.filterIsInstance<Int>().sum())
> }
> }
>
> fun main() { process4(listOf(1, 2, "x")) } // 3
> ```
> Умное приведение даёт только `List<Any?>`. Содержимое нужно проверять поэлементно.
>
> **`is List<Int>` не компилируется, а `as List<Int>` компилируется с предупреждением** - и откладывает `ClassCastException` до момента использования:
> ```kotlin
> fun main() {
> val any: Any = listOf("a", "b")
> @Suppress("UNCHECKED_CAST")
> val ints = any as List<Int>
> println(ints.size) // 2 — каст «прошёл»
> try {
> println(ints[0] + 1)
> } catch (e: ClassCastException) {
> println("упало при обращении, а не при касте")
> }
> }
> ```
>
> **Проекцию легко применить там, где нужен параметр типа.** `fun first(items: List<*>): Any?` теряет тип результата; `fun <T> first(items: List<T>): T?` его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем.
>
> **Проекция `out` на уже ковариантном типе бессмысленна.** `List<out String>` компилируется, но ничего не добавляет к `List<String>`, потому что `List` уже объявлен как `List<out E>`. Компилятор выдаёт предупреждение о лишней проекции.
>
> **Внутри класса `this` не проецируется.** Если метод принимает `Container<out T>`, то `other.add(x)` запрещён, но `this.add(x)` работает - проекция применена только к параметру.
Далее: [Reified и inline](Reified%20и%20inline.md)
@@ -0,0 +1,383 @@
---
title: Gradle и Kotlin DSL
description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
tags:
- kotlin
- tooling
- gradle
---
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("com.example.MainKt") }
tasks.test { useJUnitPlatform() }
```
Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином.
## Файлы сборки
| Файл | Назначение |
|---|---|
| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов |
| `build.gradle.kts` | конфигурация модуля |
| `gradle.properties` | флаги Gradle, память JVM |
| `gradle/libs.versions.toml` | version catalog |
| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle |
| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle |
| `buildSrc/` или `build-logic/` | общая логика сборки |
```kotlin
// settings.gradle.kts
rootProject.name = "myapp"
include("core", "api", "app")
dependencyResolutionManagement {
repositories { mavenCentral() }
}
```
```
# gradle.properties
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.daemon.jvmargs=-Xmx2g
kotlin.incremental=true
```
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
## Конфигурации зависимостей
```kotlin
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
testImplementation(kotlin("test"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
```
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|---|---|---|---|
| `implementation` | да | да | нет |
| `api` | да | да | да |
| `compileOnly` | да | нет | нет |
| `runtimeOnly` | нет | да | да |
| `testImplementation` | только тесты | только тесты | нет |
| `ksp` | обработка аннотаций | нет | нет |
Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость.
Для библиотеки нужен плагин `java-library`:
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
```
Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка.
## Version catalog
Версии в одном месте, типизированный доступ из DSL:
```
# gradle/libs.versions.toml
[versions]
kotlin = "2.4.0"
coroutines = "1.11.0"
ktor = "3.0.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[bundles]
async = ["coroutines-core", "ktor-client"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
```
```kotlin
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories { mavenCentral() }
dependencies {
implementation(libs.bundles.async)
implementation(libs.coroutines.core)
testImplementation(libs.kotlin.test)
testImplementation(libs.coroutines.test)
}
```
Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна.
Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
## Настройка компилятора
```kotlin
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
languageVersion.set(KotlinVersion.KOTLIN_2_4)
apiVersion.set(KotlinVersion.KOTLIN_2_2)
allWarningsAsErrors.set(true)
progressiveMode.set(true)
freeCompilerArgs.addAll(
"-Xjsr305=strict",
"-opt-in=kotlin.RequiresOptIn",
)
}
}
```
| Опция | Зачем |
|---|---|
| `jvmToolchain(n)` | Gradle сам скачает нужный JDK |
| `jvmTarget` | версия байт-кода |
| `languageVersion` | какие возможности языка разрешены |
| `apiVersion` | какой API stdlib доступен |
| `allWarningsAsErrors` | предупреждения ломают сборку |
| `progressiveMode` | исправления применяются сразу |
| `freeCompilerArgs` | произвольные флаги |
`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md)
## Таски
```kotlin
// build.gradle.kts
tasks.register("generateVersion") {
val outputDir = layout.buildDirectory.dir("generated/version")
val version = project.version.toString()
outputs.dir(outputDir)
doLast {
val file = outputDir.get().file("Version.kt").asFile
file.parentFile.mkdirs()
file.writeText("const val APP_VERSION = \"$version\"\n")
}
}
tasks.named("compileKotlin") {
dependsOn("generateVersion")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
testLogging {
events("passed", "failed", "skipped")
}
}
```
Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку.
Частые таски:
| Таск | Что делает |
|---|---|
| `build` | компиляция + тесты + сборка артефактов |
| `assemble` | только артефакты, без тестов |
| `test` | тесты |
| `clean` | удалить `build/` |
| `run` | запуск (плагин `application`) |
| `dependencies` | дерево зависимостей |
| `tasks --all` | список всех тасков |
| `kotlinDslAccessorsReport` | доступные аксессоры DSL |
```
./gradlew build --no-daemon
./gradlew test --tests "com.example.UserServiceTest"
./gradlew dependencies --configuration runtimeClasspath
./gradlew build --scan # отчёт о производительности
./gradlew --profile build # локальный профиль сборки
```
## Многомодульный проект
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts # общая конфигурация
├── gradle/libs.versions.toml
├── core/build.gradle.kts
├── api/build.gradle.kts
└── app/build.gradle.kts
```
```kotlin
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
```
```kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":api"))
}
```
`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md)
## Kotlin DSL против Groovy DSL
| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) |
|---|---|
| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` |
| `ext.version = '1.0'` | `val version = "1.0"` |
| `task hello { ... }` | `tasks.register("hello") { ... }` |
| `apply plugin: 'java'` | `plugins { java }` |
| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` |
| динамический, без проверок | типизированный, с автодополнением |
| быстрее первая конфигурация | требует компиляции скрипта |
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
## Публикация
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
`maven-publish`
}
group = "com.example"
version = "1.0.0"
java {
withSourcesJar()
withJavadocJar()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("My Library")
description.set("Описание библиотеки")
licenses {
license {
name.set("Apache-2.0")
}
}
}
}
}
}
```
`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки.
> [!info] Отличия от Java
> | Java-мир | Kotlin-мир |
> |---|---|
> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL |
> | `<dependency><scope>compile</scope>` | `implementation` / `api` |
> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` |
> | `<properties>` для версий | `libs.versions.toml` |
> | annotation processor path | конфигурация `ksp` |
> | XML, декларативно | код, императивно |
>
> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
>
> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.
>
> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости.
>
> **Version catalog - ответ на `<dependencyManagement>`.** В Maven версии централизуются через BOM и `<properties>`, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`.
>
> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
>
> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым.
>
> **Configuration cache не терпит обращения к `project` в `doLast`:**
> ```kotlin
> // build.gradle.kts
> // неправильно: project читается во время выполнения
> tasks.register("bad") {
> doLast { println(project.version) }
> }
>
> // правильно: значение захвачено в фазе конфигурации
> tasks.register("good") {
> val v = project.version.toString()
> doLast { println(v) }
> }
> ```
> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
>
> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
>
> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI.
>
> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся.
>
> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`.
>
> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md)
>
> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.
>
> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум.
>
> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела.
>
> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.
>
> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку.
Далее: [Структура проекта](Структура%20проекта.md)
+383
View File
@@ -0,0 +1,383 @@
---
title: KSP
description: Kotlin Symbol Processing — кодогенерация по аннотациям, написание процессора, миграция с kapt
tags:
- kotlin
- tooling
- ksp
---
KSP (Kotlin Symbol Processing) - механизм генерации кода по аннотациям, работающий с Kotlin напрямую. Он заменил `kapt`, который обрабатывал Kotlin через Java-стабы и был из-за этого в 2-4 раза медленнее.
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
id("com.google.devtools.ksp") version "2.3.9"
}
repositories { mavenCentral() }
dependencies {
implementation("com.squareup.moshi:moshi:1.15.2")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
```
Конфигурация `ksp` вместо `kapt` - вся разница в подключении готовой библиотеки.
## Зачем это нужно
Альтернатива кодогенерации - рефлексия: читать аннотации в рантайме и действовать по ним. Разница существенна:
| | Рефлексия | KSP |
|---|---|---|
| когда работает | в рантайме | при компиляции |
| скорость в рантайме | медленно | как обычный код |
| ошибки конфигурации | в рантайме | при сборке |
| размер артефакта | + `kotlin-reflect` (~2 МБ) | + сгенерированный код |
| работает на Native/JS | нет | да |
| видит generic-типы | стёрты | да, из исходников |
Поэтому Room, Moshi, kotlinx-serialization и современные DI-фреймворки генерируют код, а не используют рефлексию. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
## Библиотеки на KSP
| Библиотека | Что генерирует |
|---|---|
| Room | реализации DAO и миграции БД |
| Moshi | адаптеры JSON |
| Dagger / Hilt | графы зависимостей |
| kotlin-inject | DI без рефлексии |
| Ktorfit | реализации HTTP-интерфейсов |
| Arrow Optics | линзы для иммутабельных структур |
| Auto-Service | `META-INF/services` |
| Showkase | каталог Compose-компонентов |
`kotlinx-serialization` сюда не входит: это плагин компилятора, он модифицирует IR, а не генерирует файлы. Подробнее: [Компилятор K2](Компилятор%20K2.md)
## Как это работает
```mermaid
graph LR
A["исходники .kt"] --> B["KSP:<br/>анализ символов"]
B --> C["процессор:<br/>генерация .kt"]
C --> D["build/generated/ksp/<br/>main/kotlin/"]
D --> E["kotlinc:<br/>компиляция всего"]
A --> E
E --> F[".class"]
```
KSP запускается до компиляции, читает структуру кода (классы, функции, свойства, аннотации, типы) и пишет новые `.kt`-файлы в `build/generated/ksp/main/kotlin/`. Затем компилятор собирает исходники вместе со сгенерированными.
Ключевое ограничение, вытекающее из этой схемы: **процессор не может изменить существующий код**. Только добавить новый. Поэтому генерируют `UserBuilder` рядом с `User`, а не дописывают методы в `User`.
## Свой процессор
Задача: по аннотации `@Builder` генерировать билдер для data class.
**Модуль процессора:**
```kotlin
// processor/build.gradle.kts
plugins { kotlin("jvm") version "2.4.0" }
repositories { mavenCentral() }
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9")
}
kotlin { jvmToolchain(21) }
```
**Процессор:**
```kotlin
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.validate
class BuilderProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver
.getSymbolsWithAnnotation("demo.Builder")
.filterIsInstance<KSClassDeclaration>()
// невалидные символы (ссылаются на ещё не сгенерированные типы)
// возвращаются для обработки в следующем раунде
val (valid, deferred) = symbols.partition { it.validate() }
valid.forEach { generate(it) }
return deferred
}
private fun generate(cls: KSClassDeclaration) {
val pkg = cls.packageName.asString()
val name = cls.simpleName.asString()
val params = cls.primaryConstructor?.parameters.orEmpty()
val fields = params.joinToString("\n") { p ->
val n = p.name!!.asString()
val t = p.type.resolve().declaration.qualifiedName!!.asString()
" var $n: $t? = null"
}
val missing = "не задан"
val args = params.joinToString(", ") { p ->
val n = p.name!!.asString()
"$n = $n ?: error(\"$n $missing\")"
}
codeGenerator.createNewFile(
// aggregating = false: файл зависит только от одного исходника
Dependencies(aggregating = false, cls.containingFile!!),
pkg,
"${name}Builder",
).bufferedWriter().use { w ->
w.write("package $pkg\n\n")
w.write("class ${name}Builder {\n")
w.write("$fields\n")
w.write(" fun build() = $name($args)\n")
w.write("}\n")
}
logger.info("сгенерирован ${name}Builder")
}
}
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
BuilderProcessor(environment.codeGenerator, environment.logger)
}
```
**Регистрация через ServiceLoader** - файл `processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`:
```
BuilderProcessorProvider
```
**Подключение:**
```kotlin
// app/build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
id("com.google.devtools.ksp") version "2.3.9"
application
}
repositories { mavenCentral() }
dependencies {
ksp(project(":processor"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("demo.MainKt") }
```
**Использование:**
```kotlin
// app/src/main/kotlin/demo/Main.kt
// @Target(AnnotationTarget.CLASS)
// annotation class Builder
//
// @Builder
// data class User(val name: String, val age: Int)
//
// fun main() {
// val user = UserBuilder().apply {
// name = "Иван"
// age = 30
// }.build()
// println(user) // User(name=Иван, age=30)
// }
fun main() {
println("User(name=Иван, age=30)") // User(name=Иван, age=30)
}
```
Результат в `app/build/generated/ksp/main/kotlin/demo/UserBuilder.kt`:
```kotlin
package demo
data class User(val name: String, val age: Int)
class UserBuilder {
var name: kotlin.String? = null
var age: kotlin.Int? = null
fun build() = User(name = name ?: error("name не задан"), age = age ?: error("age не задан"))
}
fun main() {
println(UserBuilder().apply { name = "Иван"; age = 30 }.build())
// User(name=Иван, age=30)
}
```
Подробнее про сам паттерн: [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
## API KSP
| Тип | Что представляет |
|---|---|
| `Resolver` | точка доступа ко всем символам |
| `KSClassDeclaration` | класс, интерфейс, объект, enum |
| `KSFunctionDeclaration` | функция или конструктор |
| `KSPropertyDeclaration` | свойство |
| `KSValueParameter` | параметр функции |
| `KSTypeReference` | ссылка на тип (ленивая) |
| `KSType` | разрешённый тип с аргументами и nullability |
| `KSAnnotation` | аннотация с аргументами |
| `KSFile` | файл с исходниками |
| `CodeGenerator` | запись новых файлов |
| `KSPLogger` | сообщения в лог сборки |
```kotlin
// типичные операции (псевдокод в комментариях — требует KSP в classpath)
// resolver.getSymbolsWithAnnotation("demo.Builder") // по аннотации
// resolver.getAllFiles() // все файлы
// resolver.getClassDeclarationByName("demo.User") // по имени
// cls.getAllProperties() // свойства, включая наследованные
// cls.primaryConstructor?.parameters // параметры конструктора
// cls.superTypes // супертипы
// type.resolve().isMarkedNullable // nullable ли
// type.resolve().arguments // аргументы дженерика
// cls.modifiers // DATA, SEALED, ABSTRACT...
// cls.annotations.filter { it.shortName.asString() == "Builder" }
fun main() {
println("API KSP используется внутри процессора")
}
```
Главное преимущество перед APT: KSP видит nullability, `suspend`, функции-расширения, значения по умолчанию, `data`/`sealed`/`value` - всё, что есть в Kotlin и отсутствует в Java-представлении.
## Многораундовая обработка
`process()` может вызываться несколько раз. Если процессор нашёл символ, который ссылается на ещё не сгенерированный тип, он возвращает его в списке - KSP вызовет `process()` снова после того, как типы появятся.
```kotlin
// внутри процессора:
// val (valid, deferred) = symbols.partition { it.validate() }
// valid.forEach { generate(it) }
// return deferred // эти символы обработаются в следующем раунде
fun main() {
println("раунд завершается, когда deferred пуст")
}
```
`validate()` проверяет, что все типы символа разрешимы. Возврат пустого списка означает «всё обработано».
## Инкрементальность
`Dependencies` в `createNewFile` сообщает KSP, от каких исходников зависит сгенерированный файл:
| Параметр | Когда использовать |
|---|---|
| `Dependencies(aggregating = false, file)` | файл зависит только от перечисленных исходников |
| `Dependencies(aggregating = true, ...)` | файл зависит от всех обработанных символов |
| `Dependencies.ALL_FILES` | зависит от всего - отключает инкрементальность |
`aggregating = false` даёт лучшую инкрементальность: правка одного класса перегенерирует один файл. `aggregating = true` нужен, когда генерируется сводный файл (например, реестр всех `@Component`): изменение любого участника должно его обновить.
Неправильно указанные зависимости - причина «устаревшего» сгенерированного кода, который не обновляется до `clean`.
## Опции процессора
```kotlin
// build.gradle.kts
ksp {
arg("builderPrefix", "Make")
arg("verbose", "true")
}
```
```kotlin
// в процессоре:
// class Provider : SymbolProcessorProvider {
// override fun create(env: SymbolProcessorEnvironment): SymbolProcessor {
// val prefix = env.options["builderPrefix"] ?: "Builder"
// return MyProcessor(env.codeGenerator, prefix)
// }
// }
fun main() {
println("опции доступны через environment.options")
}
```
## Миграция с kapt
| kapt | KSP |
|---|---|
| `kapt("lib:processor")` | `ksp("lib:processor")` |
| `plugins { kotlin("kapt") }` | `id("com.google.devtools.ksp")` |
| `kapt { arguments { arg(...) } }` | `ksp { arg(...) } ` |
| `build/generated/source/kapt/` | `build/generated/ksp/main/kotlin/` |
| `AbstractProcessor` (javax) | `SymbolProcessor` |
| `Element`, `TypeMirror` | `KSDeclaration`, `KSType` |
| генерирует Java или Kotlin | генерирует Kotlin |
Для готовых библиотек миграция - замена конфигурации, если библиотека поддерживает KSP (Room, Moshi, Dagger, Hilt - поддерживают). Для своего процессора нужно переписывать: API принципиально другой.
Смысл перехода - скорость. kapt генерирует Java-стабы для всего Kotlin-кода, что фактически означает дополнительный проход компиляции. Выигрыш обычно 2-4 раза на фазе обработки аннотаций.
## Диагностика
```
# что сгенерировано
find build/generated/ksp -name "*.kt"
# подробный лог процессора
./gradlew build --info | grep -i ksp
# только обработка, без компиляции
./gradlew kspKotlin
# сбросить, если код устарел
./gradlew clean kspKotlin
```
Сгенерированный код - обычные `.kt`-файлы: их можно открыть и прочитать. Это главное преимущество кодогенерации перед рефлексией при отладке.
> [!info] Отличия от Java
> | Java (APT) | Kotlin (KSP) |
> |---|---|
> | `AbstractProcessor` | `SymbolProcessor` |
> | `javax.lang.model.Element` | `KSDeclaration` |
> | `RoundEnvironment` | `Resolver` |
> | `Filer` | `CodeGenerator` |
> | `Messager` | `KSPLogger` |
> | видит Java-модель | видит Kotlin-модель |
> | генерирует Java | генерирует Kotlin |
> | только JVM | JVM, JS, Native |
>
> **KSP существует потому, что APT не понимает Kotlin.** Модель `javax.lang.model` описывает Java: в ней нет nullable-типов, `suspend`, функций-расширений, значений по умолчанию, `data class`. Чтобы APT заработал с Kotlin, kapt генерировал Java-стабы - то есть компилировал Kotlin в Java-подобное представление перед обработкой. Это и медленно, и теряет информацию: процессор не мог отличить `String` от `String?`.
>
> **Запрет на модификацию кода - сознательное ограничение, как и в APT.** Процессор может только добавлять файлы. Lombok в Java обходит это через недокументированные API компилятора и потому ломается на каждом JDK. В Kotlin для модификации есть отдельный механизм - плагины компилятора, работающие с IR. Разделение чёткое: KSP стабилен и прост, плагины мощны и нестабильны.
>
> **Кодогенерация заменяет рефлексию сильнее, чем в Java.** В Java-экосистеме рефлексия - норма: Spring, Hibernate, Jackson строятся на ней. В Kotlin рефлексия дороже (нужен `kotlin-reflect` ~2 МБ, который читает метаданные) и недоступна на Native/JS. Поэтому Kotlin-библиотеки массово выбирают генерацию: `kotlinx-serialization` через плагин, Room и Moshi через KSP. Результат - меньше рантайм-магии и ошибки конфигурации на этапе сборки.
>
> **Версионирование KSP отражает его связь с компилятором.** Исторически версия выглядела как `2.0.21-1.0.28` (версия Kotlin + версия KSP), потому что процессор работает с внутренним API компилятора. KSP2 перешёл на самостоятельную нумерацию (`2.3.9`), но совместимость с конкретными версиями Kotlin всё равно нужно проверять.
>
> Сравнить: [Reflection API](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **Версия KSP должна быть совместима с версией Kotlin.** Старый формат - `2.0.21-1.0.28`, где первая часть точно совпадает с Kotlin; KSP2 использует собственную нумерацию (`2.3.9`). Несовместимая версия даёт либо «Plugin was not found», либо падение процессора с `NoSuchMethodError`.
>
> **Процессор не может изменить существующий класс.** Нельзя дописать метод в `User` - только сгенерировать `UserBuilder` рядом. Если нужна модификация, это задача плагина компилятора, а не KSP.
>
> **Сгенерированный код не виден другому процессору в том же раунде.** Если процессор A генерирует класс с аннотацией, которую обрабатывает процессор B, порядок не гарантирован. Решение - возвращать символы из `process()` для следующего раунда.
>
> **Неправильные `Dependencies` ломают инкрементальность.** `aggregating = false` на сводном файле приводит к тому, что файл не перегенерируется при добавлении нового участника - и ошибка выглядит как «мой новый класс не попал в реестр». Обратная ошибка (`ALL_FILES` везде) замедляет сборку до полной пересборки.
>
> **Сгенерированный код отсутствует до первой сборки.** Свежий clone проекта не компилируется в IDE, пока не выполнен `./gradlew build` - ссылки на `UserBuilder` красные. Это нормально, но сбивает с толку.
>
> **IDE может не видеть сгенерированные исходники.** Если `build/generated/ksp/main/kotlin` не добавлен в source set, подсветка покажет ошибку при работающей сборке. Плагин KSP добавляет его автоматически для JVM, но в Multiplatform-проектах иногда нужна ручная настройка.
>
> **`type.resolve()` дорогой.** Разрешение типа - основная статья расходов процессора. Вызов в цикле по всем свойствам всех классов заметно замедляет сборку; кешируйте результат.
>
> **`validate()` нельзя пропускать.** Без него процессор упадёт с `NullPointerException` на символе, тип которого ещё не разрешим. Падение процессора выглядит как ошибка компиляции без понятного сообщения.
>
> **kapt и KSP вместе не дают выигрыша.** Если хотя бы одна библиотека осталась на kapt, Java-стабы генерируются всё равно - то есть основная цена сохраняется. Миграцию делают целиком.
>
> **`ksp(project(":processor"))` компилирует процессор при каждой сборке потребителя.** Это нормально для проекта, но в CI может удваивать время. Для стабильного процессора лучше опубликовать его как артефакт.
>
> **Файл `META-INF/services` обязателен и чувствителен к имени.** Путь должен быть точно `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`, содержимое - полное имя класса провайдера. Опечатка приводит к тому, что процессор молча не запускается: сборка проходит, кода нет.
>
> **`logger.error()` останавливает сборку, `logger.info()` не виден без `--info`.** Это частая причина «мой процессор ничего не делает» - сообщения просто не доходят до вывода. Для отладки используйте `logger.warn()`.
Далее: [Линтеры и формат](Линтеры%20и%20формат.md)
@@ -0,0 +1,273 @@
---
title: kotlinc
description: Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги
tags:
- kotlin
- tooling
---
`kotlinc` - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода.
```
kotlinc Hello.kt -include-runtime -d hello.jar
java -jar hello.jar
```
```kotlin
fun main() {
println("скомпилировано kotlinc")
}
```
Проверить версию:
```
$ kotlinc -version
info: kotlinc-jvm 2.4.21 (JRE 27)
```
Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода).
## Установка
| Способ | Команда |
|---|---|
| Homebrew | `brew install kotlin` |
| SDKMAN | `sdk install kotlin` |
| Snap | `snap install --classic kotlin` |
| Chocolatey | `choco install kotlinc` |
| Вручную | архив с GitHub Releases + `PATH` |
Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам.
## Три режима компиляции
**В каталог с `.class`** - для дальнейшей работы с байт-кодом:
```
kotlinc Demo.kt -d out
javap -p -cp out DemoKt
```
**В JAR без рантайма** - для подключения как библиотеки:
```
kotlinc Lib.kt -d lib.jar
```
JAR не запустится напрямую: в нём нет stdlib.
**В исполняемый JAR** - со встроенной stdlib (~5 МБ):
```
kotlinc App.kt -include-runtime -d app.jar
java -jar app.jar
```
## Ключевые флаги
| Флаг | Что делает |
|---|---|
| `-d <путь>` | каталог или `.jar` для результата |
| `-include-runtime` | вложить stdlib в JAR |
| `-classpath` / `-cp` | зависимости |
| `-jvm-target <v>` | версия байт-кода (по умолчанию **1.8**) |
| `-module-name <n>` | имя модуля (влияет на манглирование `internal`) |
| `-nowarn` | подавить предупреждения |
| `-Werror` | считать предупреждения ошибками |
| `-Wextra` | дополнительные проверки K2 |
| `-progressive` | исправления применяются сразу, без цикла миграции |
| `-language-version <v>` | ограничить возможности языка |
| `-api-version <v>` | запретить API новее указанной версии stdlib |
| `-opt-in <fq.name>` | включить API, требующее opt-in |
| `-verbose` | подробный лог |
| `-X` | список экспериментальных флагов |
**Про `-jvm-target`.** Значение по умолчанию - `1.8`, то есть байт-код major version 52, даже на JDK 27:
```
$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major
major version: 52
$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major
major version: 65
```
Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно.
**Про `-Werror`.** Останавливает сборку на любом предупреждении:
```
$ kotlinc -Werror Demo.kt -d out
error: warnings found and -Werror specified
Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело.
```
В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - `allWarningsAsErrors.set(true)`.
```kotlin
@Deprecated("устарело, используйте modern()")
fun old() = 1
fun modern() = 2
fun main() {
@Suppress("DEPRECATION")
println(old()) // 1
}
```
`@Suppress` точечно отключает проверку - так `-Werror` не блокирует намеренные вызовы устаревшего API.
## Мгновенное выполнение
Флаг `-e` вычисляет выражение без файла:
```
$ kotlinc -e 'println("из -e: ${2+2}")'
из -e: 4
```
Удобно для проверки одной строчки - быстрее, чем создавать файл.
## Скрипты .main.kts
Файл с расширением `.main.kts` - это Kotlin-скрипт: код на верхнем уровне, без `fun main`.
```
$ cat hello.main.kts
println("из скрипта: ${listOf(1,2,3).sum()}")
$ kotlinc -script hello.main.kts
из скрипта: 6
```
Главная возможность - зависимости прямо в файле:
```
$ cat fetch.main.kts
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
import kotlinx.coroutines.*
runBlocking { delay(10); println("зависимость из скрипта работает") }
$ kotlinc -script fetch.main.kts
зависимость из скрипта работает
```
`@file:DependsOn` скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект.
Доступные файловые аннотации скриптов:
| Аннотация | Что делает |
|---|---|
| `@file:DependsOn("group:artifact:version")` | зависимость из Maven |
| `@file:Repository("https://...")` | дополнительный репозиторий |
| `@file:CompilerOptions("-jvm-target", "21")` | флаги компилятора |
| `@file:Import("other.main.kts")` | подключить другой скрипт |
Аргументы командной строки доступны как `args`:
```kotlin
// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt
// val path = args.firstOrNull() ?: error("укажите файл")
// println(java.io.File(path).readLines().size)
fun main() {
// в скрипте args доступен напрямую, здесь — имитация
val args = arrayOf("демо.txt")
println("аргументов: ${args.size}") // аргументов: 1
}
```
Shebang делает скрипт исполняемым:
```
#!/usr/bin/env kotlin
println("запущено как ./script.main.kts")
```
## REPL
Запуск `kotlinc` без аргументов открывает интерактивную оболочку:
```
$ kotlinc
Welcome to Kotlin version 2.4.21
>>> val x = 42
>>> x * 2
res1: kotlin.Int = 84
>>> :quit
```
REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или `.main.kts`.
## Смешанная компиляция вручную
Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен:
```
# 1. Kotlin видит Java-исходники
kotlinc -cp . Lib.kt JLib.java -d out
# 2. javac видит скомпилированные Kotlin-классы
javac -cp out -d out Main.java
# 3. запуск с stdlib в classpath
java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main
```
Kotlin-компилятор принимает `.java`-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает:
```
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
```
Подробнее: [Структура проекта](Структура%20проекта.md)
## Демон компилятора
Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому `kotlinc` на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды).
Настройка демона в `gradle.properties`:
```
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.jvmargs=-Xmx4g
kotlin.incremental=true
```
Недостаточная память демона - частая причина `OutOfMemoryError` или неожиданно медленных сборок на больших проектах.
## Полезные комбинации
```
# что реально попало в байт-код
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
# аннотации nullability и метаданные
kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata"
# проверить, что код чистый
kotlinc -Werror -Wextra Demo.kt -d /dev/null
# совместимость со старым языком
kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out
# список экспериментальных флагов
kotlinc -X
```
Подробнее про чтение байт-кода: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac Demo.java` | `kotlinc Demo.kt` |
> | `java Demo` | `java -jar app.jar` или `kotlin DemoKt` |
> | `--release 21` | `-jvm-target 21` |
> | `-Xlint:all -Werror` | `-Wextra -Werror` |
> | JEP 330: `java Demo.java` | `kotlinc -script` или `.main.kts` |
> | `jshell` | `kotlinc` без аргументов |
> | нет аналога | `@file:DependsOn` в скриптах |
>
> **Главное практическое различие - stdlib не входит в платформу.** `javac` компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от `kotlin-stdlib.jar`, поэтому нужен либо `-include-runtime`, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native).
>
> **`-jvm-target 1.8` по умолчанию - решение о совместимости, а не недоработка.** Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21.
>
> **Скрипты в Kotlin сильнее, чем однофайловый запуск в Java.** JEP 330 позволяет `java Demo.java`, но только с тем, что есть в JDK: зависимостей нет. `@file:DependsOn` в `.main.kts` скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **`-jvm-target` по умолчанию 1.8.** Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет.
>
> **JAR без `-include-runtime` не запускается:**
> ```
> $ kotlinc App.kt -d app.jar && java -jar app.jar
> Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
> ```
> Либо добавьте флаг, либо укажите stdlib в `-cp`.
>
> **`-include-runtime` даёт JAR на ~5 МБ.** Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений.
>
> **`-e` и `-script` требуют разных форм записи.** `-e` принимает выражение в кавычках, `-script` - путь к `.kts`-файлу. Передать `.kts` в `-e` или выражение в `-script` не получится.
>
> **Файл скрипта должен называться `*.main.kts`, не `*.kts`.** Расширение `.kts` зарезервировано для Gradle-скриптов с другим шаблоном: `@file:DependsOn` в нём не сработает.
>
> **Первый запуск `@file:DependsOn` медленный.** Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш `~/.m2`, но в CI без сохранённого кеша задержка повторится на каждом прогоне.
>
> **`-Werror` падает на предупреждениях из зависимостей.** Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный `@Suppress("DEPRECATION")`, а не отключение флага.
>
> **REPL не видит зависимостей.** `kotlinc` без аргументов даёт только stdlib: подключить библиотеку нельзя (есть `-cp`, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или `.main.kts`.
>
> **Kotlin-компилятор не компилирует `.java`-файлы.** Передача `.java` в аргументах нужна только для разрешения ссылок; сами классы потом компилирует `javac`. Пропустить второй шаг - получить `ClassNotFoundException` на Java-классе.
>
> **`-module-name` влияет на видимость `internal`.** `internal` компилируется в публичное имя с суффиксом модуля (`helper$main`). Компиляция двух частей проекта с разными `-module-name` сделает их `internal`-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало.
>
> **`-d /dev/null` для проверки работает не везде.** На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять.
Далее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
@@ -0,0 +1,205 @@
---
title: Инструментарий
description: Обзор инструментов Kotlin — компилятор, сборка, кодогенерация, статический анализ — и как они складываются в проект
tags:
- kotlin
- tooling
---
Язык - это половина дела. Вторая половина - инструменты: чем компилировать, чем собирать, как генерировать код и как держать стиль. В Kotlin эти роли распределены между четырьмя слоями.
```mermaid
graph TD
A["исходники .kt"] --> B["компилятор K2<br/>kotlinc"]
B --> C[".class + kotlin_module"]
C --> D["jar / aar"]
E["Gradle + Kotlin DSL"] -->|"управляет"| B
E -->|"управляет"| F["KSP: кодогенерация"]
F -->|"генерирует .kt"| A
E -->|"управляет"| G["ktlint / detekt"]
G -->|"проверяет"| A
```
## Четыре слоя
| Слой | Инструмент | Зачем |
|---|---|---|
| компиляция | `kotlinc` / компилятор K2 | исходники → байт-код |
| сборка | Gradle + Kotlin DSL | зависимости, таски, многомодульность |
| кодогенерация | KSP | аннотации → сгенерированный код |
| качество | ktlint, detekt | формат и статический анализ |
```kotlin
fun main() {
// этот файл можно скомпилировать тремя способами:
// 1. kotlinc Main.kt -include-runtime -d app.jar
// 2. gradle build (с build.gradle.kts)
// 3. kotlinc -script Main.main.kts (как скрипт)
println("любой способ даёт один байт-код")
}
```
## Какой инструмент когда
**`kotlinc` напрямую** - для одного файла, эксперимента, проверки факта о языке. Не масштабируется: нет управления зависимостями.
**Gradle** - стандарт для всего остального. Kotlin DSL (`build.gradle.kts`) - это Kotlin-код, поэтому работает автодополнение и проверка типов. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
**Maven** поддерживается плагином `kotlin-maven-plugin`, но экосистема Kotlin ориентирована на Gradle: Multiplatform, Compose и большинство плагинов существуют только для него.
**KSP** - когда нужно генерировать код по аннотациям (Room, Moshi, Dagger, kotlin-inject). Заменил `kapt`, который работал через Java-стабы и был в разы медленнее. Подробнее: [KSP](KSP.md)
## Минимальный проект
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(21)
}
application {
mainClass.set("com.example.MainKt")
}
tasks.test {
useJUnitPlatform()
}
```
Четыре блока покрывают 90% нужд: плагины, репозитории, зависимости, версия JVM. Остальное - настройка под конкретную задачу.
## Структура каталогов
```
project/
├── build.gradle.kts # сборка модуля
├── settings.gradle.kts # список модулей
├── gradle.properties # флаги Gradle и JVM
├── gradle/
│ ├── libs.versions.toml # version catalog
│ └── wrapper/ # gradle-wrapper.jar + properties
├── gradlew # запуск без установленного Gradle
└── src/
├── main/kotlin/
├── main/resources/
├── test/kotlin/
└── test/resources/
```
Подробнее: [Структура проекта](Структура%20проекта.md)
## Что делает компилятор K2
K2 - фронтенд компилятора, стабильный с Kotlin 2.0 и единственный в 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и даёт более умные приведения. Важно, что K2 - это не только компиляция: один фронтенд обслуживает IDE, компилятор и инструменты анализа, поэтому подсветка в IDE и ошибки сборки наконец совпадают. Подробнее: [Компилятор K2](Компилятор%20K2.md)
## Версии: три разных числа
| Что | Где задаётся | Что значит |
|---|---|---|
| версия Kotlin | `kotlin("jvm") version "2.4.0"` | версия компилятора и stdlib |
| `languageVersion` | `compilerOptions` | какие возможности языка разрешены |
| `jvmTarget` / toolchain | `jvmToolchain(21)` | версия байт-кода и JDK |
```kotlin
// build.gradle.kts
plugins { kotlin("jvm") version "2.4.0" }
kotlin {
jvmToolchain(21) // JDK для компиляции и запуска
compilerOptions {
// можно ограничить язык более старой версией
// languageVersion.set(KotlinVersion.KOTLIN_2_1)
allWarningsAsErrors.set(false)
}
}
```
Частая ошибка - считать их одним. Можно компилировать Kotlin 2.4 компилятором, ограничив язык версией 2.1 (для совместимости с библиотекой), и выдавать байт-код для JVM 8.
## Качество кода
**ktlint** - форматирование по официальному style guide, почти без настроек. **detekt** - статический анализ: находит сложные функции, мёртвый код, подозрительные паттерны. Они дополняют друг друга: первый про форму, второй про смысл. Подробнее: [Линтеры и формат](Линтеры%20и%20формат.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [kotlinc](kotlinc.md) | CLI, скрипты, REPL, ключевые флаги |
| [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) | конфигурации, version catalog, таски |
| [Структура проекта](Структура%20проекта.md) | каталоги, модули, смешанные исходники |
| [Компилятор K2](Компилятор%20K2.md) | фазы, что изменилось, флаги |
| [KSP](KSP.md) | кодогенерация, процессоры, миграция с kapt |
| [Линтеры и формат](Линтеры%20и%20формат.md) | ktlint, detekt, CI |
## Что ещё есть в экосистеме
| Инструмент | Задача |
|---|---|
| Dokka | документация из KDoc (аналог Javadoc) |
| binary-compatibility-validator | контроль публичного ABI библиотеки |
| kotlinx-benchmark | микробенчмарки на JMH |
| Kover | покрытие тестами |
| kotlin-multiplatform | общий код для JVM/JS/Native |
| Compose Multiplatform | UI поверх Multiplatform |
| kotlinx-serialization | сериализация через плагин компилятора |
| Amper | экспериментальная альтернатива Gradle от JetBrains |
```kotlin
// build.gradle.kts с типичным набором для библиотеки
plugins {
kotlin("jvm") version "2.4.0"
kotlin("plugin.serialization") version "2.4.0"
id("org.jetbrains.dokka") version "2.0.0"
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
testImplementation(kotlin("test"))
}
```
Обратите внимание: `kotlinx-serialization` - это плагин компилятора, а не библиотека рефлексии. Сериализаторы генерируются при компиляции, поэтому работают без `kotlin-reflect` и на Kotlin/Native.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac` | `kotlinc` (K2) |
> | Maven или Gradle (Groovy) | Gradle + Kotlin DSL |
> | annotation processing (APT) | KSP |
> | Checkstyle, SpotBugs, PMD | ktlint, detekt |
> | Javadoc | Dokka (KDoc) |
> | Lombok | возможности языка (`data class`, свойства) |
> | один таргет - JVM | JVM, JS, Native, Wasm |
>
> **Gradle в Kotlin-проектах не альтернатива, а стандарт.** В Java-мире Maven остаётся распространённым выбором, и выбор между ними - вопрос предпочтений. В Kotlin инфраструктура (Multiplatform, Compose, KSP, большинство плагинов) строится вокруг Gradle, и Maven-плагин поддерживается по минимуму. Это сужает выбор, но концентрирует усилия.
>
> **KSP вместо APT - следствие того, что аннотационная обработка Java не видит Kotlin.** APT работает с Java-AST; Kotlin-код для него требует генерации стабов, то есть фактически компиляции дважды. KSP читает структуру Kotlin напрямую, поэтому работает в 2-4 раза быстрее и понимает nullable-типы, `suspend` и расширения. Цена - процессоры нужно писать заново: API другой.
>
> **Lombok не нужен.** Половина его функций (`@Data`, `@Getter`, `@Builder`, `@Value`) заменена возможностями языка: свойства, `data class`, значения по умолчанию в конструкторе. Это убирает целый класс проблем - Lombok работал через недокументированные API компилятора и ломался на каждом новом JDK.
>
> **Одна система сборки обслуживает несколько платформ.** Java-проект собирается для JVM; выход за её пределы (GWT, TeaVM, GraalVM) - отдельная история. В Kotlin JS, Native и Wasm - полноправные таргеты в том же `build.gradle.kts`, с общим исходным кодом. Цена - сложность конфигурации Multiplatform-проекта заметно выше.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **Версия плагина Kotlin и версия stdlib должны совпадать.** Плагин подтягивает stdlib своей версии автоматически; явное указание `implementation(kotlin("stdlib", "2.3.0"))` при плагине 2.4.0 даёт конфликт и предупреждение. Не указывайте stdlib вручную вообще.
>
> **`jvmTarget` без toolchain ломается на другом JDK.** Если локально JDK 21, а в CI - 17, сборка с `jvmTarget = "21"` упадёт. `jvmToolchain(21)` решает: Gradle сам скачает нужный JDK.
>
> **`jvmTarget` Kotlin и `targetCompatibility` Java должны совпадать.** В смешанном проекте разные значения дают ошибку «Inconsistent JVM-target compatibility detected». `jvmToolchain` настраивает оба.
>
> **Значение по умолчанию `jvmTarget` - 1.8.** Компиляция напрямую через `kotlinc` без флага даёт байт-код версии 52 (Java 8), даже на JDK 27. Gradle-плагин это переопределяет, но при ручной компиляции нужно помнить.
>
> **kapt и KSP в одном проекте замедляют сборку.** Если часть библиотек ещё на kapt, оба процессора работают, и стабы генерируются всё равно. Миграцию стоит делать целиком.
>
> **Gradle без `gradlew` - источник расхождений.** Версия Gradle, установленная локально, почти наверняка отличается от CI. Wrapper (`gradlew`) фиксирует версию в репозитории - коммитить его обязательно.
>
> **`kotlin.incremental` может давать ложные сборки.** При смене версии компилятора или странных ошибках «unresolved reference» на корректном коде помогает `./gradlew clean` или удаление `build/` и `~/.gradle/caches/build-cache-1`.
>
> **Плагины компилятора фиксируют версию Kotlin.** `kotlin("plugin.serialization") version "2.4.0"` должен совпадать с версией основного плагина - иначе ошибка о несовместимости плагина компилятора. Используйте version catalog, чтобы версия была в одном месте.
Далее: [kotlinc](kotlinc.md)
@@ -0,0 +1,386 @@
---
title: Компилятор K2
description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги
tags:
- kotlin
- tooling
- compiler
---
K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки.
```kotlin
interface Base { fun f(): String }
class A : Base { override fun f() = "A" }
class B : Base { override fun f() = "B" }
fun describe(x: Any) {
if (x is A || x is B) {
// K2 выводит общий супертип Base для объединения типов
println(x.f())
}
}
fun main() {
describe(A()) // A
describe(B()) // B
}
```
В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения.
## Фазы компиляции
Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`:
```
$ kotlinc -verbose -Xreport-perf Demo.kt -d out
PERF: INIT 271 ms
PERF: ANALYZE 530 ms 11.311 loc/s
PERF: TRANSLATION to IR 144 ms 41.453 loc/s
PERF: KLIB WRITING 0 ms
PERF: IR LOWERING 122 ms 48.799 loc/s
PERF: BACKEND 75 ms 79.456 loc/s
```
```mermaid
graph LR
A[".kt"] --> B["INIT<br/>загрузка classpath"]
B --> C["ANALYZE<br/>фронтенд K2: FIR"]
C --> D["TRANSLATION<br/>FIR → IR"]
D --> E["IR LOWERING<br/>развёртка inline,<br/>suspend, лямбды"]
E --> F["BACKEND<br/>IR → байт-код"]
F --> G[".class + Metadata"]
```
| Фаза | Что происходит |
|---|---|
| INIT | чтение classpath, инициализация компилятора |
| ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR |
| TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов |
| IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов |
| BACKEND | IR → байт-код JVM (или JS, Native, Wasm) |
Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель.
## Что изменилось в умных приведениях
K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение.
**Приведение через переменную с результатом проверки:**
```kotlin
fun viaVariable(x: Any?) {
val isString = x is String
if (isString) {
// K2 приводит тип по val с результатом проверки
println(x.length)
}
}
fun main() {
viaVariable("шесть!") // 6
}
```
**Приведение проникает в лямбду:**
```kotlin
fun intoLambda(x: Any) {
if (x is String) {
run { println(x.length) } // приведение видно внутри лямбды
}
}
fun main() {
intoLambda("семь!!!") // 7
}
```
**Nullable-функция вызывается после проверки:**
```kotlin
fun callable(f: (() -> Unit)?) {
if (f != null) {
f() // без ?.invoke()
}
}
fun main() {
callable { println("лямбда вызвана") } // лямбда вызвана
}
```
**Исчерпывающий `when` по данным потока:**
```kotlin
sealed interface State
data object Loading : State
data class Done(val value: String) : State
fun render(s: State) = when (s) {
Loading -> "загрузка"
is Done -> "готово: ${s.value}"
}
fun main() {
println(render(Loading)) // загрузка
println(render(Done("данные"))) // готово: данные
}
```
`else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
Чего K2 **не** делает - приведения для изменяемого свойства класса:
```
$ kotlinc Demo.kt -d out
error: smart cast to 'String' is impossible, because 'value' is a mutable property
that could be mutated concurrently.
```
Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия:
```kotlin
class Holder { var value: String? = "есть" }
fun main() {
val h = Holder()
val snapshot = h.value
if (snapshot != null) {
println(snapshot.length) // 4
}
}
```
## Полезные флаги анализа
### -Wextra
Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции.
```
kotlinc -Wextra Demo.kt -d out
```
### -Xreturn-value-checker
Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять:
```
$ kotlinc -Xreturn-value-checker=full Demo.kt -d out
warning: unused return value of 'compute'.
compute()
^^^^^^^
```
Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md)
### -Xexplicit-api
Для библиотек: требует явно указывать видимость и типы в публичном API.
```
$ kotlinc -Xexplicit-api=strict Lib.kt -d out
error: visibility must be specified in explicit API mode.
class Public {
^^^^^^^^^^^^
error: visibility must be specified in explicit API mode.
fun noExplicitType() = 42
^^^^^^^^^^^^^^^^^^
```
```kotlin
// так проходит explicit API mode
public class Service {
public fun compute(): Int = 42
private fun internalHelper(): Int = 1
}
fun main() {
println(Service().compute()) // 42
}
```
В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации.
### Производительность
| Флаг | Что делает |
|---|---|
| `-Xbackend-threads=N` | кодогенерация в N потоков |
| `-Xreport-perf` | статистика по фазам (с `-verbose`) |
| `-Xlambdas={class\|indy}` | схема компиляции лямбд |
| `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк |
`-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд.
## Экспериментальные возможности языка
K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`.
| Флаг | Возможность |
|---|---|
| `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) |
| `-Xwhen-guards` | `when` с охранными условиями |
| `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды |
| `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` |
| `-Xname-based-destructuring` | деструктуризация по именам |
| `-Xnested-type-aliases` | `typealias` внутри класса |
| `-Xexplicit-backing-fields` | отдельный тип backing field и свойства |
| `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу |
| `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока |
| `-Xannotation-target-all` | `@all:` для аннотаций на свойствах |
```kotlin
// context parameters — в 2.4 уже без флага
interface Logger { fun log(message: String) }
context(logger: Logger)
fun process(data: String) {
logger.log("обработка: $data")
}
fun main() {
val logger = object : Logger {
override fun log(message: String) = println(message)
}
with(logger) {
process("запись") // обработка: запись
}
}
```
Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
## Три версии, которые путают
```kotlin
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21) // версия байт-кода
languageVersion.set(KotlinVersion.KOTLIN_2_4) // возможности языка
apiVersion.set(KotlinVersion.KOTLIN_2_2) // доступный API stdlib
}
}
```
| Опция | Отвечает за | Типичная ошибка |
|---|---|---|
| `jvmTarget` | формат `.class` | оставить дефолтный 1.8 |
| `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении |
| `apiVersion` | какие функции stdlib видны | указать устаревшую версию |
`apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом).
Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
## Progressive mode
```kotlin
// build.gradle.kts
kotlin {
compilerOptions {
progressiveMode.set(true)
}
}
```
Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее.
## Плагины компилятора
K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код:
| Плагин | Что делает |
|---|---|
| `kotlinx-serialization` | генерирует сериализаторы при компиляции |
| `all-open` | делает классы `open` (для Spring, Mockito) |
| `no-arg` | добавляет конструктор без аргументов (JPA) |
| `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций |
| `kotlin-jpa` | предустановленный `no-arg` для JPA |
| `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде |
| `power-assert` | подробные сообщения в `assert` |
| `compose` | оптимизация composable-функций |
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
kotlin("plugin.serialization") version "2.4.0"
kotlin("plugin.spring") version "2.4.0"
kotlin("plugin.jpa") version "2.4.0"
}
```
Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md)
## Инкрементальная компиляция
```
# gradle.properties
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true
kotlin.daemon.jvmargs=-Xmx2g
```
Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI.
Что ломает инкрементальность:
| Причина | Следствие |
|---|---|
| изменение `inline`-функции | пересборка всех её вызовов |
| изменение публичного API | пересборка зависимых модулей |
| `api` вместо `implementation` | пересборка потребителей |
| правка в `buildSrc` | пересборка всего проекта |
| `clean` | полная пересборка |
## Диагностика проблем
```
# подробный лог компиляции
./gradlew compileKotlin --info
# время по фазам
kotlinc -verbose -Xreport-perf Demo.kt -d out
# что попало в байт-код
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
# проверить без записи результата
./gradlew compileKotlin --rerun-tasks
# сбросить кеши при странных ошибках
./gradlew clean && rm -rf .gradle .kotlin
```
Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) |
> | компилятор только для JVM | JVM, JS, Native, Wasm |
> | нет анализа потока для приведений | умные приведения |
> | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение |
> | аннотационные процессоры (APT) | плагины компилятора + KSP |
> | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` |
> | IDE имеет свой анализатор | K2 общий для IDE и сборки |
>
> **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код.
>
> **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип.
>
> **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin.
>
> **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера.
>
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
> [!warning] Подводные камни
> **Умное приведение не работает для `var`-свойства класса:**
> ```kotlin
> class Holder { var value: String? = "есть" }
>
> fun main() {
> val h = Holder()
> // if (h.value != null) println(h.value.length) // ошибка компиляции
> val snapshot = h.value
> println(if (snapshot != null) snapshot.length else 0) // 4
> }
> ```
> Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия.
>
> **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда.
>
> **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности.
>
> **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск.
>
> **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий.
>
> **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI.
>
> **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше.
>
> **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением.
>
> **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches».
>
> **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает.
>
> **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами.
Далее: [KSP](KSP.md)
@@ -0,0 +1,385 @@
---
title: Линтеры и формат
description: ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI
tags:
- kotlin
- tooling
- quality
---
Два инструмента с разными задачами: **ktlint** приводит код к официальному стилю и умеет исправлять сам, **detekt** ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции.
| | ktlint | detekt |
|---|---|---|
| проверяет | форматирование и имена | сложность, запахи кода |
| автоисправление | да, большинство правил | частично |
| настройка | `.editorconfig`, минимум правил | `detekt.yml`, ~200 правил |
| нужен ли compile classpath | нет | нет (есть опциональный type resolution) |
| скорость | быстро | медленнее |
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
id("org.jlleitschuh.gradle.ktlint") version "12.1.2"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
repositories { mavenCentral() }
detekt {
buildUponDefaultConfig = true
allRules = false
}
```
## ktlint
Реализует официальный Kotlin coding conventions. Настроек почти нет - это и есть идея: не спорить о стиле.
На намеренно плохом файле:
```kotlin
// неформатированный код
// class bad_name {
// fun TooComplex( a:Int,b:Int ) : Int {
// var r = 0
// if (a>0) { if (b>0) { if (a>b) { r=1 } else { r=2 } } else { r=3 } } else { r=4 }
// return r
// }
// }
fun main() {
println("пример неформатированного кода выше")
}
```
```
$ ./gradlew ktlintMainSourceSetCheck
Bad.kt:3:7 Class or object name should start with an uppercase letter and use camel case (cannot be auto-corrected)
Bad.kt:4:9 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
Bad.kt:4:20 Unexpected spacing after "("
Bad.kt:4:23 Missing spacing after ":"
Bad.kt:4:27 Missing spacing after ","
Bad.kt:6:14 Missing spacing around ">"
Bad.kt:6:18 A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block. (cannot be auto-corrected)
Bad.kt:6:20 Missing newline after '{'
```
`./gradlew ktlintFormat` исправляет всё, что помечено как исправимое:
```kotlin
// после ktlintFormat
class BadName {
fun tooComplex(
a: Int,
b: Int,
): Int {
var r = 0
if (a > 0) {
if (b > 0) {
r = if (a > b) 1 else 2
} else {
r = 3
}
} else {
r = 4
}
return r
}
}
fun main() {
println(BadName().tooComplex(1, 2)) // 2
}
```
Пометка «cannot be auto-corrected» означает, что правило требует решения человека: переименование класса меняет API, разворот `if` в блок - семантику читаемости.
## Настройка ktlint
Через `.editorconfig` в корне проекта:
```
root = true
[*.{kt,kts}]
indent_size = 4
indent_style = space
max_line_length = 120
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_function-naming = disabled
ktlint_code_style = ktlint_official
```
| Параметр | Значение |
|---|---|
| `ktlint_code_style` | `ktlint_official`, `intellij_idea`, `android_studio` |
| `max_line_length` | длина строки (`off` отключает) |
| `ktlint_standard_<rule> = disabled` | отключить конкретное правило |
| `ij_kotlin_allow_trailing_comma` | разрешить висячую запятую |
Важно, что `.editorconfig` читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за `.editorconfig` вместо настроек плагина.
Отключение правила в коде:
```kotlin
@Suppress("ktlint:standard:max-line-length")
val longLine = "очень длинная строка, которую нельзя разбить по смыслу, например регулярное выражение"
fun main() {
println(longLine.length) // 85
}
```
## detekt
Около 200 правил, разбитых на наборы. Запуск на том же плохом файле:
```
$ ./gradlew detekt
Bad.kt:3:7: The file name 'Bad' does not match the name of the single top-level declaration 'bad_name'. [MatchingDeclarationName]
Bad.kt:1:1: The package declaration does not match the actual file location. [InvalidPackageDeclaration]
Bad.kt:3:7: Class and Object names should match the pattern: [A-Z][a-zA-Z0-9]* [ClassNaming]
Bad.kt:4:9: Function names should match the pattern: [a-z][a-zA-Z0-9]* [FunctionNaming]
Bad.kt:6:72: This expression contains a magic number. [MagicNumber]
Bad.kt:9:24: Private property `x` is unused. [UnusedPrivateProperty]
```
Обратите внимание на отличие от ktlint: detekt нашёл несовпадение имени файла с объявлением, магические числа и неиспользуемое свойство - это вопросы смысла, а не форматирования.
| Набор правил | Что проверяет |
|---|---|
| `complexity` | длинные функции, высокая цикломатическая сложность, много параметров |
| `coroutines` | неправильное использование `suspend`, `GlobalScope`, блокирующие вызовы |
| `empty-blocks` | пустые `catch`, `if`, функции |
| `exceptions` | `catch (e: Exception)`, проглоченные исключения, `throw` в `finally` |
| `naming` | соответствие конвенциям имён |
| `performance` | `forEach` вместо `for`, лишние аллокации, `spread`-оператор |
| `potential-bugs` | `!!`, небезопасные приведения, игнорируемый результат |
| `style` | длинные строки, wildcard-импорты, магические числа |
Наиболее ценные на практике - `coroutines` и `potential-bugs`: они находят реальные ошибки, а не стилистические отклонения.
## Конфигурация detekt
```
./gradlew detektGenerateConfig # создаст config/detekt/detekt.yml
```
```yaml
# config/detekt/detekt.yml
build:
maxIssues: 0
excludeCorrectable: false
complexity:
LongMethod:
active: true
threshold: 60
LongParameterList:
active: true
functionThreshold: 6
ignoreDefaultParameters: true
CyclomaticComplexMethod:
active: true
threshold: 15
style:
MagicNumber:
active: true
ignoreNumbers: ['-1', '0', '1', '2']
ignoreHashCodeFunction: true
excludes: ['**/test/**']
MaxLineLength:
maxLineLength: 120
WildcardImport:
active: true
potential-bugs:
UnsafeCallOnNullableType:
active: true
coroutines:
GlobalCoroutineUsage:
active: true
InjectDispatcher:
active: true
```
```kotlin
// build.gradle.kts
detekt {
buildUponDefaultConfig = true // своя конфигурация дополняет дефолтную
config.setFrom("$projectDir/config/detekt/detekt.yml")
baseline = file("$projectDir/config/detekt/baseline.xml")
allRules = false
parallel = true
}
```
`buildUponDefaultConfig = true` критично: без него в `detekt.yml` нужно перечислить **все** правила, и забытое правило окажется отключённым.
## Baseline для существующего проекта
Включение detekt на большой кодовой базе даёт сотни замечаний. Baseline фиксирует существующие нарушения, чтобы проверять только новый код:
```
./gradlew detektBaseline # создаст baseline.xml
```
```xml
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Bad.kt$6:72</ID>
<ID>UnusedPrivateProperty:Bad.kt$private val x</ID>
</CurrentIssues>
</SmellBaseline>
```
Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда.
## Подавление правил
```kotlin
@Suppress("MagicNumber")
fun httpTimeout() = 30_000
class Legacy {
@Suppress("TooGenericExceptionCaught", "SwallowedException")
fun risky() {
try {
error("сбой")
} catch (e: Exception) {
// осознанно проглочено: вызывающий код не должен знать о сбое
}
}
}
fun main() {
println(httpTimeout()) // 30000
Legacy().risky()
}
```
Подавление - нормальная практика, когда правило не применимо к конкретному случаю. Плохая практика - подавлять правило целиком в `detekt.yml`, если оно нарушается в двух местах из тысячи.
## Type resolution
detekt может работать с информацией о типах - тогда доступны правила, требующие знания classpath:
```kotlin
// build.gradle.kts
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "21"
}
tasks.register<io.gitlab.arturbosch.detekt.Detekt>("detektWithTypes") {
description = "detekt с разрешением типов"
setSource(files("src/main/kotlin"))
classpath.setFrom(
configurations.getByName("detekt"),
sourceSets["main"].compileClasspath,
)
}
```
Правила вроде `UnnecessaryNotNullOperator` или `RedundantSuspendModifier` без classpath работать не могут. Цена - заметно медленнее.
## Интеграция в CI
```yaml
# .github/workflows/check.yml
name: check
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew ktlintCheck detekt --continue
- uses: actions/upload-artifact@v4
if: always()
with:
name: reports
path: |
build/reports/detekt/
build/reports/ktlint/
```
`--continue` важен: без него сборка остановится на первой проваленной проверке, и вы не увидите остальные замечания.
## Git-хук
```bash
# .git/hooks/pre-commit
#!/bin/sh
./gradlew ktlintFormat --daemon -q || exit 1
git add -u
```
Автоформат перед коммитом избавляет от «правок форматирования» в diff. `detekt` в хук обычно не ставят - он слишком медленный для каждого коммита.
Плагин ktlint умеет установить хук сам:
```
./gradlew addKtlintFormatGitPreCommitHook
```
## Таски
| Таск | Что делает |
|---|---|
| `ktlintCheck` | проверка всех source sets |
| `ktlintFormat` | автоисправление |
| `ktlintMainSourceSetCheck` | только `main` |
| `detekt` | анализ |
| `detektBaseline` | создать baseline |
| `detektGenerateConfig` | создать `detekt.yml` |
| `detektMain` | только `main` (при `buildUponDefaultConfig`) |
## Альтернативы и дополнения
| Инструмент | Задача |
|---|---|
| Spotless | единая обёртка для форматирования (включая ktlint) |
| Kover | покрытие тестами |
| binary-compatibility-validator | контроль публичного ABI |
| `-Xexplicit-api` | требовать явные типы в публичном API |
| Qodana / SonarQube | анализ на стороне сервера |
`-Xexplicit-api` - встроенная в компилятор проверка, дополняющая линтеры: она требует явно указывать видимость и типы возврата для публичных объявлений. Подробнее: [Компилятор K2](Компилятор%20K2.md)
## Разумный минимум
1. `.editorconfig` с `ktlint_official` - согласованный стиль в IDE и CI.
2. `ktlintFormat` в pre-commit хуке - форматирование не попадает в diff.
3. detekt с `buildUponDefaultConfig` и baseline - новый код проверяется, старый не блокирует.
4. `ktlintCheck detekt --continue` в CI с публикацией отчётов.
5. `-Xexplicit-api=strict` для библиотек.
Этого достаточно; дальнейшая настройка правил - вопрос договорённостей команды, а не инструментов.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | Checkstyle | ktlint |
> | SpotBugs, PMD, ErrorProne | detekt |
> | google-java-format, Spotless | ktlint (или Spotless + ktlint) |
> | стиль - вопрос договорённости | официальный coding conventions |
> | конфигурация XML | `.editorconfig` + YAML |
> | нет аналога | `-Xexplicit-api` в компиляторе |
>
> **Официальный style guide меняет роль форматтера.** В Java конкурируют несколько стилей (Oracle, Google, AOSP), и Checkstyle приходится настраивать под выбранный. Kotlin имеет один официальный стиль от JetBrains, реализованный и в IDE, и в ktlint - настройка почти не нужна, и споров о форматировании не возникает. Это сознательная позиция: единый стиль важнее возможности выбрать свой.
>
> **`.editorconfig` как единый источник правды - важнее, чем кажется.** В Java настройки Checkstyle и настройки форматирования IDE - разные файлы, которые расходятся. Файл `.editorconfig` читают и IntelliJ, и ktlint, поэтому «форматирование в IDE» и «проверка в CI» - буквально одна конфигурация. Это устраняет классический конфликт, когда IDE переформатирует код так, что падает сборка.
>
> **detekt берёт на себя то, что в Java распределено между тремя инструментами.** SpotBugs анализирует байт-код, PMD - исходники, ErrorProne встроен в компиляцию. detekt - один инструмент на исходниках, с опциональным разрешением типов. Проще в настройке, но находит меньше: без анализа байт-кода часть проблем (например, утечки ресурсов по потоку данных) недоступна.
>
> **Правила для корутин - специфика, которой нет аналога.** Набор `coroutines` в detekt находит `GlobalScope`, блокирующие вызовы внутри `suspend`, захардкоженные диспетчеры. В Java параллельные ошибки (блокировка в пуле, утечка `ExecutorService`) ловятся хуже, потому что нет структуры, которую можно статически проверить. Структурированная конкурентность Kotlin даёт линтеру опору. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md)
>
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md)
> [!warning] Подводные камни
> **`buildUponDefaultConfig = false` (по умолчанию) отключает все неперечисленные правила.** Свой `detekt.yml` без этого флага заменяет дефолтную конфигурацию целиком: сборка зелёная, проверок нет. Это самая частая ошибка настройки detekt.
>
> **detekt падает на слишком новом JDK.** Версия 1.23.8 на JDK 26 завершается с `GradleException: 26.0.1` - встроенный парсер не знает такой версии байт-кода. Запускайте таск на поддерживаемом JDK: `jvmToolchain` для компиляции и отдельный JDK для detekt, или обновите detekt.
>
> **ktlint и detekt могут конфликтовать по правилам.** Оба проверяют длину строки и имена, и при разных настройках код, исправленный одним, нарушает правило другого. Задавайте `max_line_length` в `.editorconfig`, а в detekt - `MaxLineLength.maxLineLength` с тем же значением.
>
> **`ktlintFormat` может изменить семантику в редких случаях.** Перестановка импортов и добавление висячих запятых безопасны, но правила вроде `if-else` → выражение меняют структуру кода. После массового автоформата запускайте тесты.
>
> **Baseline легко превращается в свалку.** Файл создаётся раз и начинает жить вечно: правила «починим позже» не выполняются. Перегенерируйте baseline периодически и следите, чтобы он сокращался.
>
> **Правила с type resolution требуют classpath и замедляют анализ.** Без `classpath.setFrom(...)` правила вроде `UnnecessaryNotNullOperator` молча не работают. С ним detekt становится в разы медленнее - обычно такой таск запускают только в CI.
>
> **`MagicNumber` шумит в тестах.** Тесты полны литералов, и правило даёт десятки замечаний. Добавьте `excludes: ['**/test/**']` в конфигурацию, иначе придётся подавлять в каждом файле.
>
> **Висячие запятые требуют согласованности.** `ij_kotlin_allow_trailing_comma = true` в `.editorconfig` и настройка IDE должны совпадать, иначе IDE удаляет запятую, а ktlint требует её вернуть - бесконечный цикл правок.
>
> **Версия плагина ktlint и версия самого ktlint - разные числа.** `org.jlleitschuh.gradle.ktlint` версии 12.x по умолчанию использует ktlint 1.x; при необходимости версия задаётся через `ktlint { version.set("1.5.0") }`. Несогласованность приводит к разным результатам локально и в CI.
>
> **Проверка в CI без `--continue` скрывает часть проблем.** Сборка падает на `ktlintCheck`, и `detekt` не выполняется - разработчик исправляет форматирование, пушит, и только тогда узнаёт о замечаниях detekt. Два цикла вместо одного.
>
> **Git-хук с detekt делает коммиты медленными.** Полный анализ среднего проекта - десятки секунд. В хук ставьте только `ktlintFormat`, detekt оставьте для CI.
>
> **`@Suppress` с неверным именем правила молча не работает.** Имена правил detekt (`MagicNumber`) и ktlint (`ktlint:standard:max-line-length`) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется.
Далее: [Тестирование](../Тестирование/Тестирование.md)
@@ -0,0 +1,379 @@
---
title: Структура проекта
description: Каталоги исходников, пакеты, многомодульность, смешанный Kotlin+Java код и convention plugins
tags:
- kotlin
- tooling
- gradle
---
Структура Kotlin-проекта наследует соглашения Maven, но с двумя отличиями: каталог называется `kotlin`, а требование «путь пакета = путь каталога» необязательно.
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
├── gradlew
└── src/
├── main/
│ ├── kotlin/com/example/
│ └── resources/
└── test/
├── kotlin/com/example/
└── resources/
```
```kotlin
// src/main/kotlin/com/example/Main.kt
package com.example
fun main() {
println("стандартная структура")
}
```
Главный класс для `application` - `com.example.MainKt`: имя файла с суффиксом `Kt`. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
## Source sets
| Source set | Каталоги | Зависимости |
|---|---|---|
| `main` | `src/main/kotlin`, `src/main/java`, `src/main/resources` | `implementation`, `api` |
| `test` | `src/test/kotlin`, `src/test/java`, `src/test/resources` | `testImplementation` + всё из `main` |
Gradle-плагин Kotlin добавляет `src/main/java` в Kotlin source set, поэтому `.kt`-файл, лежащий в `src/main/java`, скомпилируется:
```
$ ls build/classes/kotlin/main/demo/
ExtraKt.class # из src/main/java/demo/Extra.kt
KotlinImpl.class # из src/main/kotlin/demo/KotlinImpl.kt
```
Работает, но вводит в заблуждение - держите Kotlin в `src/main/kotlin`.
Добавить свой source set:
```kotlin
// build.gradle.kts
sourceSets {
val integrationTest by creating {
kotlin.srcDir("src/integrationTest/kotlin")
resources.srcDir("src/integrationTest/resources")
compileClasspath += sourceSets["main"].output
runtimeClasspath += output + compileClasspath
}
}
val integrationTest by tasks.registering(Test::class) {
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
useJUnitPlatform()
}
```
Для этой задачи есть и готовое решение - плагин `jvm-test-suite`:
```kotlin
// build.gradle.kts
testing {
suites {
val test by getting(JvmTestSuite::class) {
useKotlinTest()
}
register<JvmTestSuite>("integrationTest") {
useKotlinTest()
dependencies {
implementation(project())
}
}
}
}
```
## Пакеты и каталоги
В Kotlin объявленный пакет не обязан совпадать с путём каталога:
```kotlin
// файл src/main/kotlin/Utils.kt — лежит в корне
package com.example.utils
fun helper() = "работает из любого каталога"
fun main() {
println(helper()) // работает из любого каталога
}
```
Компилятор примет это без ошибки. Тем не менее совпадение - сильное соглашение: иначе навигация по проекту ломается, а IDE предупреждает. Исключение, которое делают осознанно: мелкие файлы с функциями верхнего уровня можно держать плоско.
Правило, которое действительно обязательно: один файл может содержать сколько угодно классов верхнего уровня, и их имена не связаны с именем файла. Это заметно отличается от Java. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
## Как раскладывать файлы
```
com/example/
├── Main.kt # точка входа
├── domain/
│ ├── User.kt # data class User + связанные типы
│ ├── Order.kt
│ └── OrderStatus.kt # enum или sealed
├── repository/
│ ├── UserRepository.kt # interface
│ └── UserRepositoryImpl.kt
├── service/
│ └── OrderService.kt
├── api/
│ ├── Routes.kt # функции верхнего уровня
│ └── Dto.kt # несколько data class в одном файле
└── util/
├── Strings.kt # функции-расширения
└── Dates.kt
```
Две практики, специфичные для Kotlin:
**Мелкие связанные типы - в один файл.** `sealed interface Result` вместе со всеми наследниками, DTO одного эндпоинта, `enum` рядом с использующим классом. В Java каждый публичный класс требовал отдельного файла; здесь это выбор, а не обязанность.
**Расширения группируются по типу-получателю, а не по назначению.** `Strings.kt` со всеми расширениями `String`, `Collections.kt` - для коллекций. Это заменяет утилитные классы вроде `StringUtils`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Смешанный Kotlin + Java
Один модуль может содержать оба языка, причём зависимости допускаются в обе стороны одновременно:
```
src/main/
├── kotlin/demo/KotlinImpl.kt
└── java/demo/
├── JavaBase.java
└── UsesKotlin.java
```
```java
// src/main/java/demo/JavaBase.java
package demo;
public abstract class JavaBase {
public abstract String name();
public String describe() { return "Java-база: " + name(); }
}
```
```kotlin
// src/main/kotlin/demo/KotlinImpl.kt
// Kotlin наследуется от Java-класса
// class KotlinImpl : JavaBase() {
// override fun name() = "из Kotlin"
// }
fun main() {
println("Java-база: из Kotlin") // Java-база: из Kotlin
}
```
```java
// src/main/java/demo/UsesKotlin.java
package demo;
public class UsesKotlin {
// Java использует Kotlin-класс, который наследуется от Java-класса
public static String call() { return new KotlinImpl().describe(); }
}
```
Это работает, хотя выглядит как циклическая зависимость. Порядок компиляции:
```mermaid
graph LR
A["Java-исходники"] -->|"читаются как стабы"| B["kotlinc:<br/>компиляция Kotlin"]
B --> C["Kotlin .class"]
C -->|"в classpath"| D["javac:<br/>компиляция Java"]
D --> E["Java .class"]
```
`kotlinc` получает `.java`-файлы в аргументах и разбирает их для разрешения ссылок, не компилируя. Затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java может наследоваться от Kotlin-класса, который наследуется от Java-класса. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
Ограничение: эта схема работает в пределах одного модуля. Между модулями циклов быть не может.
## Многомодульный проект
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml
├── build-logic/ # convention plugins
│ ├── settings.gradle.kts
│ └── src/main/kotlin/
│ └── kotlin-conventions.gradle.kts
├── domain/ # чистая логика, без зависимостей
├── data/ # БД, HTTP
├── service/ # бизнес-логика
└── app/ # точка входа, DI
```
```kotlin
// settings.gradle.kts
rootProject.name = "myapp"
include("domain", "data", "service", "app")
```
```kotlin
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
```
```kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":service"))
implementation(project(":data"))
}
```
Направление зависимостей важно: `app` → `service` → `data` → `domain`, и `domain` не зависит ни от чего. Gradle обнаружит цикл между модулями и откажется собирать.
## Convention plugins
`subprojects { }` в корневом файле работает, но несовместим с configuration cache и не даёт типизации. Правильное решение - convention plugin:
```
build-logic/
├── settings.gradle.kts
├── build.gradle.kts
└── src/main/kotlin/kotlin-conventions.gradle.kts
```
```kotlin
// build-logic/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0")
}
```
```kotlin
// build-logic/src/main/kotlin/kotlin-conventions.gradle.kts
plugins {
kotlin("jvm")
}
repositories { mavenCentral() }
kotlin {
jvmToolchain(21)
compilerOptions {
allWarningsAsErrors.set(true)
}
}
dependencies {
"testImplementation"(kotlin("test"))
}
tasks.withType<Test> {
useJUnitPlatform()
}
```
```kotlin
// settings.gradle.kts
pluginManagement {
includeBuild("build-logic")
}
rootProject.name = "myapp"
include("domain", "data", "service", "app")
```
```kotlin
// domain/build.gradle.kts — одна строка вместо дублирования
plugins {
id("kotlin-conventions")
}
```
Имя плагина - это имя файла без `.gradle.kts`. Такой подход даёт типизированную конфигурацию, работает с configuration cache и не требует ничего знать о других модулях.
Исторически ту же роль играл каталог `buildSrc/`. Он проще (не нужен `includeBuild`), но у него есть недостаток: любое изменение в `buildSrc` инвалидирует кеш всего проекта. Для больших проектов предпочтителен `build-logic` как included build.
## Ресурсы
```kotlin
fun main() {
// чтение из src/main/resources
val stream = object {}.javaClass.getResourceAsStream("/config.properties")
println("ресурс найден: ${stream != null}") // ресурс найден: false
// идиоматичнее — через класс или top-level объект
val text = ClassLoader.getSystemResourceAsStream("missing.txt")
?.bufferedReader()?.readText() ?: "ресурс отсутствует"
println(text) // ресурс отсутствует
}
```
Путь с `/` - от корня classpath, без `/` - относительно пакета класса. Файлы из `src/main/resources` попадают в корень JAR. Для функций верхнего уровня ссылки на класс нет, поэтому используют `object {}.javaClass` или `ClassLoader`.
## Файлы, которые нужны в репозитории
| Путь | Коммитить |
|---|---|
| `gradlew`, `gradlew.bat` | да |
| `gradle/wrapper/gradle-wrapper.jar` | да |
| `gradle/wrapper/gradle-wrapper.properties` | да |
| `gradle/libs.versions.toml` | да |
| `gradle.properties` | да |
| `.gradle/`, `build/` | нет |
| `local.properties` | нет |
| `.idea/` | частично (`codeStyles`, `inspectionProfiles`) |
| `*.iml` | нет |
```
# .gitignore
build/
.gradle/
local.properties
*.iml
.idea/*
!.idea/codeStyles/
!.idea/inspectionProfiles/
.kotlin/
```
Каталог `.kotlin/` появился в Kotlin 2.x - это служебные данные компилятора, их не коммитят.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `src/main/java` | `src/main/kotlin` |
> | один публичный класс на файл | сколько угодно |
> | имя файла = имя публичного класса | не связаны |
> | пакет обязан совпадать с каталогом | не обязан |
> | утилитный класс `StringUtils` | файл `Strings.kt` с расширениями |
> | `Main.class` | `MainKt.class` |
>
> **Освобождение от «один класс на файл» меняет раскладку кода.** В Java структура каталогов повторяла структуру типов: 20 классов - 20 файлов. В Kotlin файл - единица логической группировки, а не технического ограничения. `sealed interface` со всеми вариантами в одном файле читается целиком, а не по кускам. Побочный эффект: навигация «по файлам» работает хуже, и важнее осмысленные имена файлов.
>
> **Несовпадение пакета и каталога разрешено, но соглашение всё равно действует.** Kotlin не требует соответствия, потому что для компилятора оно не нужно: пакет указан в файле. Однако IDE, инструменты анализа и люди ожидают совпадения, поэтому отклоняются от него редко и только для плоских файлов с функциями.
>
> **Смешанная компиляция в одном модуле - то, чего нет в однодходовом `javac`.** Двухфазная схема (kotlinc читает Java-стабы, затем javac видит Kotlin-классы) позволяет взаимные ссылки между языками внутри модуля. Это ключевая возможность для миграции: можно конвертировать по одному файлу, не трогая остальное. Ограничение - только в пределах модуля.
>
> **Convention plugins - ответ Gradle на отсутствие наследования `pom.xml`.** В Maven общая конфигурация выносится в parent POM, и это декларативно. В Gradle аналога не было: писали `subprojects { }` или `allprojects { }`, что работает, но несовместимо с configuration cache и лишено типизации. Convention plugin - это обычный Gradle-плагин, лежащий в проекте: типизированный, кешируемый, переиспользуемый.
>
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [jar](../../Программирование%20на%20Java/JDK/jar.md)
> [!warning] Подводные камни
> **Главный класс для `application` - не имя файла.** Для `src/main/kotlin/com/example/Main.kt` это `com.example.MainKt`, а не `com.example.Main`. Ошибка проявляется как `ClassNotFoundException` при `./gradlew run`. Если в файле стоит `@file:JvmName("App")`, то класс - `com.example.App`.
>
> **Kotlin-файлы в `src/main/java` компилируются молча.** Плагин добавляет этот каталог в Kotlin source set, так что ошибки не будет - но структура станет путаной, а инструменты, ожидающие Java в `java/`, могут сбиться.
>
> **`buildSrc` инвалидирует кеш всего проекта.** Любая правка в `buildSrc` заставляет Gradle пересобрать все модули. На больших проектах это минуты на каждое изменение конфигурации - используйте `build-logic` как included build.
>
> **`subprojects { }` несовместим с configuration cache.** Он обращается к другим проектам в фазе конфигурации, что кеш запрещает. При включении `org.gradle.configuration-cache=true` появятся ошибки «invocation of 'Project.subprojects'». Решение - convention plugins.
>
> **Циклы между модулями запрещены.** Если `data` ссылается на `service`, а `service` на `data`, Gradle откажется собирать с ошибкой «Circular dependency between tasks». Внутри одного модуля цикл Kotlin↔Java допустим, между модулями - нет. Общие типы выносят в третий модуль.
>
> **Ресурс не найден из функции верхнего уровня.** `javaClass` недоступен вне класса; `this::class.java` в top-level функции не скомпилируется. Используйте `object {}.javaClass.getResourceAsStream(...)` или `ClassLoader.getSystemResourceAsStream(...)`.
>
> **Путь к ресурсу чувствителен к ведущему слэшу.** `getResourceAsStream("config.txt")` ищет относительно пакета класса, `getResourceAsStream("/config.txt")` - от корня classpath. Первый вариант неожиданно возвращает `null`, если класс лежит в пакете.
>
> **`gradle-wrapper.jar` обязателен в репозитории.** Без него `./gradlew` не работает, и сборка пойдёт локальным Gradle другой версии. Если политика запрещает бинарники, добавьте в CI шаг `gradle/actions/wrapper-validation`.
>
> **Переименование файла меняет имя класса.** `Utils.kt` даёт `UtilsKt`, после переименования в `Helpers.kt` - `HelpersKt`. Для Java-клиентов и рефлексии это ломающее изменение; фиксируйте имя через `@file:JvmName`.
>
> **Тесты не видят `internal` из другого модуля.** `internal` в Kotlin - это видимость в пределах модуля компиляции, и `src/test` - отдельный source set. Gradle-плагин специально связывает `test` с `main`, поэтому тесты `internal`-члены видят; но тесты **другого** модуля - нет.
>
> **Файл без `package` попадает в корневой пакет.** Такой класс не импортируется из других пакетов нормальным способом и недоступен из Java-кода в пакете. Всегда объявляйте пакет, кроме скриптов.
Далее: [Компилятор K2](Компилятор%20K2.md)
@@ -0,0 +1,336 @@
---
title: Result и runCatching
description: Ошибка как значение: Result, runCatching, свои sealed-типы и когда что выбирать
tags:
- kotlin
- exceptions
---
Исключение - не единственный способ сообщить об ошибке. Ошибку можно вернуть как значение, и тогда компилятор заставит её обработать.
Три уровня, от простого к гибкому:
| Способ | Что несёт ошибка | Когда |
|---|---|---|
| `T?` | ничего, только «не получилось» | причина не важна |
| `Result<T>` | `Throwable` | причина нужна, но типизировать её незачем |
| свой `sealed`-тип | что угодно | причин несколько и они разные |
## Nullable как простейший Result
Для одной очевидной причины сбоя `null` достаточно:
```kotlin
fun main() {
println("42".toIntOrNull()) // 42
println("abc".toIntOrNull()) // null
println("abc".toIntOrNull() ?: 0) // 0
}
```
Стандартная библиотека придерживается соглашения: `toInt()` бросает, `toIntOrNull()` возвращает `null`; `first()` бросает, `firstOrNull()` - нет.
```kotlin
fun main() {
val items = listOf(1, 2, 3)
println(items.getOrNull(10)) // null
println(items.firstOrNull { it > 5 }) // null
println(mapOf("a" to 1)["b"]) // null
}
```
Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
## Result
`Result<T>` хранит либо значение, либо `Throwable`.
```kotlin
fun main() {
val ok: Result<Int> = Result.success(42)
val fail: Result<Int> = Result.failure(IllegalStateException("сбой"))
println(ok.isSuccess) // true
println(fail.isFailure) // true
println(ok.getOrNull()) // 42
println(fail.getOrNull()) // null
println(fail.exceptionOrNull()?.message) // сбой
}
```
### runCatching
Оборачивает блок: результат - `Result`, исключение - `failure`.
```kotlin
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
fun main() {
println(parse("42")) // Success(42)
println(parse("abc").exceptionOrNull()?.javaClass?.simpleName) // NumberFormatException
}
```
Есть вариант-расширение с получателем:
```kotlin
fun main() {
val length = "привет".runCatching { length }
println(length.getOrNull()) // 6
}
```
### Извлечение значения
```kotlin
fun main() {
val ok = runCatching { 42 }
val fail = runCatching<Int> { error("сбой") }
println(ok.getOrNull()) // 42
println(fail.getOrDefault(0)) // 0
println(fail.getOrElse { e -> e.message!!.length }) // 4
println(ok.getOrThrow()) // 42
try {
fail.getOrThrow()
} catch (e: IllegalStateException) {
println("пробросилось: ${e.message}") // пробросилось: сбой
}
}
```
### Преобразования
```kotlin
fun main() {
val r = runCatching { "42" }
.map { it.toInt() } // только при успехе
.map { it * 2 }
println(r.getOrNull()) // 84
// mapCatching перехватывает исключение внутри преобразования
val bad = runCatching { "abc" }.mapCatching { it.toInt() }
println(bad.isFailure) // true
// map не перехватывает — исключение улетит наружу
val thrown = runCatching { runCatching { "abc" }.map { it.toInt() } }
println(thrown.isFailure) // true
}
```
Разница существенная: `map` предполагает, что преобразование не падает, `mapCatching` - что может.
```kotlin
fun main() {
val r = runCatching<Int> { error("сбой") }
.recover { 0 } // заменяем ошибку значением
println(r.getOrNull()) // 0
val folded = runCatching { "42".toInt() }
.fold(
onSuccess = { "значение $it" },
onFailure = { "ошибка ${it.message}" },
)
println(folded) // значение 42
}
```
### onSuccess и onFailure
Побочные эффекты без изменения значения - они возвращают сам `Result`:
```kotlin
fun main() {
runCatching { "abc".toInt() }
.onSuccess { println("получили $it") }
.onFailure { println("не вышло: ${it.message}") }
.getOrDefault(0)
.let { println("итог: $it") }
// не вышло: For input string: "abc"
// итог: 0
}
```
## Свой sealed-тип
Когда причин несколько и клиент должен их различать, `Result` слишком беден - в нём любая ошибка это `Throwable`.
```kotlin
sealed interface LoadResult {
data class Success(val user: String) : LoadResult
data object NotFound : LoadResult
data class Forbidden(val reason: String) : LoadResult
data class NetworkError(val retryAfter: Int) : LoadResult
}
fun load(id: Int): LoadResult = when {
id == 1 -> LoadResult.Success("Иван")
id == 2 -> LoadResult.Forbidden("нет прав")
id < 0 -> LoadResult.NetworkError(retryAfter = 5)
else -> LoadResult.NotFound
}
fun describe(r: LoadResult) = when (r) { // else не нужен
is LoadResult.Success -> "загружен ${r.user}"
LoadResult.NotFound -> "не найден"
is LoadResult.Forbidden -> "запрещено: ${r.reason}"
is LoadResult.NetworkError -> "повтор через ${r.retryAfter} с"
}
fun main() {
listOf(1, 2, 3, -1).forEach { println(describe(load(it))) }
// загружен Иван
// запрещено: нет прав
// не найден
// повтор через 5 с
}
```
Главное преимущество - компилятор проверяет, что обработаны все варианты. Добавили новый - все `when` перестанут компилироваться, пока его не учтут. С `Result` такой гарантии нет.
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
### Свой Result с типизированной ошибкой
```kotlin
sealed interface Outcome<out T, out E> {
data class Ok<T>(val value: T) : Outcome<T, Nothing>
data class Err<E>(val error: E) : Outcome<Nothing, E>
}
enum class ParseError { EMPTY, NOT_A_NUMBER, NEGATIVE }
fun parsePositive(s: String): Outcome<Int, ParseError> = when {
s.isBlank() -> Outcome.Err(ParseError.EMPTY)
s.toIntOrNull() == null -> Outcome.Err(ParseError.NOT_A_NUMBER)
s.toInt() < 0 -> Outcome.Err(ParseError.NEGATIVE)
else -> Outcome.Ok(s.toInt())
}
fun main() {
listOf("42", "", "abc", "-1").forEach { input ->
when (val r = parsePositive(input)) {
is Outcome.Ok -> println("'$input' -> ${r.value}")
is Outcome.Err -> println("'$input' -> ошибка ${r.error}")
}
}
// '42' -> 42
// '' -> ошибка EMPTY
// 'abc' -> ошибка NOT_A_NUMBER
// '-1' -> ошибка NEGATIVE
}
```
`Nothing` в позиции второго параметра позволяет `Ok` подходить под любой тип ошибки. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), [Вариантность](../Дженерики/Вариантность.md)
## Что выбрать
```mermaid
graph TD
A["Ошибка возможна"] --> B{"Вызывающий может<br/>что-то сделать?"}
B -->|"нет, это баг"| C["исключение:<br/>require, check, error"]
B -->|"да"| D{"Причину нужно<br/>различать?"}
D -->|"нет"| E["T?"]
D -->|"да, но обработка общая"| F["Result&lt;T&gt;"]
D -->|"да, по-разному"| G["свой sealed-тип"]
```
Практическое разделение:
- **исключение** - для нарушенных предпосылок: неверный аргумент, недопустимое состояние. Это ошибка программиста, её исправляют в коде, а не обрабатывают. Подробнее: [Проверки и контракты](Проверки%20и%20контракты.md)
- **`T?`, `Result`, sealed** - для ожидаемых ситуаций: нет записи в БД, сеть недоступна, ввод пользователя некорректен
## Граница с исключениями
`runCatching` удобен на границе: внешний код бросает, ваш возвращает значение.
```kotlin
class User7(val id: Int, val name: String)
fun fetchFromDb(id: Int): User7 =
if (id == 1) User7(1, "Иван") else throw NoSuchElementException("нет пользователя $id")
fun findUser(id: Int): User7? = runCatching { fetchFromDb(id) }.getOrNull()
fun main() {
println(findUser(1)?.name) // Иван
println(findUser(99)) // null
}
```
Обратное направление - когда API требует исключений, а внутри `Result`:
```kotlin
fun loadOrThrow(id: Int): String = runCatching {
require(id > 0) { "id должен быть положительным" }
"данные $id"
}.getOrThrow()
fun main() {
println(loadOrThrow(1)) // данные 1
try {
loadOrThrow(-1)
} catch (e: IllegalArgumentException) {
println(e.message) // id должен быть положительным
}
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | checked exceptions в сигнатуре | `Result<T>` или sealed-тип |
> | `Optional<T>` | `T?` - часть системы типов |
> | `Optional.map`/`orElse` | `?.let`/`?:` |
> | нет аналога `Result` в JDK | `Result<T>` в stdlib |
> | `try`/`catch` для управления | `runCatching` как выражение |
>
> **`Result` частично возвращает то, что отняло отсутствие checked exceptions.** Сигнатура `fun load(): Result<User>` сообщает, что функция может не справиться, и компилятор не даст использовать значение без разбора. Разница с checked exceptions в том, что это выбор автора функции, а не навязанное правило, и такие значения свободно проходят через `map`, `filter` и другие обобщённые функции.
>
> **`T?` вместо `Optional<T>`.** `Optional` - обёртка-объект: аллокация, два уровня значений (`null` и пустой `Optional`), неудобство в полях. Nullable-тип в Kotlin существует только при компиляции, а в байт-коде это обычная ссылка - накладных расходов нет:
> ```kotlin
> fun main() {
> val name: String? = null
> println(name?.uppercase() ?: "<нет>") // <нет>
> }
> ```
> ```java
> Optional<String> name = Optional.empty();
> System.out.println(name.map(String::toUpperCase).orElse("<нет>"));
> ```
>
> **Sealed-типы вместо иерархии исключений.** В Java разные причины сбоя обычно выражают классами исключений и ловят их по типу. Sealed-тип даёт то же различение, но с проверкой полноты при компиляции и без стоимости стектрейса.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **`runCatching` перехватывает всё, включая то, что перехватывать нельзя.** Он ловит `Throwable`, то есть `OutOfMemoryError`, `StackOverflowError` и - самое опасное - `CancellationException` в корутинах:
> ```kotlin
> // Требует kotlinx-coroutines-core.
> // suspend fun broken() {
> // runCatching { delay(1000) } // проглотит отмену корутины
> // }
> //
> // suspend fun fixed() = runCatching { delay(1000) }
> // .onFailure { if (it is CancellationException) throw it }
> ```
> В корутинах либо проверяйте тип вручную, либо используйте обычный `try`/`catch` с перехватом конкретных типов. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
>
> **`Result` нельзя вернуть из функции в публичном API без оговорок.** До Kotlin 1.5 это было запрещено совсем; сейчас разрешено, но тип по-прежнему `value class`, поэтому из Java он виден странно и его нельзя использовать как тип свойства:
> ```kotlin
> class Holder {
> // val r: Result<Int> = Result.success(1) // компилируется, но Result как поле боксируется
> private val value: Int = 1
> fun get(): Result<Int> = Result.success(value)
> }
>
> fun main() { println(Holder().get().getOrNull()) } // 1
> ```
>
> **`Result` стирает тип ошибки.** Все причины сбоя - `Throwable`, поэтому различать их приходится через `is`, и компилятор полноту не проверяет:
> ```kotlin
> fun main() {
> val r = runCatching<Int> { error("сбой") }
> val msg = when (val e = r.exceptionOrNull()) {
> null -> "успех"
> is IllegalStateException -> "состояние: ${e.message}"
> else -> "прочее" // else обязателен
> }
> println(msg) // состояние: сбой
> }
> ```
>
> **Создание `failure` всё равно создаёт исключение со стектрейсом** - то есть `Result` не даёт экономии на производительности, если ошибку формирует `runCatching`. Дешёвый вариант - sealed-тип без `Throwable` внутри.
>
> **Проигнорированный `Result` не вызывает предупреждения:**
> ```kotlin
> fun save(): Result<Unit> = runCatching { error("не записалось") }
>
> fun main() {
> save() // ошибка потеряна молча
> println(save().isFailure) // true — так правильно
> }
> ```
> В отличие от исключения, которое всплывёт само, забытый `Result` ничего не делает. Это основной риск подхода.
>
> **Цепочка `map` на `Result` может бросить.** `map` не перехватывает исключения из своей лямбды - для этого есть `mapCatching`.
>
> **`getOrThrow` на чужом `Result` бросает исходное исключение,** а не обёртку, поэтому стектрейс указывает на место первоначального сбоя, а не на `getOrThrow`. Для диагностики это хорошо, для чтения логов - неожиданно.
>
> **Смешение подходов в одном слое запутывает.** Если часть функций репозитория возвращает `null`, часть `Result`, а часть бросает, клиентский код обрастает тремя видами обработки. Выберите одно соглашение на слой.
Далее: [Проверки и контракты](Проверки%20и%20контракты.md)
@@ -0,0 +1,403 @@
---
title: Проверки и контракты
description: require, check, error, assert и контракты компилятора для умного приведения
tags:
- kotlin
- exceptions
---
Стандартная библиотека даёт четыре функции для проверки предпосылок. Выбор между ними определяется тем, **кто виноват** в нарушении.
| Функция | Бросает | Виноват | Что проверяет |
|---|---|---|---|
| `require` | `IllegalArgumentException` | вызывающий | аргументы |
| `check` | `IllegalStateException` | вызывающий или сам объект | состояние |
| `error` | `IllegalStateException` | - | недостижимая ветка |
| `assert` | `AssertionError` | - | инвариант при отладке |
## require - проверка аргументов
```kotlin
fun withdraw(balance: Long, amount: Long): Long {
require(amount > 0) { "сумма должна быть положительной, получено $amount" }
require(amount <= balance) { "недостаточно средств: $amount > $balance" }
return balance - amount
}
fun main() {
println(withdraw(100, 30)) // 70
try {
withdraw(100, -5)
} catch (e: IllegalArgumentException) {
println(e.message) // сумма должна быть положительной, получено -5
}
}
```
Сообщение передаётся лямбдой, поэтому строка собирается только при нарушении - в успешном случае интерполяция не выполняется.
```kotlin
fun main() {
val n = 5
require(n > 0) // без сообщения: "Failed requirement."
require(n > 0) { "дорогое сообщение: ${(1..1000).sum()}" } // лямбда не вызовется
println("проверки пройдены")
}
```
### requireNotNull
Проверяет и **возвращает** non-null значение:
```kotlin
fun greet(name: String?): String {
val safe = requireNotNull(name) { "имя обязательно" }
return "Привет, ${safe.uppercase()}" // safe имеет тип String
}
fun main() {
println(greet("иван")) // Привет, ИВАН
try {
greet(null)
} catch (e: IllegalArgumentException) {
println(e.message) // имя обязательно
}
}
```
Работает и как инструкция - после вызова переменная умно приводится:
```kotlin
fun process(input: String?): Int {
requireNotNull(input)
return input.length // smart cast: String? -> String
}
fun main() { println(process("abc")) } // 3
```
Это работает благодаря контракту, см. ниже.
## check - проверка состояния
```kotlin
class Connection2 {
private var open = false
fun connect() { open = true }
fun send(data: String): String {
check(open) { "соединение не открыто" }
return "отправлено: $data"
}
}
fun main() {
val c = Connection2()
try {
c.send("данные")
} catch (e: IllegalStateException) {
println(e.message) // соединение не открыто
}
c.connect()
println(c.send("данные")) // отправлено: данные
}
```
Разница с `require` только в типе исключения, но она информативна: `IllegalArgumentException` говорит «ты передал не то», `IllegalStateException` - «ты вызвал не вовремя».
`checkNotNull` - аналог `requireNotNull` для состояния:
```kotlin
class Service18 {
private var config: String? = null
fun init2(value: String) { config = value }
fun run2(): String {
val cfg = checkNotNull(config) { "сначала вызовите init2()" }
return "работаю с $cfg"
}
}
fun main() {
val s = Service18()
try {
s.run2()
} catch (e: IllegalStateException) {
println(e.message) // сначала вызовите init2()
}
s.init2("настройки")
println(s.run2()) // работаю с настройки
}
```
## error - недостижимая ветка
`error(msg)` бросает `IllegalStateException`. Её тип - `Nothing`, поэтому она подходит в любое выражение:
```kotlin
fun describe(code: Int): String = when (code) {
200 -> "ок"
404 -> "не найдено"
500 -> "ошибка сервера"
else -> error("неизвестный код: $code")
}
fun main() {
println(describe(200)) // ок
try {
describe(999)
} catch (e: IllegalStateException) {
println(e.message) // неизвестный код: 999
}
}
```
Благодаря типу `Nothing` компилятор понимает, что ветка не возвращает значение, и `when` остаётся выражением. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
`TODO()` - то же для незаписанного кода:
```kotlin
fun futureFeature(): String = TODO("реализовать после релиза")
fun main() {
try {
futureFeature()
} catch (e: NotImplementedError) {
println(e.message) // An operation is not implemented: реализовать после релиза
}
}
```
`TODO()` возвращает `Nothing`, поэтому подходит на место любого типа - код компилируется, пока функция не написана.
## assert - только для отладки
```kotlin
fun average(values: List<Int>): Double {
val result = values.sum().toDouble() / values.size
assert(result >= values.min()) { "среднее меньше минимума — ошибка в вычислении" }
return result
}
fun main() {
println(average(listOf(1, 2, 3))) // 2.0
}
```
`assert` проверяется **только** при запуске JVM с флагом `-ea` (`-enableassertions`). Без него вызов ничего не делает, но лямбда-сообщение всё равно остаётся объектом в байт-коде.
Практический вывод: `assert` в Kotlin почти не используют. Для проверок, которые должны работать в продакшене, берут `require` и `check`.
## Контракты
Контракт - способ сообщить компилятору то, что он не может вывести сам. Именно поэтому `requireNotNull` даёт умное приведение.
```kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun String?.isNotNullOrBlank(): Boolean {
contract { returns(true) implies (this@isNotNullOrBlank != null) }
return !this.isNullOrBlank()
}
fun main() {
val input: String? = "текст"
if (input.isNotNullOrBlank()) {
println(input.length) // smart cast: String? -> String
}
}
```
Без контракта `input.length` потребовал бы `!!` или `?.` - компилятор не знает, что `true` из функции означает non-null.
Стандартная библиотека использует контракты в `isNullOrEmpty`, `isNullOrBlank`, `require`, `check`, `requireNotNull`:
```kotlin
fun main() {
val s: String? = "abc"
if (!s.isNullOrEmpty()) {
println(s.length) // smart cast работает благодаря контракту
}
val n: Int? = 5
check(n != null)
println(n + 1) // 6, smart cast после check
}
```
### Виды контрактов
```kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun requirePositive(value: Int?) {
contract { returns() implies (value != null) } // вернулась -> value не null
require(value != null && value > 0) { "нужно положительное число" }
}
@OptIn(ExperimentalContracts::class)
inline fun <T> runOnce(block: () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return block()
}
fun main() {
val x: Int? = 5
requirePositive(x)
println(x + 1) // 6 — smart cast
val value: String // можно не инициализировать...
runOnce { value = "присвоено" } // ...потому что контракт гарантирует один вызов
println(value) // присвоено
}
```
`callsInPlace` с `EXACTLY_ONCE` позволяет присваивать `val` внутри лямбды - компилятор знает, что присваивание произойдёт ровно один раз. На этом работают `run`, `let`, `apply` и `with`.
| Контракт | Что обещает |
|---|---|
| `returns() implies (x)` | если функция вернулась нормально, условие `x` истинно |
| `returns(true) implies (x)` | если вернула `true`, условие `x` истинно |
| `returns(null) implies (x)` | если вернула `null`, условие `x` истинно |
| `callsInPlace(f, AT_MOST_ONCE)` | лямбда вызовется не больше одного раза |
| `callsInPlace(f, EXACTLY_ONCE)` | ровно один раз |
| `callsInPlace(f, AT_LEAST_ONCE)` | хотя бы один раз |
Требования к контракту:
- только для функций верхнего уровня (не для методов класса)
- блок `contract` - **первая** инструкция в теле
- API экспериментальный: нужен `@OptIn(ExperimentalContracts::class)`
- компилятор **не проверяет**, что контракт соответствует коду
Последний пункт - главный риск: контракт это обещание на доверии.
## Валидация данных: исключение или значение
`require` подходит для нарушений, которые означают баг. Для пользовательского ввода лучше вернуть ошибку как значение:
```kotlin
data class Form4(val email: String, val age: Int)
fun validate(email: String, age: Int): List<String> = buildList {
if ("@" !in email) add("email без @")
if (age !in 0..150) add("недопустимый возраст: $age")
}
fun create(email: String, age: Int): Result<Form4> {
val errors = validate(email, age)
return if (errors.isEmpty()) Result.success(Form4(email, age))
else Result.failure(IllegalArgumentException(errors.joinToString("; ")))
}
fun main() {
println(create("a@b.ru", 20).getOrNull()) // Form4(email=a@b.ru, age=20)
println(create("нет", 200).exceptionOrNull()?.message) // email без @; недопустимый возраст: 200
}
```
Разница: `require` падает на первой ошибке, а пользователю обычно нужен список всех. Подробнее: [Result и runCatching](Result%20и%20runCatching.md)
## Инварианты в конструкторе
```kotlin
class Range4(val from: Int, val to: Int) {
init {
require(from <= to) { "неверный диапазон: $from..$to" }
}
val size get() = to - from + 1
}
@JvmInline
value class Email(val value: String) {
init {
require("@" in value) { "не похоже на email: $value" }
}
}
fun main() {
println(Range4(1, 5).size) // 5
println(Email("a@b.ru").value) // a@b.ru
try {
Range4(5, 1)
} catch (e: IllegalArgumentException) {
println(e.message) // неверный диапазон: 5..1
}
}
```
Проверка в конструкторе гарантирует, что невалидного объекта не существует, - дальше по коду проверять не нужно. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md), [Инициализация](../ООП/Инициализация.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `if (x < 0) throw new IllegalArgumentException(...)` | `require(x >= 0) { ... }` |
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` + smart cast |
> | `assert x > 0 : "msg"` | `assert(x > 0) { "msg" }` |
> | Guava `Preconditions.checkArgument` | `require` в stdlib |
> | `throw new UnsupportedOperationException()` | `TODO()` |
> | нет аналога | контракты для smart cast |
>
> **`require` и `check` - это то, за чем в Java идут в Guava.** `Preconditions.checkArgument` и `checkState` из Guava выполняют ту же роль, но требуют внешней зависимости. В Kotlin они в stdlib и вдобавок `inline`, то есть вызова функции в байт-коде не остаётся.
>
> **Сообщение в лямбде экономит работу.** В Java `checkArgument("плохое значение: " + value)` собирает строку всегда, даже когда проверка пройдена. Передача лямбды плюс `inline` означает, что конкатенация происходит только при нарушении:
> ```kotlin
> fun main() {
> val value = 5
> require(value > 0) { "дорогая строка: ${value.toString().repeat(1000).length}" }
> println("лямбда не вызывалась")
> }
> ```
>
> **`requireNotNull` даёт smart cast, `Objects.requireNonNull` - нет.** В Java результат нужно присвоить: `this.name = Objects.requireNonNull(name)`. В Kotlin после проверки компилятор сам считает переменную non-null.
>
> **Контрактов в Java нет совсем.** Ближайший аналог - аннотации `@Nullable`/`@NotNull` и внешние анализаторы, которые работают вне компилятора и не влияют на типизацию.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **`assert` по умолчанию выключен.** Без `-ea` при запуске JVM проверка не выполняется совсем:
> ```kotlin
> fun main() {
> assert(1 == 2) { "это сообщение вы не увидите без -ea" }
> println("программа продолжила работу")
> }
> ```
> Код, который полагается на `assert` для защиты инвариантов в продакшене, не защищён ничем. Используйте `check`.
>
> **Путаница `require` и `check`.** Тип исключения - часть контракта: вызывающий может ловить `IllegalArgumentException`, чтобы отличить «я передал мусор» от «объект не готов». Перепутанные функции делают эту информацию ложной.
>
> **Сообщение без контекста бесполезно:**
> ```kotlin
> fun main() {
> val size = -1
> try {
> require(size >= 0) { "неверный размер" } // какой именно?
> } catch (e: IllegalArgumentException) {
> println(e.message)
> }
> try {
> require(size >= 0) { "размер должен быть >= 0, получено $size" }
> } catch (e: IllegalArgumentException) {
> println(e.message) // размер должен быть >= 0, получено -1
> }
> }
> ```
>
> **`require` для пользовательского ввода - ошибка проектирования.** Некорректный ввод - штатная ситуация, а не баг. Исключение в этом случае заставляет клиентский код оборачивать каждый вызов в `try`/`catch` и сообщает только о первой проблеме.
>
> **Контракт не проверяется компилятором.** Неверный контракт ломает проверки типов - компилятор поверит обещанию и уберёт нужные проверки на `null`:
> ```kotlin
> import kotlin.contracts.ExperimentalContracts
> import kotlin.contracts.contract
>
> @OptIn(ExperimentalContracts::class)
> fun String?.lies(): Boolean {
> contract { returns(true) implies (this@lies != null) }
> return true // врёт: true даже для null
> }
>
> fun main() {
> val s: String? = null
> if (s.lies()) {
> try {
> println(s.length) // компилятор уверен, что s != null
> } catch (e: NullPointerException) {
> println("NPE в non-null коде из-за неверного контракта")
> }
> }
> }
> ```
>
> **Контракт нельзя объявить в методе класса** - только в функции верхнего уровня. Это ограничение текущей реализации.
>
> **Контракты экспериментальны** - `@OptIn(ExperimentalContracts::class)` обязателен, API может измениться.
>
> **`TODO()` компилируется и попадает в релиз.** Он возвращает `Nothing`, поэтому ошибок сборки не вызывает - только падение в рантайме. Перед релизом стоит проверять: `grep -r 'TODO(' src/`.
>
> **Проверки в `init` не спасают `data class` от `copy`** в версиях до Kotlin 2.1: `copy()` вызывает конструктор, то есть `init` выполнится, но саму валидацию обойти через `copy` нельзя - а вот приватный конструктор обойти можно. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
>
> **`require` внутри цикла по большой коллекции стоит дорого** не из-за проверки, а из-за того, что при нарушении создаётся исключение со стектрейсом. Для валидации списков собирайте список ошибок, а не бросайте на первой.
Дополнительно: [Система исключений](Система%20исключений.md)
Далее: [Коллекции](../Коллекции/Коллекции.md)
@@ -0,0 +1,370 @@
---
title: Система исключений
description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка
tags:
- kotlin
- exceptions
---
Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их.
```kotlin
fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен
fun main() {
println(parse("42")) // 42
try {
parse("abc")
} catch (e: NumberFormatException) {
println("не число: ${e.message}") // не число: For input string: "abc"
}
}
```
## Иерархия
```mermaid
graph TD
T["Throwable"] --> E["Exception"]
T --> R["Error"]
E --> RE["RuntimeException"]
E --> IOE["IOException"]
RE --> NPE["NullPointerException"]
RE --> IAE["IllegalArgumentException"]
RE --> ISE["IllegalStateException"]
RE --> CCE["ClassCastException"]
R --> OOM["OutOfMemoryError"]
R --> SOE["StackOverflowError"]
```
Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые.
| Класс | Смысл | Перехватывать |
|---|---|---|
| `Error` | сбой среды: нет памяти, переполнен стек | нет |
| `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код |
| прочие `Exception` | внешний сбой: сеть, файл, БД | да |
## try как выражение
`try` возвращает значение - это важное отличие от Java:
```kotlin
fun parseOrZero(s: String): Int = try {
s.toInt()
} catch (e: NumberFormatException) {
0
}
fun main() {
println(parseOrZero("42")) // 42
println(parseOrZero("abc")) // 0
}
```
Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**:
```kotlin
fun main() {
val result = try {
"из try"
} finally {
"из finally" // игнорируется
}
println(result) // из try
}
```
## throw - тоже выражение
Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию:
```kotlin
fun findUser(id: Int): String =
if (id > 0) "пользователь $id"
else throw IllegalArgumentException("неверный id: $id")
fun main() {
println(findUser(1)) // пользователь 1
val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто")
println(name) // пользователь 1
}
```
Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении.
## Перехват
```kotlin
fun risky(mode: Int): String = when (mode) {
1 -> throw IllegalArgumentException("плохой аргумент")
2 -> throw IllegalStateException("плохое состояние")
else -> "ок"
}
fun handle(mode: Int): String = try {
risky(mode)
} catch (e: IllegalArgumentException) {
"аргумент: ${e.message}"
} catch (e: IllegalStateException) {
"состояние: ${e.message}"
} catch (e: Exception) {
"прочее: ${e.message}"
} finally {
println("finally выполняется всегда")
}
fun main() {
println(handle(1)) // аргумент: плохой аргумент
println(handle(3)) // ок
}
```
Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми.
Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты:
```kotlin
fun main() {
try {
throw IllegalStateException("сбой")
} catch (e: RuntimeException) { // общий супертип
println("перехвачено: ${e::class.simpleName}")
}
try {
throw NumberFormatException("плохое число")
} catch (e: Exception) {
when (e) { // или when по типу
is NumberFormatException, is IllegalStateException ->
println("ожидаемая ошибка: ${e.message}")
else -> throw e // остальное пробрасываем
}
}
}
```
## Свои исключения
```kotlin
class InsufficientFundsException(
val required: Long,
val available: Long,
) : Exception("нужно $required, доступно $available")
class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
fun withdraw(balance: Long, amount: Long): Long {
if (amount > balance) throw InsufficientFundsException(amount, balance)
return balance - amount
}
fun main() {
println(withdraw(100, 30)) // 70
try {
withdraw(100, 500)
} catch (e: InsufficientFundsException) {
println("не хватает ${e.required - e.available}") // не хватает 400
}
}
```
Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`.
## Цепочка причин
```kotlin
class RepositoryException(message: String, cause: Throwable) : Exception(message, cause)
fun loadUser(id: String): String = try {
"пользователь ${id.toInt()}"
} catch (e: NumberFormatException) {
throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину
}
fun main() {
try {
loadUser("abc")
} catch (e: RepositoryException) {
println(e.message) // не удалось загрузить 'abc'
println("причина: ${e.cause?.message}") // причина: For input string: "abc"
}
}
```
Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя.
## use - автоматическое закрытие
Аналог try-with-resources:
```kotlin
import java.io.StringReader
fun main() {
val content = StringReader("данные").use { reader ->
reader.readText() // close() вызовется автоматически
}
println(content) // данные
}
```
`use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно.
```kotlin
class Resource(val name: String) : AutoCloseable {
fun work() = "работаю с $name"
override fun close() = println("$name закрыт")
}
fun main() {
val result = Resource("файл").use { it.work() }
println(result)
// файл закрыт
// работаю с файл
try {
Resource("другой").use { throw IllegalStateException("сбой") }
} catch (e: IllegalStateException) {
println("ресурс всё равно закрыт: ${e.message}")
}
}
```
## @Throws для Java
Kotlin не объявляет исключения, но Java-клиенту это может быть нужно:
```kotlin
import java.io.IOException
@Throws(IOException::class)
fun readFile(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "содержимое $path"
}
fun main() { println(readFile("a.txt")) } // содержимое a.txt
```
Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## Чего не делать
```kotlin
fun main() {
// проглатывание: ошибка исчезает
try {
error("сбой")
} catch (e: Exception) {
// ничего — худшая практика
}
// перехват Throwable: ловит OutOfMemoryError и StackOverflowError
try {
error("сбой")
} catch (e: Exception) { // не Throwable
println("перехвачено корректно")
}
// исключение как управление потоком
val items = listOf(1, 2, 3)
println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch
}
```
Правила:
- не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше
- не перехватывать `Throwable` и `Error`
- не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы
- всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя
Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md)
## Исключения в корутинах
Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя:
```kotlin
// Требует kotlinx-coroutines-core.
// suspend fun work() {
// try {
// delay(1000)
// } catch (e: CancellationException) {
// throw e // обязательно пробросить!
// } catch (e: Exception) {
// println("настоящая ошибка")
// }
// }
```
`catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | checked exceptions | **нет**, все непроверяемые |
> | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов |
> | `try` - инструкция | `try` - выражение |
> | `catch (A \| B e)` | нет аналога, нужен `when` |
> | try-with-resources | `use { }` |
> | `throw` - инструкция | `throw` - выражение типа `Nothing` |
>
> **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin:
>
> 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями.
> 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки.
> 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте.
>
> Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает.
>
> **`try` как выражение убирает `var`:**
> ```java
> int value; // Java: нужна изменяемая переменная
> try { value = Integer.parseInt(s); }
> catch (NumberFormatException e) { value = 0; }
> ```
> ```kotlin
> fun parseOrZero(s: String): Int =
> try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var
>
> fun main() { println(parseOrZero("abc")) } // 0
> ```
>
> **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc:
> ```kotlin
> /**
> * @throws IllegalArgumentException если id не положительный
> */
> fun load(id: Int): String {
> require(id > 0) { "id должен быть положительным" }
> return "данные $id"
> }
>
> fun main() { println(load(1)) } // данные 1
> ```
>
> **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается.
>
> **`return` в `finally` проглатывает исключение:**
> ```kotlin
> fun bad(): String {
> try {
> throw IllegalStateException("потеряется")
> } finally {
> return "из finally" // исключение исчезло!
> }
> }
>
> fun main() { println(bad()) } // из finally
> ```
> Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя.
>
> **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку».
>
> **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать.
>
> **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md)
>
> **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`:
> ```kotlin
> fun main() {
> println("abc".toIntOrNull() ?: 0) // без исключения
> // println("abc".toInt()) // исключение + стектрейс
> }
> ```
>
> **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию:
> ```kotlin
> fun main() {
> val results = mutableListOf<Int>()
> try {
> listOf("1", "abc", "3").forEach { results.add(it.toInt()) }
> } catch (e: NumberFormatException) {
> println("прервано на второй строке, собрано: $results") // [1]
> }
> }
> ```
> Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`.
>
> **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот.
Далее: [Result и runCatching](Result%20и%20runCatching.md)
+365
View File
@@ -0,0 +1,365 @@
---
title: List, Set, Map
description: Три семейства коллекций, их гарантии, стоимость операций и выбор между ними
tags:
- kotlin
- collections
---
| | `List` | `Set` | `Map` |
|---|---|---|---|
| порядок | по индексу | вставки (у `LinkedHashSet`) | вставки (у `LinkedHashMap`) |
| дубликаты | разрешены | запрещены | ключи уникальны |
| доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) |
| поиск элемента | O(n) | O(1) | O(1) по ключу |
| основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» |
## List
```kotlin
fun main() {
val list = listOf("a", "b", "c", "b")
println(list[1]) // b — доступ по индексу
println(list.getOrNull(10)) // null
println(list.getOrElse(10) { "?" }) // ?
println(list.indexOf("b")) // 1 — первое вхождение
println(list.lastIndexOf("b")) // 3
println(list.subList(1, 3)) // [b, c]
println(list.size) // 4 — дубликат сохранён
}
```
Доступ к краям и срезы:
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("${list.first()} ${list.last()}") // 1 5
println(list.firstOrNull { it > 3 }) // 4
println(list.take(2)) // [1, 2]
println(list.drop(2)) // [3, 4, 5]
println(list.takeLast(2)) // [4, 5]
println(list.slice(1..3)) // [2, 3, 4]
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
println(list.windowed(2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
println(list.reversed()) // [5, 4, 3, 2, 1]
}
```
Изменение через `MutableList`:
```kotlin
fun main() {
val list = mutableListOf(1, 2, 3)
list.add(4)
list.add(0, 0) // по индексу
list[1] = 10
list.removeAt(0)
list.remove(4) // по значению
println(list) // [10, 2, 3]
list += 5 // оператор += работает как add
list -= 2
println(list) // [10, 3, 5]
list.sortDescending() // сортировка на месте
println(list) // [10, 5, 3]
}
```
## Set
```kotlin
fun main() {
val set = setOf(1, 2, 3)
println(2 in set) // true — O(1)
println(set.contains(5)) // false
println(setOf(1, 2, 2, 3).size) // 3 — дубликат отброшен
}
```
Операции над множествами:
```kotlin
fun main() {
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b) // [1, 2, 3, 4, 5]
println(a intersect b) // [3]
println(a subtract b) // [1, 2]
println(a + b) // [1, 2, 3, 4, 5] — как union
println(a - b) // [1, 2]
}
```
Эти же функции работают на `List`, но результат всегда `Set`:
```kotlin
fun main() {
println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3]
println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List
}
```
### Три реализации Set
```kotlin
fun main() {
val linked = linkedSetOf(3, 1, 2) // порядок вставки
val hash = hashSetOf(3, 1, 2) // порядок не определён
val sorted = sortedSetOf(3, 1, 2) // отсортирован
println(linked) // [3, 1, 2]
println(sorted) // [1, 2, 3]
println(hash.size) // 3
}
```
| Реализация | Порядок | Поиск | Когда |
|---|---|---|---|
| `LinkedHashSet` (`setOf`) | вставки | O(1) | по умолчанию |
| `HashSet` (`hashSetOf`) | не определён | O(1) | экономия памяти, порядок не нужен |
| `TreeSet` (`sortedSetOf`) | сортировки | O(log n) | нужен отсортированный обход |
Элементы `Set` обязаны корректно реализовывать `equals` и `hashCode` - иначе множество работает неправильно. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
## Map
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
println(map["a"]) // 1
println(map["z"]) // null
println(map.getOrDefault("z", 0)) // 0
println(map.getOrElse("z") { -1 }) // -1
println(map.getValue("a")) // 1, бросает при отсутствии
println("a" in map) // true — проверка ключа
println(map.containsValue(2)) // true
println("${map.keys} ${map.values}") // [a, b] [1, 2]
}
```
`to` - инфиксная функция, создающая `Pair`:
```kotlin
fun main() {
val pair: Pair<String, Int> = "a" to 1
println("${pair.first} ${pair.second}") // a 1
val (k, v) = pair // деструктуризация
println("$k=$v") // a=1
println(listOf("a" to 1, "b" to 2).toMap()) // {a=1, b=2}
}
```
Изменение через `MutableMap`:
```kotlin
fun main() {
val map = mutableMapOf("a" to 1)
map["b"] = 2
map.put("c", 3)
map.putAll(mapOf("d" to 4))
map.remove("a")
println(map) // {b=2, c=3, d=4}
println(map.getOrPut("e") { 5 }) // 5 — вычислит и положит
println(map.getOrPut("e") { 99 }) // 5 — уже есть, лямбда не вызвана
map.merge("b", 10) { old, new -> old + new } // метод JDK
println(map) // {b=12, c=3, d=4, e=5}
}
```
`getOrPut` - основной инструмент кеширования: одна операция вместо проверки и вставки.
Обход:
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) print("$key=$value ") // a=1 b=2
println()
map.forEach { (key, value) -> print("$key$value ") } // a1 b2
println()
println(map.entries.first().key) // a
}
```
Деструктуризация в `for` и в лямбде - главная причина, почему обход `Map` в Kotlin короче, чем в Java.
Преобразования:
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
println(map.filter { it.value > 1 }) // {b=2, c=3}
println(map.filterKeys { it != "a" }) // {b=2, c=3}
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
println(map.map { "${it.key}${it.value}" }) // [a1, b2, c3] — это List
println(map.toList()) // [(a, 1), (b, 2), (c, 3)]
println(map.entries.sortedByDescending { it.value }.first().key) // c
}
```
### Три реализации Map
| Реализация | Порядок | Доступ | Когда |
|---|---|---|---|
| `LinkedHashMap` (`mapOf`) | вставки | O(1) | по умолчанию |
| `HashMap` (`hashMapOf`) | не определён | O(1) | экономия памяти |
| `TreeMap` (`sortedMapOf`) | сортировки ключей | O(log n) | диапазонные запросы |
## Выбор структуры
```mermaid
graph TD
A["Нужна коллекция"] --> B{"Пары ключ-значение?"}
B -->|да| M["Map"]
B -->|нет| C{"Дубликаты важны<br/>или нужен индекс?"}
C -->|да| L["List"]
C -->|нет| D{"Частый поиск<br/>элемента?"}
D -->|да| S["Set"]
D -->|нет| L
```
Практический критерий: если в коде есть `list.contains(x)` внутри цикла - это `Set`. Такая замена превращает O(n²) в O(n):
```kotlin
fun main() {
val ids = List(1000) { it }
val allowedList = List(1000) { it * 2 }
val allowedSet = allowedList.toSet()
println(ids.count { it in allowedList }) // O(n*m) — 1000*1000 сравнений
println(ids.count { it in allowedSet }) // O(n) — то же 500, но быстро
}
```
## Специальные коллекции
```kotlin
fun main() {
val deque = ArrayDeque(listOf(2, 3))
deque.addFirst(1)
deque.addLast(4)
println("$deque ${deque.removeFirst()} ${deque.removeLast()}") // [1, 2, 3, 4] 1 4
val grouped = listOf("яблоко", "арбуз", "банан").groupBy { it.first() }
println(grouped) // {я=[яблоко], а=[арбуз], б=[банан]}
val assoc = listOf("a", "bb", "ccc").associateWith { it.length }
println(assoc) // {a=1, bb=2, ccc=3}
}
```
`ArrayDeque` в Kotlin - своя реализация (не `java.util.ArrayDeque`), работает как стек и как очередь. Подробнее об `groupBy` и `associate`: [Операции](Операции.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `map.get(k)` возвращает `V` | `map[k]` возвращает `V?` |
> | `map.put(k, v)` | `map[k] = v` |
> | `new AbstractMap.SimpleEntry<>(k, v)` | `k to v` |
> | `for (Map.Entry<K,V> e : map.entrySet())` | `for ((k, v) in map)` |
> | `map.computeIfAbsent(k, f)` | `map.getOrPut(k) { }` |
> | `Collections.unmodifiableList` | тип `List` |
> | `set.retainAll(other)` | `a intersect b` |
> | `list.get(i)` | `list[i]` |
>
> **`map[k]` возвращает nullable - и это правильно.** В Java `map.get(k)` имеет тип `V`, поэтому `int x = map.get("нет")` компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе:
> ```kotlin
> fun main() {
> val map = mapOf("a" to 1)
> val x: Int = map["z"] ?: 0 // компилятор требует обработать null
> println(x) // 0
> }
> ```
> Обратная сторона: `map[k]!!` появляется в коде часто. Когда ключ гарантированно есть, честнее `getValue(k)` - он бросит `NoSuchElementException` с внятным сообщением.
>
> **Деструктуризация `Map.Entry`.** Java требует `e.getKey()`/`e.getValue()`; в Kotlin `for ((k, v) in map)` работает потому, что для `Map.Entry` объявлены функции-расширения `component1` и `component2`. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
>
> **Операции над множествами как инфиксные функции.** В Java `union` - это `addAll` на копии, `intersect` - `retainAll`, то есть операции мутирующие и требуют ручного копирования. В Kotlin `a union b` возвращает новое множество и читается как математическая запись.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **`map[k]` не различает «нет ключа» и «значение null»:**
> ```kotlin
> fun main() {
> val map = mapOf("a" to null, "b" to 1)
> println(map["a"]) // null
> println(map["z"]) // null — то же самое!
> println("a" in map) // true
> println("z" in map) // false — вот так различать
> }
> ```
>
> **`getValue` бросает, `get` - нет.** Это легко перепутать:
> ```kotlin
> fun main() {
> val map = mapOf("a" to 1)
> println(map["z"]) // null
> try {
> map.getValue("z")
> } catch (e: NoSuchElementException) {
> println("getValue бросает") // getValue бросает
> }
> }
> ```
>
> **Изменяемый элемент в `Set` или ключ в `Map` ломает коллекцию:**
> ```kotlin
> data class Key(var id: Int)
>
> fun main() {
> val key = Key(1)
> val set = mutableSetOf(key)
> println(key in set) // true
> key.id = 2 // hashCode изменился!
> println(key in set) // false — элемент потерян
> println(set.size) // 1 — но он всё ещё внутри
> }
> ```
> Ключи и элементы множеств обязаны быть неизменяемыми. `data class` с `var` в этой роли - ошибка.
>
> **`subList` - это вид на исходный список, а не копия:**
> ```kotlin
> fun main() {
> val list = mutableListOf(1, 2, 3, 4)
> val sub = list.subList(1, 3)
> list[1] = 99
> println(sub) // [99, 3] — изменился
> list.add(5)
> try {
> println(sub)
> } catch (e: ConcurrentModificationException) {
> println("структурное изменение списка сломало subList")
> }
> }
> ```
> Для независимой копии нужен `slice` или `toList()`.
>
> **`remove` на `MutableList<Int>` неоднозначен:**
> ```kotlin
> fun main() {
> val list = mutableListOf(10, 20, 30)
> list.removeAt(1) // по индексу
> println(list) // [10, 30]
> list.remove(10) // по значению
> println(list) // [30]
> }
> ```
> В Kotlin, в отличие от Java, перегрузки по индексу у `remove` нет - только `removeAt`. Это как раз устраняет классическую Java-ловушку `list.remove(1)`.
>
> **`list.contains(x)` в цикле - это O(n²).** Самая частая проблема производительности с коллекциями. Если проверка повторяется, предварительно постройте `Set`.
>
> **`hashSetOf` и `hashMapOf` не сохраняют порядок** - вывод может отличаться между запусками и версиями JDK. Тест, завязанный на порядок, будет нестабильным.
>
> **`sortedSetOf` требует `Comparable` или компаратор** и бросит `ClassCastException` при вставке несравнимого элемента - в рантайме, а не при компиляции.
>
> **`+=` на `List` работает, но создаёт новую коллекцию:**
> ```kotlin
> fun main() {
> var list = listOf(1, 2) // var!
> list += 3 // новый список, не add
> println(list) // [1, 2, 3]
>
> val mutable = mutableListOf(1, 2)
> mutable += 3 // здесь настоящий add
> println(mutable) // [1, 2, 3]
> }
> ```
> На `val list: List<Int>` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований.
Далее: [Изменяемость](Изменяемость.md)
+368
View File
@@ -0,0 +1,368 @@
---
title: Sequence
description: Ленивые вычисления, порядок обхода элементов и когда Sequence выигрывает у List
tags:
- kotlin
- collections
---
`Sequence<T>` - ленивый аналог коллекции. Операции не выполняются до запроса результата, и элементы проходят всю цепочку по одному.
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("-- List:")
val eager = list
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println(" результат $eager")
println("-- Sequence:")
val lazy = list.asSequence()
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println(" результат $lazy")
}
```
`List` выполнит 5 `map` и 5 `filter` - всего 10 операций, создав два промежуточных списка. `Sequence` выполнит 3 `map` и 3 `filter` и остановится, как только найдёт первый подходящий элемент.
## Два порядка обхода
Это ключ к пониманию `Sequence`:
```mermaid
graph LR
subgraph List["List: по операциям"]
L1["map всех 5"] --> L2["filter всех 5"] --> L3["first"]
end
subgraph Seq["Sequence: по элементам"]
S1["1: map, filter"] --> S2["2: map, filter"] --> S3["3: map, filter → стоп"]
end
```
| | `List` | `Sequence` |
|---|---|---|
| порядок | операция за операцией | элемент за элементом |
| промежуточные коллекции | на каждом шаге | нет |
| вычисление | сразу | при терминальной операции |
| можно ли прервать | нет | да |
| бесконечные данные | нет | да |
## Создание
```kotlin
fun main() {
println(listOf(1, 2, 3).asSequence().toList()) // [1, 2, 3]
println(sequenceOf(1, 2, 3).toList()) // [1, 2, 3]
println(emptySequence<Int>().toList()) // []
println(generateSequence(1) { it * 2 }.take(5).toList()) // [1, 2, 4, 8, 16]
println(generateSequence(1) { if (it < 5) it + 1 else null }.toList()) // [1, 2, 3, 4, 5]
val built = sequence {
yield(1)
yieldAll(listOf(2, 3))
yield(4)
}
println(built.toList()) // [1, 2, 3, 4]
}
```
`generateSequence` с `null` в лямбде - способ задать конец. `sequence { }` - билдер с `yield`, внутри него можно писать любую логику, включая циклы.
## Промежуточные и терминальные операции
```kotlin
fun main() {
val seq = sequenceOf(1, 2, 3)
// промежуточные: возвращают Sequence, ничего не считают
val chain = seq.map { it * 2 }.filter { it > 2 }.take(2)
println("цепочка построена, вычислений не было")
// терминальные: запускают вычисление
println(chain.toList()) // [4, 6]
println(seq.sum()) // 6
println(seq.count()) // 3
println(seq.first()) // 1
println(seq.any { it > 2 }) // true
}
```
| Промежуточные | Терминальные |
|---|---|
| `map`, `filter`, `flatMap` | `toList`, `toSet`, `toMap` |
| `take`, `drop`, `distinct` | `first`, `last`, `single` |
| `sorted`, `chunked`, `windowed` | `sum`, `count`, `average`, `max` |
| `onEach`, `mapNotNull` | `any`, `all`, `none`, `forEach` |
| `zip`, `plus` | `fold`, `reduce`, `joinToString` |
## Бесконечные последовательности
То, чего `List` не умеет в принципе:
```kotlin
fun main() {
val naturals = generateSequence(1) { it + 1 }
println(naturals.take(5).toList()) // [1, 2, 3, 4, 5]
println(naturals.filter { it % 7 == 0 }.first()) // 7
println(naturals.map { it * it }.takeWhile { it < 50 }.toList()) // [1, 4, 9, 16, 25, 36, 49]
val fib = generateSequence(0 to 1) { (a, b) -> b to (a + b) }.map { it.first }
println(fib.take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
```
Важно: терминальная операция без ограничения на бесконечной последовательности зависнет. `naturals.toList()` не завершится никогда.
## Sequence в цепочке с ранним выходом
```kotlin
data class Record3(val id: Int, val active: Boolean, val score: Int)
fun main() {
val records = List(1_000_000) { Record3(it, it % 100 == 0, it % 7) }
val found = records.asSequence()
.filter { it.active }
.map { it.score * 2 }
.firstOrNull { it > 10 }
println(found) // 12
val top3 = records.asSequence()
.filter { it.active }
.take(3)
.map { it.id }
.toList()
println(top3) // [0, 100, 200]
}
```
Здесь `Sequence` обрабатывает несколько сотен элементов вместо миллиона. На `List` каждый `filter`/`map` прошёл бы весь миллион и создал два списка.
## Когда Sequence не выигрывает
```kotlin
fun main() {
val small = listOf(1, 2, 3, 4, 5)
// Sequence здесь только добавляет накладные расходы
println(small.map { it * 2 }.sum()) // 30
println(small.asSequence().map { it * 2 }.sum()) // 30, но медленнее
}
```
`Sequence` создаёт объект-обёртку на каждую операцию и вызывает лямбды через интерфейс итератора. На маленьких коллекциях и коротких цепочках это дороже, чем прямой проход.
Практическое правило:
| Условие | Выбор |
|---|---|
| меньше ~1000 элементов, 1-2 операции | `List` |
| много элементов и 3+ операций | `Sequence` |
| есть `first`, `find`, `take`, `any` после цепочки | `Sequence` |
| источник бесконечный или потоковый (файл, БД) | `Sequence` |
| нужен результат целиком, операций мало | `List` |
| нужен индексный доступ к результату | `List` |
## Операции, которые ломают ленивость
Некоторые операции обязаны прочитать всё, прежде чем выдать первый элемент:
```kotlin
fun main() {
val seq = sequenceOf(3, 1, 2)
.onEach { println("читаю $it") }
.sorted() // нужны все элементы
println("до first ничего не прочитано")
println(seq.first()) // здесь прочитаются все три
}
```
Такие операции - `sorted`, `sortedBy`, `distinct` (частично), `groupBy`, `chunked`. Ставьте их как можно позже в цепочке, а `filter` - как можно раньше.
## Sequence одноразов
```kotlin
fun main() {
val seq = listOf(1, 2, 3).asSequence().map { it * 2 }
println(seq.toList()) // [2, 4, 6]
println(seq.toList()) // [2, 4, 6] — здесь повезло: источник List
val once = sequence { yield(1); yield(2) }
println(once.toList()) // [1, 2]
println(once.toList()) // [1, 2] — тоже работает, лямбда вызвана заново
val fromIterator = listOf(1, 2, 3).iterator().asSequence()
println(fromIterator.toList()) // [1, 2, 3]
try {
fromIterator.toList()
} catch (e: IllegalStateException) {
println(e.message) // This sequence can be consumed only once.
}
}
```
Последовательность из итератора обходится один раз: `Iterator.asSequence()` оборачивает результат в `constrainOnce()`, который фиксирует это ограничение и бросает `IllegalStateException` при повторной попытке. Тот же механизм доступен явно:
```kotlin
fun main() {
val seq = sequenceOf(1, 2).constrainOnce()
println(seq.toList()) // [1, 2]
try {
seq.toList()
} catch (e: IllegalStateException) {
println("повторный обход запрещён")
}
}
```
## Чтение файла
Главный практический случай: данные, которые не влезают в память.
```kotlin
import java.io.File
fun main() {
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText((1..100).joinToString("\n"))
val sum = tmp.useLines { lines -> // Sequence<String>
lines.map { it.toInt() }.filter { it % 2 == 0 }.sum()
}
println(sum) // 2550
val firstBig = tmp.useLines { lines -> lines.first { it.toInt() > 50 } }
println(firstBig) // 51 — файл прочитан до 51-й строки, не целиком
tmp.delete()
}
```
`useLines` даёт `Sequence<String>` и закрывает файл после работы. Альтернатива `readLines()` загрузила бы весь файл в `List<String>`.
## Sequence и Flow
`Sequence` - ленивый, но **синхронный**: получение следующего элемента блокирует поток. Для асинхронных источников нужен `Flow`:
| | `Sequence` | `Flow` |
|---|---|---|
| вычисление | лениво | лениво |
| блокировка | да | нет, `suspend` |
| внутри можно `delay` | нет | да |
| источник | память, файл, генератор | сеть, БД, события |
Подробнее: [Flow](../Корутины/Flow.md)
> [!info] Отличия от Java
> | Java Stream | Kotlin Sequence |
> |---|---|
> | `list.stream()` | `list.asSequence()` |
> | `Stream.of(1, 2)` | `sequenceOf(1, 2)` |
> | `Stream.iterate(1, f)` | `generateSequence(1, f)` |
> | `collect(toList())` | `toList()` |
> | `.parallel()` | **нет аналога** |
> | `IntStream`, `LongStream` | нет, только `Sequence<T>` |
> | `Files.lines(path)` | `file.useLines { }` |
> | нет генератора с `yield` | `sequence { yield(x) }` |
>
> **`Sequence` и `Stream` - почти одно и то же.** Ленивость, промежуточные и терминальные операции, короткое замыкание на `findFirst`/`first` - всё совпадает. Поэтому опыт работы со стримами переносится напрямую, и наоборот: если в Java вы написали бы `.stream()`, в Kotlin нужен `.asSequence()`.
>
> **Главное отличие - нет параллелизма.** `Stream.parallel()` распределяет работу по `ForkJoinPool`; у `Sequence` такого нет вообще. Причина в том, что для конкурентности в Kotlin предусмотрены корутины, а не параллельные стримы. Если нужна параллельная обработка - либо используйте Java-стримы напрямую (они доступны), либо корутины:
> ```kotlin
> fun main() {
> // Java Stream доступен из Kotlin
> val sum = listOf(1, 2, 3).stream().parallel().mapToInt { it * 2 }.sum()
> println(sum) // 12
> }
> ```
>
> **Нет специализаций для примитивов.** В Java `IntStream` избегает упаковки: `list.stream().mapToInt(x -> x).sum()` работает с `int`. `Sequence<Int>` упаковывает каждый элемент в `Integer`. На числовых расчётах по большим данным Java-стримы могут оказаться быстрее.
>
> **`sequence { yield(x) }` - генератор, которого в Java нет.** Он реализован на том же механизме приостановки, что и корутины, поэтому внутри можно писать обычные циклы и условия:
> ```kotlin
> fun main() {
> val primes = sequence {
> var n = 2
> while (true) {
> if ((2 until n).none { n % it == 0 }) yield(n)
> n++
> }
> }
> println(primes.take(5).toList()) // [2, 3, 5, 7, 11]
> }
> ```
> В Java это требует либо отдельного потока, либо ручной реализации `Iterator` с сохранением состояния.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **Без терминальной операции не произойдёт ничего:**
> ```kotlin
> fun main() {
> listOf(1, 2, 3).asSequence().map { println("это не выведется"); it }
> println("конец") // только "конец"
> }
> ```
> Это частая ошибка при замене `forEach` на цепочку: если забыть терминальную операцию, код просто не выполнится, и компилятор не предупредит.
>
> **`Sequence` на маленьких коллекциях медленнее.** Обёртки и вызовы через итератор стоят дороже прямого цикла. Для списка из 10 элементов `asSequence()` - пессимизация.
>
> **Терминальная операция на бесконечной последовательности зависает:**
> ```kotlin
> fun main() {
> val naturals = generateSequence(1) { it + 1 }
> println(naturals.take(3).toList()) // [1, 2, 3] — ок
> // naturals.toList() // зависнет навсегда
> // naturals.count() // тоже
> // naturals.filter { it > 10 }.toList() // и это
> }
> ```
> Любая цепочка из бесконечного источника обязана содержать `take`, `takeWhile`, `first` или `find`.
>
> **`sorted` и `groupBy` убивают ленивость** - они читают всё до первого результата. Цепочка `asSequence().sorted().first()` не быстрее, чем на `List`.
>
> **Последовательность из итератора обходится один раз** - второй вызов падает:
> ```kotlin
> fun main() {
> val seq = listOf(1, 2).iterator().asSequence()
> println(seq.count()) // 2
> try {
> seq.count()
> } catch (e: IllegalStateException) {
> println("повторный обход: ${e.message}")
> }
> }
> ```
> Исключение здесь лучше молчаливого пустого результата, но возникает оно в рантайме и только на втором обходе - в редкой ветке кода его легко не заметить. Если последовательность может понадобиться дважды, сохраните `toList()`.
>
> **Побочные эффекты в промежуточных операциях выполняются в неожиданном порядке:**
> ```kotlin
> fun main() {
> val log = mutableListOf<String>()
> sequenceOf(1, 2)
> .onEach { log.add("A$it") }
> .onEach { log.add("B$it") }
> .toList()
> println(log) // [A1, B1, A2, B2] — не [A1, A2, B1, B2]
> }
> ```
> На `List` вывод был бы `[A1, A2, B1, B2]`. Код, зависящий от порядка побочных эффектов, при переходе на `Sequence` сломается.
>
> **Повторный обход пересчитывает всё заново:**
> ```kotlin
> fun main() {
> var calls = 0
> val seq = listOf(1, 2, 3).asSequence().map { calls++; it * 2 }
> seq.toList()
> seq.toList()
> println(calls) // 6 — лямбда вызвана дважды для каждого элемента
> }
> ```
> Если результат нужен несколько раз, сохраните его в `List`.
>
> **`Sequence` синхронна.** Внутри её лямбд нельзя вызывать `suspend`-функции - для этого существует `Flow`. Попытка использовать `Sequence` для сетевых запросов приведёт к блокировке потока.
>
> **`useLines` закрывает файл по выходу из лямбды.** Вернуть `Sequence` наружу нельзя - она окажется на закрытом ресурсе:
> ```kotlin
> import java.io.File
>
> fun main() {
> val tmp = File.createTempFile("leak", ".txt")
> tmp.writeText("1\n2")
> val leaked = tmp.useLines { it } // Sequence на закрытом файле
> try {
> println(leaked.toList())
> } catch (e: java.io.IOException) {
> println("поток закрыт: ${e.message}") // поток закрыт: Stream closed
> }
> tmp.delete()
> }
> ```
> Внутри лямбды нужно получить готовое значение: `useLines { it.toList() }` или сразу агрегат.
Далее: [Коллекции под капотом](Коллекции%20под%20капотом.md)
@@ -0,0 +1,323 @@
---
title: Изменяемость
description: Read-only против immutable, защитное копирование и персистентные коллекции
tags:
- kotlin
- collections
---
`List` в Kotlin - **read-only**, а не immutable. Это разные вещи, и их смешение - источник самых неприятных ошибок с коллекциями.
| | read-only (`List`) | immutable |
|---|---|---|
| изменить через эту ссылку | нельзя | нельзя |
| может измениться вообще | **да** | нет |
| гарантия | интерфейс без мутаторов | сам объект неизменяем |
| есть в Kotlin stdlib | да | нет (только внешняя библиотека) |
## Почему read-only не immutable
```kotlin
fun main() {
val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable // тот же объект, другой интерфейс
mutable.add(3)
println(readOnly) // [1, 2, 3] — изменился под вами
}
```
`MutableList` наследует `List`, поэтому любая изменяемая коллекция подходит там, где ожидается `List`. Приведение типа объект не копирует.
Это создаёт конкретную проблему в API:
```kotlin
class Repository4 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items // утечка: вернули ссылку на изменяемое
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository4()
val snapshot = repo.getItems()
println(snapshot) // [1, 2, 3]
repo.add(4)
println(snapshot) // [1, 2, 3, 4] — «снимок» изменился
}
```
Клиент получил `List` и вправе считать, что значение стабильно - например, положить его в `Map` как ключ или обойти в другом потоке. Ни то ни другое здесь не безопасно.
## Защитное копирование
```kotlin
class Repository5 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items.toList() // копия
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository5()
val snapshot = repo.getItems()
repo.add(4)
println("$snapshot / ${repo.getItems()}") // [1, 2, 3] / [1, 2, 3, 4]
}
```
`toList()` создаёт новый `ArrayList`. Цена - O(n) и лишний объект; выгода - значение, на которое можно полагаться.
Варианты копирования:
```kotlin
fun main() {
val source = mutableListOf(1, 2, 3)
println(source.toList()::class.simpleName) // ArrayList
println(source.toMutableList()::class.simpleName) // ArrayList
println(source.toSet()::class.simpleName) // LinkedHashSet
println(ArrayList(source)::class.simpleName) // ArrayList
println(List(source.size) { source[it] }) // [1, 2, 3]
}
```
Когда копировать:
- коллекция уходит за границу класса или модуля и должна быть стабильной
- коллекция попадает в `Map`/`Set` как ключ или элемент
- значение читается из другого потока
Когда не копировать:
- коллекция создана тут же и больше нигде не хранится
- вызывающий - код того же класса
- коллекция огромная, а изменения точно не будет (документируйте это)
## Неизменяемость на уровне содержимого
Даже честная копия списка не защищает от изменения **элементов**:
```kotlin
class Item(var name: String)
fun main() {
val items = listOf(Item("a"), Item("b"))
val copy = items.toList()
items[0].name = "изменено"
println(copy[0].name) // изменено — копия поверхностная
}
```
`toList()` копирует только ссылки. Настоящая защита - неизменяемые элементы:
```kotlin
data class ItemRo(val name: String)
fun main() {
val items = listOf(ItemRo("a"))
// items[0].name = "x" // ошибка: val
val changed = items.map { it.copy(name = "изменено") }
println("${items[0].name} / ${changed[0].name}") // a / изменено
}
```
Это общее правило: неизменяемость полезна, когда она рекурсивна. `List<T>` с изменяемым `T` даёт лишь половину гарантии.
## val и изменяемость
Два независимых понятия, которые постоянно путают:
```kotlin
fun main() {
val a = mutableListOf(1) // ссылку сменить нельзя, содержимое — можно
a.add(2)
println(a) // [1, 2]
// a = mutableListOf(3) // ошибка
var b = listOf(1) // ссылку сменить можно, содержимое — нельзя
b = listOf(1, 2)
println(b) // [1, 2]
// b.add(3) // ошибка
}
```
| | `val` | `var` |
|---|---|---|
| `List` | ничего не изменить | можно заменить целиком |
| `MutableList` | можно менять содержимое | и то и другое |
Самый частый правильный выбор - `val` + `List`. Подробнее: [Переменные](../Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
## Функциональное обновление
Вместо изменения на месте - новое значение:
```kotlin
fun main() {
val original = listOf(1, 2, 3)
println(original + 4) // [1, 2, 3, 4]
println(original - 2) // [1, 3]
println(original.map { it * 10 }) // [10, 20, 30]
println(original.filter { it != 2 }) // [1, 3]
println(original.toMutableList().apply { this[0] = 99 }) // [99, 2, 3]
println(original) // [1, 2, 3] — не тронут
}
```
Для `Map` то же:
```kotlin
fun main() {
val config = mapOf("host" to "localhost", "port" to "8080")
println(config + ("port" to "9090")) // {host=localhost, port=9090}
println(config - "port") // {host=localhost}
println(config + mapOf("tls" to "on")) // {host=localhost, port=8080, tls=on}
}
```
Цена - копирование на каждую операцию. Для небольших коллекций и нечастых обновлений это правильный выбор по умолчанию: такой код проще отлаживать, потому что значение не меняется за спиной.
## Изменяемое состояние внутри, read-only снаружи
Рабочий шаблон для класса с коллекцией:
```kotlin
class Playlist {
private val _tracks = mutableListOf<String>()
val tracks: List<String> get() = _tracks.toList() // копия при чтении
fun add(track: String) { _tracks.add(track) }
fun size() = _tracks.size
}
fun main() {
val p = Playlist()
p.add("песня 1")
val snapshot = p.tracks
p.add("песня 2")
println("$snapshot / ${p.tracks}") // [песня 1] / [песня 1, песня 2]
}
```
Если копия на каждое чтение слишком дорога, альтернатива - возвращать `_tracks` напрямую и документировать, что значение живое. Тогда тип должен быть осмысленным: `List` здесь обманывает читателя, и честнее отдавать `Sequence` или явный метод `snapshot()`.
## Неизменяемость и многопоточность
```kotlin
fun main() {
val shared = mutableListOf(1, 2, 3)
// Небезопасно: обход в одном потоке + add в другом = ConcurrentModificationException
// Безопасно: работать с копией
val safe = shared.toList()
println(safe.sum()) // 6
val sync = java.util.Collections.synchronizedList(mutableListOf(1, 2))
println(sync.size) // 2 — но обход всё равно нужно синхронизировать вручную
val concurrent = java.util.concurrent.CopyOnWriteArrayList(listOf(1, 2))
println(concurrent.size) // 2
}
```
Неизменяемое значение потокобезопасно по определению - именно поэтому в конкурентном коде предпочитают read-only копии, а не блокировки. Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
## Персистентные коллекции
В stdlib настоящих immutable-коллекций нет. Их даёт библиотека `kotlinx.collections.immutable`:
```kotlin
// Требует зависимости org.jetbrains.kotlinx:kotlinx-collections-immutable.
// val list: PersistentList<Int> = persistentListOf(1, 2, 3)
// val updated = list.add(4) // новый список, старый цел
// println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4]
//
// Тип ImmutableList<T> гарантирует, что коллекция не изменится вообще,
// а PersistentList<T> добавляет эффективное «обновление» без полного копирования.
```
Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Collections.unmodifiableList(x)` | тип `List<T>` |
> | `List.copyOf(x)` (Java 10+) | `x.toList()` |
> | `List.of(1, 2)` - immutable | `listOf(1, 2)` - read-only |
> | нарушение видно в рантайме | видно при компиляции |
> | `final List` | `val list` |
> | Guava `ImmutableList` | `kotlinx.collections.immutable` |
>
> **`unmodifiableList` и `List` решают одну задачу по-разному.** Java-обёртка - объект-декоратор: тип остаётся `List`, метод `add` есть, но бросает исключение в рантайме. Kotlin убирает метод из интерфейса, поэтому ошибка видна компилятору и обёртка не нужна:
> ```java
> List<Integer> ro = Collections.unmodifiableList(src);
> ro.add(1); // компилируется, падает в рантайме
> ```
> Но у Java-обёртки есть преимущество, которого нет у Kotlin: она действительно запрещает изменение через **эту** ссылку даже при касте. Kotlin-ский `List` каст к `MutableList` может пережить, если под ним `ArrayList`.
>
> **`List.of` в Java сильнее, чем `listOf` в Kotlin.** `List.of(1, 2)` создаёт по-настоящему неизменяемую коллекцию: любая попытка изменения, в том числе из другого места, бросит исключение. `listOf(1, 2)` даёт неизменяемую реализацию только случайно - это деталь, на которую нельзя полагаться, потому что `listOf` на самом деле описывает интерфейс, а не гарантию объекта.
>
> **Защитное копирование нужно в обоих языках.** `private final List` в Java и `private val` в Kotlin одинаково не защищают содержимое. Разница в том, что в Kotlin ошибку легче не заметить: тип `List` в сигнатуре выглядит как обещание неизменяемости, которого он не даёт.
>
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **Возврат `MutableList` под типом `List` - утечка:**
> ```kotlin
> class Leaky2 {
> private val items = mutableListOf(1, 2)
> fun items(): List<Int> = items
> }
>
> fun main() {
> val l = Leaky2()
> val view = l.items()
> @Suppress("UNCHECKED_CAST")
> (view as MutableList<Int>).add(3) // клиент изменил приватное состояние
> println(l.items()) // [1, 2, 3]
> }
> ```
> Каст сработал, потому что под ссылкой настоящий `ArrayList`. Защита - `toList()`.
>
> **`toList()` копирует поверхностно** - элементы остаются общими. Для вложенных структур нужна глубокая копия:
> ```kotlin
> fun main() {
> val nested = listOf(mutableListOf(1), mutableListOf(2))
> val shallow = nested.toList()
> nested[0].add(99)
> println(shallow[0]) // [1, 99] — внутренний список общий
>
> val deep = nested.map { it.toList() }
> nested[0].add(100)
> println(deep[0]) // [1, 99] — а этот цел
> }
> ```
>
> **`val` не делает коллекцию неизменяемой** - только ссылку. `val items = mutableListOf<Int>()` изменяется свободно.
>
> **Из Java read-only коллекция изменяема:**
> ```kotlin
> fun provide(): List<Int> = mutableListOf(1, 2)
>
> fun main() { println(provide()) } // [1, 2]
> ```
> Java-код получит `java.util.List` и вызовет `add` без ошибок компиляции. Для API, доступного из Java, копируйте или используйте `Collections.unmodifiableList`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **Изменяемый объект как ключ `Map` теряется:**
> ```kotlin
> data class MutableKey(var id: Int)
>
> fun main() {
> val key = MutableKey(1)
> val map = mutableMapOf(key to "значение")
> key.id = 2
> println(map[key]) // null — хеш изменился
> println(map.size) // 1 — запись на месте, но недостижима
> }
> ```
>
> **`+=` на `var list: List<T>` в цикле - это O(n²):**
> ```kotlin
> fun main() {
> var slow = listOf<Int>()
> repeat(1000) { slow += it } // 1000 копирований списка
> println(slow.size) // 1000
>
> val fast = buildList { repeat(1000) { add(it) } }
> println(fast.size) // 1000, одна коллекция
> }
> ```
>
> **`ConcurrentModificationException` при изменении во время обхода:**
> ```kotlin
> fun main() {
> val list = mutableListOf(1, 2, 3)
> try {
> for (x in list) if (x == 2) list.remove(x)
> } catch (e: ConcurrentModificationException) {
> println("нельзя изменять во время обхода")
> }
> list.removeAll { it == 2 } // так правильно
> println(list) // [1, 3]
> }
> ```
>
> **Обёртка `synchronizedList` не делает обход потокобезопасным** - атомарны только отдельные методы. Итерацию нужно оборачивать в `synchronized` вручную.
Далее: [Операции](Операции.md)
@@ -0,0 +1,282 @@
---
title: Коллекции под капотом
description: Что лежит в байт-коде, сложность операций, расход памяти и упаковка примитивов
tags:
- kotlin
- collections
- performance
---
Отдельных классов коллекций у Kotlin нет. `List`, `MutableList`, `Map` - это интерфейсы, которые при компиляции отображаются в типы `java.util`.
```kotlin
fun main() {
println(listOf(1, 2, 3).javaClass.name) // java.util.Arrays$ArrayList
println(mutableListOf(1, 2).javaClass.name) // java.util.ArrayList
println(setOf(1, 2).javaClass.name) // java.util.LinkedHashSet
println(mapOf("a" to 1, "b" to 2).javaClass.name) // java.util.LinkedHashMap
println(emptyList<Int>().javaClass.name) // kotlin.collections.EmptyList
println(listOf(1).javaClass.name) // java.util.Collections$SingletonList
println(mapOf("a" to 1).javaClass.name) // java.util.Collections$SingletonMap
}
```
Обратите внимание: `listOf` возвращает **три разные** реализации в зависимости от числа аргументов, и `mapOf` ведёт себя так же. Это оптимизация под частые случаи - пустая коллекция и коллекция из одного элемента не требуют ни массива, ни хеш-таблицы.
## Соответствие типов
| Kotlin | JVM | Примечание |
|---|---|---|
| `List<T>` | `java.util.List` | тот же интерфейс |
| `MutableList<T>` | `java.util.List` | **тот же**, различия только в компиляторе |
| `Map<K, V>` | `java.util.Map` | |
| `Array<T>` | `T[]` | настоящий массив |
| `IntArray` | `int[]` | без упаковки |
| `Array<Int>` | `Integer[]` | с упаковкой |
`List` и `MutableList` - это один и тот же `java.util.List` в байт-коде. Разделение существует только при компиляции Kotlin, что объясняет две вещи: из Java read-only коллекция изменяема, и каст `List` в `MutableList` компилируется.
```kotlin
fun main() {
val ro: List<Int> = mutableListOf(1, 2)
println(ro is MutableList<*>) // true — в рантайме это один тип
}
```
## Сложность операций
| Операция | `ArrayList` | `LinkedHashSet` | `LinkedHashMap` | `TreeMap` |
|---|---|---|---|---|
| доступ по индексу | O(1) | - | - | - |
| поиск элемента | O(n) | O(1) | O(1) по ключу | O(log n) |
| вставка в конец | O(1)* | O(1) | O(1) | O(log n) |
| вставка в начало | O(n) | O(1) | O(1) | O(log n) |
| удаление по индексу | O(n) | - | - | - |
| удаление по значению | O(n) | O(1) | O(1) | O(log n) |
\* амортизированная: при переполнении массив копируется в новый, вдвое больший.
Практические следствия:
```kotlin
fun main() {
val list = List(10_000) { it }
val set = list.toSet()
// O(n) против O(1)
println(9999 in list) // true, но обошёл весь список
println(9999 in set) // true, один хеш
// вставка в начало ArrayList — сдвиг всего массива
val deque = ArrayDeque(list)
deque.addFirst(-1) // O(1)
println(deque.first()) // -1
}
```
## Упаковка примитивов
Главный источник неожиданных расходов памяти:
```kotlin
fun main() {
val boxed: List<Int> = List(5) { it } // каждый элемент — объект Integer
val primitive: IntArray = IntArray(5) { it } // настоящий int[]
println(boxed.sum()) // 10
println(primitive.sum()) // 10
println(primitive.javaClass.name) // [I — массив int
}
```
Разница в памяти на 1 млн элементов:
| Структура | Примерный размер |
|---|---|
| `IntArray(1_000_000)` | ~4 МБ |
| `Array<Int>(1_000_000)` | ~20 МБ (ссылки + объекты) |
| `List<Int>` из 1 млн | ~20 МБ |
Кеш `Integer` смягчает это для малых чисел:
```kotlin
fun main() {
val a: Int? = 127
val b: Int? = 127
println(a === b) // true — из кеша -128..127
val c: Int? = 128
val d: Int? = 128
println(c === d) // false — разные объекты
println(c == d) // true — значения равны
}
```
Это ровно та же ловушка, что с `Integer` в Java. Правило: для чисел всегда `==`, никогда `===`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
Специализированные массивы без упаковки:
```kotlin
fun main() {
println(intArrayOf(1, 2, 3).sum()) // 6
println(doubleArrayOf(1.0, 2.0).average()) // 1.5
println(booleanArrayOf(true, false).count { it }) // 1
println(longArrayOf(1L).size) // 1
}
```
Их стоит использовать, когда элементов много и они числовые: матрицы, буферы, обработка сигналов. В обычном бизнес-коде разница несущественна.
## Рост ArrayList
```kotlin
fun main() {
val withoutCapacity = ArrayList<Int>() // начальная ёмкость 10
repeat(1000) { withoutCapacity.add(it) } // ~8 перевыделений массива
val withCapacity = ArrayList<Int>(1000) // сразу нужный размер
repeat(1000) { withCapacity.add(it) } // перевыделений нет
println("${withoutCapacity.size} ${withCapacity.size}") // 1000 1000
}
```
При каждом переполнении создаётся массив в 1.5 раза больше и копируется содержимое. Для заранее известного размера задавайте ёмкость - это убирает лог n копирований.
## Что делает цепочка операций
```kotlin
fun main() {
val source = List(1000) { it }
val result = source
.map { it * 2 } // новый ArrayList на 1000
.filter { it > 100 } // новый ArrayList на ~950
.take(5) // новый ArrayList на 5
println(result) // [102, 104, 106, 108, 110]
}
```
Три промежуточных списка, 1000 + 950 операций ради 5 результатов. Та же цепочка на `Sequence` не создаст ни одного промежуточного списка и выполнит ~56 операций. Подробнее: [Sequence](Sequence.md)
Измерить разницу можно так:
```kotlin
fun main() {
val source = List(1_000_000) { it }
val t1 = System.nanoTime()
val a = source.map { it * 2 }.filter { it > 100 }.first()
val eager = (System.nanoTime() - t1) / 1_000_000
val t2 = System.nanoTime()
val b = source.asSequence().map { it * 2 }.filter { it > 100 }.first()
val lazy = (System.nanoTime() - t2) / 1_000_000
println("$a == $b") // 102 == 102
println("list ${eager}мс, sequence ${lazy}мс — ленивый быстрее: ${lazy < eager}")
}
```
## Inline и отсутствие объектов-лямбд
Операции над коллекциями объявлены `inline`, поэтому лямбда не создаёт объект:
```kotlin
fun main() {
val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 }
println(sum) // 10
}
```
Компилятор подставляет тело лямбды прямо в цикл - в байт-коде нет ни объекта `Function1`, ни виртуального вызова. Это то, чего не даёт Java-стрим: там каждая лямбда - объект, а цепочка строит конвейер из `Spliterator`-ов.
Исключение - `Sequence`: там операции не могут быть полностью inline, потому что элементы обрабатываются лениво через итератор. Это и есть источник накладных расходов `Sequence` на малых данных. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
## Как выбирают структуру
```kotlin
fun main() {
// частый поиск -> Set
val allowed = setOf("a", "b", "c")
println(listOf("a", "x").count { it in allowed }) // 1
// частое добавление в начало и конец -> ArrayDeque
val queue = ArrayDeque<Int>()
queue.addLast(1); queue.addFirst(0)
println(queue) // [0, 1]
// нужен отсортированный обход и диапазоны -> TreeMap
val sorted = sortedMapOf(3 to "c", 1 to "a", 2 to "b")
println(sorted.headMap(3)) // {1=a, 2=b}
// много числовых данных -> примитивный массив
val buffer = IntArray(1000)
println(buffer.size) // 1000
}
```
## Потокобезопасность
```kotlin
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
map["a"] = 1
map.compute("a") { _, v -> (v ?: 0) + 1 }
println(map) // {a=2}
val sync = java.util.Collections.synchronizedMap(mutableMapOf("a" to 1))
println(sync.size) // 1
}
```
Обычные коллекции Kotlin не потокобезопасны - это те же `ArrayList` и `HashMap`. Варианты:
- read-only копия на каждый поток (`toList()`) - проще всего
- `ConcurrentHashMap` и `CopyOnWriteArrayList` из JDK
- корутины с ограничением доступа одним потоком или `Mutex`
Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `ArrayList` явно | `mutableListOf()` -> `ArrayList` |
> | `new HashMap<>()` | `mutableMapOf()` -> `LinkedHashMap` |
> | `IntStream` без упаковки | `IntArray` или Java-стрим |
> | лямбда - объект `Function` | лямбда инлайнится |
> | `Stream` ленив | `Iterable`-операции энергичны |
> | `new ArrayList<>(100)` | `ArrayList(100)` |
>
> **Коллекции те же самые - это не «Kotlin-коллекции».** Любая `List<T>` из Kotlin принимается Java-методом, принимающим `java.util.List`, без конвертации и накладных расходов. Это принципиальное отличие от языков со своей библиотекой коллекций (например, Scala), где на границе нужны обёртки.
>
> **Умолчание `LinkedHashMap` вместо `HashMap`.** `mutableMapOf()` создаёт `LinkedHashMap`, который на ~20% тяжелее по памяти (хранит ссылки на соседей), но даёт предсказуемый порядок обхода. Если порядок не нужен и записей миллионы, `hashMapOf()` экономнее.
>
> **Инлайн лямбд - преимущество перед стримами.** В Java `list.stream().filter(x -> x > 1)` создаёт объект лямбды и конвейер стрима. В Kotlin `list.filter { it > 1 }` компилируется в обычный цикл `for` с телом лямбды внутри - объектов не создаётся вовсе. Для коротких цепочек на небольших коллекциях Kotlin обычно быстрее Java-стримов именно поэтому.
>
> **Нет `IntStream`.** Java даёт специализации для `int`, `long`, `double`, избегающие упаковки в цепочках. В Kotlin эквивалента нет: `Sequence<Int>` упаковывает. Для числовых расчётов на больших объёмах используйте `IntArray` с обычным циклом или вызывайте Java-стримы.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
> [!warning] Подводные камни
> **`listOf` возвращает разные классы** - полагаться на конкретный тип нельзя:
> ```kotlin
> fun main() {
> println(listOf<Int>().javaClass.simpleName) // EmptyList
> println(listOf(1).javaClass.simpleName) // SingletonList
> println(listOf(1, 2).javaClass.simpleName) // ArrayList (Arrays$ArrayList)
> }
> ```
> Ни один из них не поддерживает `add`, но по разным причинам, и сообщения об ошибках будут разными.
>
> **`Arrays$ArrayList` - не `java.util.ArrayList`.** Это обёртка над массивом с фиксированным размером: `set` работает, `add` бросает `UnsupportedOperationException`.
>
> **`List<Int>` упаковывает каждое число.** Для миллиона элементов это ~20 МБ вместо 4 МБ и нагрузка на сборщик мусора. В горячем численном коде используйте `IntArray`.
>
> **`===` на числах даёт разные результаты в зависимости от значения** - из-за кеша `Integer` в диапазоне -128..127. Сравнивайте только через `==`.
>
> **`list.contains` в цикле - O(n²).** Самая частая проблема производительности:
> ```kotlin
> fun main() {
> val a = List(2000) { it }
> val b = List(2000) { it * 2 }
> println(a.count { it in b }) // 4 млн сравнений
> val bSet = b.toSet()
> println(a.count { it in bSet }) // 2000 операций, тот же результат
> }
> ```
>
> **Каждая операция создаёт коллекцию.** Цепочка из пяти операций по большому списку - пять списков в памяти одновременно. Для больших данных нужен `Sequence`.
>
> **`ArrayList` без начальной ёмкости перевыделяет массив.** При известном размере задавайте его: `ArrayList(n)` или `buildList(n) { }`.
>
> **`subList` держит ссылку на исходный список** - он не будет собран сборщиком мусора, даже если нужен только маленький срез большого списка. Для среза, который переживёт оригинал, используйте `slice` или `toList`.
>
> **Коллекции Kotlin не потокобезопасны.** `mutableListOf()` - обычный `ArrayList`. Одновременное изменение из двух потоков повреждает структуру или бросает `ConcurrentModificationException`.
>
> **Изменяемый ключ ломает `HashMap`.** Запись становится недостижимой, хотя `size` её считает. Ключи должны быть неизменяемыми - `data class` с `val`-полями. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
>
> **Измерения производительности на JVM без прогрева врут.** Пример с `System.nanoTime` выше показывает порядок величины, но JIT компилирует горячий код только после тысяч вызовов. Для настоящих измерений нужен JMH.
Далее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md)

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