1. Kotlin/JVM и Java standard library: что вообще происходит
Когда вы пишете Kotlin‑код «обычного» консольного приложения, вы почти наверняка пишете Kotlin/JVM: ваш код компилируется в байткод и выполняется на JVM (Java Virtual Machine). Это значит, что рядом с вашим кодом всегда находится JDK (Java Development Kit) со стандартными пакетами java.*: строки, коллекции, системные функции, UUID и много чего ещё. По сути, Kotlin не «заменяет» Java‑мир — он очень прагматично живёт вместе с ним и спокойно берёт готовые инструменты из JDK.
Чтобы не пугаться масштаба, запомним простую мысль: Java API для Kotlin — это просто «ещё одна библиотека», только очень большая и уже установленная почти везде, где есть JVM.
Java API в Kotlin выглядит «обычно»: точка, скобки и без ритуалов
Если вы ожидаете, что для вызова Java‑кода нужно открыть портал в ад, произнести заклинание и принести в жертву пару скобочек — вы приятно разочаруетесь. В Kotlin вызов Java API выглядит максимально буднично: создаём объект через «вызов типа» и вызываем методы через точку.
Здесь полезно выстроить интуицию: Java‑класс — это просто тип, который можно импортировать, создать через конструктор и использовать, как любой другой объект. Java‑метод — это просто метод: obj.method(...). А Java «статический метод» — это просто вызов через имя класса: Type.staticMethod(...). Да, это настолько скучно — и в этом весь кайф.
2. Пакеты и import: как Kotlin находит Java‑классы
Когда проект маленький, хочется писать «коротко и без лишних слов». Но компилятор не телепат: если вы используете класс UUID, ему нужно понимать, какой именно UUID (из какого пакета) вы имеете в виду. Поэтому у нас два основных способа обратиться к Java‑типу: через import или через полное имя.
Вспоминаем идею адреса: пакет — это как «улица», класс — это «дом». Если вы не написали import, то можете «дойти пешком» по полному адресу: java.util.UUID. А если написали import, то дальше можно говорить коротко: UUID. Это тот же принцип, который вы уже использовали с собственными функциями и пакетами, просто теперь пакеты начинаются на java..
Способ №1: полное имя
Полное имя удобно, когда вы делаете маленький пример, пробуете что-то в песочнице или просто не хотите пока засорять файл импортами. Это особенно помогает новичкам: вы видите весь путь и быстрее связываете в голове «класс → пакет».
fun main() {
val id = java.util.UUID.randomUUID().toString()
println(id) // например: 6c6f3a7a-0a51-4b4e-9a61-1dbd7c2f0fb2
}
Здесь java.util.UUID — Java‑класс из JDK. Метод randomUUID() — «статический» (вызов через имя класса), а toString() превращает UUID в строку.
Способ №2: import
import делает код чище и короче. Особенно когда класс используется много раз. В нормальных проектах это основной путь: вы импортируете нужные типы и спокойно работаете с короткими именами.
import java.util.UUID
fun main() {
val id = UUID.randomUUID().toString()
println(id) // например: 1c0d6e2a-9d0d-4e1a-9e84-63aaf1c30a24
}
Если IntelliJ IDEA подсвечивает красным UUID, обычно это значит одно из двух: либо забыли import, либо опечатались в имени.
3. Конструкторы и static‑методы Java: базовая практика
Когда вы видите Java‑класс, не нужно думать «это другое». В Kotlin создание объекта через конструктор выглядит как вызов функции: Type(...). Это относится и к Kotlin‑классам (позже в курсе), и к Java‑классам (уже сейчас). Хорошая новость: синтаксис один и тот же.
Важно только помнить, что многие Java‑объекты по стилю мутабельны (изменяемы): вы создаёте объект, а потом «дописываете» в него данные методами add, append, put и так далее. В Kotlin мы тоже так умеем (например, MutableList), просто сегодня это особенно заметно.
Пример: StringBuilder() как Java‑класс
StringBuilder вы могли уже видеть как инструмент для сборки длинных строк. На JVM это класс из Java мира, и Kotlin с ним дружит без вопросов.
fun main() {
val sb = StringBuilder()
sb.append("Kotlin ")
sb.append("и Java дружат.")
println(sb.toString()) // Kotlin и Java дружат.
}
Обратите внимание на стиль: append меняет объект sb «на месте». Это нормально и ожидаемо для Java API.
Java static‑методы: вызов через имя типа
В Java есть концепция static: метод принадлежит не конкретному объекту, а самому классу. Kotlin в общем случае не заставляет вас думать про static как про отдельную сущность: вы просто вызываете метод через имя типа — и всё.
Почему это важно? Потому что многие «утилиты» в JDK сделаны именно так: System.getenv(...), System.getProperty(...), UUID.randomUUID() и т.п. Это очень частый паттерн в реальных приложениях: «дай системную информацию», «сгенерируй идентификатор», «возьми текущие настройки».
Пример: System.getProperty(...) — узнаём что-нибудь о среде
System — это Java‑класс, который помогает общаться с окружающим миром (операционная система, свойства JVM, окружение). Для CLI‑программы это полезно хотя бы для дружелюбного «привет, пользователь».
fun main() {
val user = System.getProperty("user.name")
println("Привет, $user!") // Привет, alex!
}
Свойств очень много. Два часто используемых: "user.name" и "user.dir" (рабочая директория процесса). Про файлы мы сегодня не говорим, но директория как строка уже может быть полезной для диагностики.
Мини‑таблица: «статический» vs «обычный» метод
Чтобы мозг перестал путаться, удобно держать в голове такую табличку:
| Что делаем | Как выглядит | Пример |
|---|---|---|
| Вызываем метод через класс | |
|
| Создаём объект | |
|
| Вызываем метод через объект | |
|
Эта схема простая, но она закрывает 80% «почему это не компилируется?!» на старте.
4. Java‑коллекции: ArrayList и «мутация — это норма»
К этому месту вы уже уверенно пользуетесь коллекциями Kotlin (List, MutableList, Map), и это отлично. Но на JVM рядом существует огромный пласт кода и библиотек, которые используют Java‑коллекции: ArrayList, HashMap, HashSet и т.д. Kotlin умеет с ними работать напрямую. Более того, на JVM это настолько привычно, что иногда вы даже не заметите, где заканчивается Kotlin и начинается Java.
Ключевая практическая разница для новичка: Java‑коллекции по дизайну в основном изменяемые, и это считается нормой. Поэтому метод add будет менять список, а не возвращать новый.
Пример: создаём ArrayList и добавляем элементы
import java.util.ArrayList
fun main() {
val xs = ArrayList<Int>()
xs.add(10)
xs.add(20)
println(xs.size) // 2
}
Сравните это с Kotlin‑стилем listOf(10, 20): там список read‑only (по ссылке), а здесь — «живой» и меняющийся.
Зачем вообще трогать Java‑коллекции, если есть Kotlin‑коллекции?
В этой лекции мы не будем углубляться в тонкости совместимости (это отдельная большая тема). Но практическая причина простая: огромное количество API в мире JVM исторически принимает и возвращает Java‑коллекции. И если вы хотите уверенно пользоваться библиотеками, вам нужно хотя бы не пугаться ArrayList.
5. Мини‑проект: «Text Analyzer» становится взрослее
До этого дня мы писали довольно автономные программы: ввёл текст → посчитал → вывел отчёт. Сегодня мы сделаем маленький, но очень «реальный» шаг: добавим в отчёт служебную информацию, которую обычно хочется видеть в логах и отчётах. Например: идентификатор запуска, имя пользователя и рабочую директорию. Всё это даст нам Java API из JDK — бесплатно, без зависимостей и без магии.
Чтобы примеры связались в единое целое, будем считать, что у нас уже есть упрощённый анализатор текста (в стиле лекций про пайплайны): токенизация, подсчёт частот, вывод top‑N.
Генерируем идентификатор запуска через UUID.randomUUID()
В длинных сценариях полезно уметь сказать: «Вот этот отчёт относится к конкретному запуску программы». Особенно если пользователь присылает вам скриншот или кусок лога. UUID — это просто уникальная строка, которую легко сгенерировать и почти невозможно случайно повторить.
Сделаем функцию newRunId(), которая возвращает строку. Внутри используем Java‑класс UUID и его static‑метод.
import java.util.UUID
fun newRunId(): String {
return UUID.randomUUID().toString()
}
fun main() {
println(newRunId()) // например: 9a9dbb6c-2c92-40bb-9c5c-2ee7d4c8d77b
}
Мы пока не обсуждаем «почему так устроено» — нам важно другое: вы увидели, что Java static‑метод в Kotlin вызывается абсолютно естественно.
Берём информацию о среде через System.getProperty(...)
Когда программа запускается на разных машинах, иногда хочется понять: «Где я вообще работаю?». Это особенно актуально, когда у пользователя «у меня не работает», а у вас «у меня работает». Простые системные свойства помогают хотя бы начать разговор предметно: какой пользователь, какая директория запуска.
Сделаем небольшую функцию, которая собирает пару значений.
fun runtimeInfo(): String {
val user = System.getProperty("user.name")
val dir = System.getProperty("user.dir")
return "user=$user, dir=$dir"
}
fun main() {
println(runtimeInfo()) // user=alex, dir=/Users/alex/projects/analyzer
}
Здесь всё ещё нет ничего «Kotlin‑специфичного»: это обычные вызовы Java API и обычные строки Kotlin.
Собираем заголовок отчёта через StringBuilder
Когда вы выводите отчёт в консоль, очень быстро хочется красиво оформить многострочный текст: заголовок, метаданные, затем результат анализа. Конкатенация строк через + работает, но для большого текста проще и понятнее собрать всё в StringBuilder и вернуть готовую строку. Это классический подход на JVM (и да, это Java‑класс).
Сделаем функцию buildHeader(...), которая возвращает многострочный заголовок.
fun buildHeader(runId: String, info: String): String {
val sb = StringBuilder()
sb.appendLine("=== Text Analyzer Report ===")
sb.appendLine("runId: $runId")
sb.appendLine("env: $info")
return sb.toString()
}
Метод appendLine добавляет строку и перевод строки. Если у вас его нет (в зависимости от окружения), можно заменить на append("...\n"), идея та же.
Где import не нужен — и почему это нормально
Новичков часто сбивает с толку, что часть Java‑вещей мы импортируем, а часть — нет. Например, System мы не импортировали. Это не потому, что «так захотелось», а потому что на JVM некоторые пакеты подключаются автоматически (в частности, java.lang). Но на уровне практики правило простое: если компилятор не видит имя — добавьте import (или временно напишите полное имя класса) и живите дальше.
Для тренировки можете сами переписать System.getProperty как java.lang.System.getProperty — оно будет работать так же.
Используем Java ArrayList как «буфер строк» перед финальным выводом
Иногда удобно строить отчёт не одной строкой, а списком строк: добавил строку — потом объединил. Это подход «буферизации», и он встречается в старом Java‑коде сплошь и рядом. Мы сделаем это нарочно, чтобы вы почувствовали стиль Java API: создали изменяемую структуру, наполнили, затем преобразовали.
import java.util.ArrayList
fun buildLines(): ArrayList<String> {
val lines = ArrayList<String>()
lines.add("line 1")
lines.add("line 2")
return lines
}
fun main() {
println(buildLines().size) // 2
}
Смысл не в том, что так «лучше, чем Kotlin». Смысл в том, что так часто бывает в API, с которым вы столкнётесь, и вам нужно чувствовать себя уверенно.
Собираем всё вместе: мини‑main нашего анализатора
Теперь склеим идеи в один маленький сценарий: генерируем runId, берём runtimeInfo, печатаем заголовок, затем читаем строку текста и выводим длину (как простейшую «заглушку анализа»). Мы не будем заново писать токенизацию и частоты — они у вас уже есть из предыдущих дней.
import java.util.UUID
fun main() {
val runId = UUID.randomUUID().toString()
val info = runtimeInfo()
println(buildHeader(runId, info))
val text = readln()
println("chars: ${text.length}") // chars: 12
}
Если вы сейчас подумали «подождите, runtimeInfo() и buildHeader() где-то выше, а тут их нет» — всё правильно: это один проект, просто функции лежат в том же файле или импортируются из другого. Мы продолжаем развивать приложение постепенно, как и делали раньше.
6. Схема: Kotlin → JVM → JDK
После первых встреч с Java API бывает ощущение, что Kotlin «внезапно стал Java». На самом деле он просто стоит на плечах JVM и использует JDK как стандартный набор инструментов. Удобно представить это в виде простой блок‑схемы: вы пишете Kotlin, он компилируется, а во время выполнения рядом лежат классы JDK, которые вы спокойно вызываете.
flowchart TD
A[Kotlin код] --> B[Компилятор Kotlin]
B --> C[байткод JVM]
C --> D[JVM]
D --> E[JDK / java.* классы]
D --> F[Ваше приложение: анализатор текста]
E --> F
Эта схема важна психологически: вы не «подключаете Java отдельно». Вы уже внутри мира JVM — просто начали осознанно брать оттуда полезные вещи.
7. Типичные ошибки при вызове Java API из Kotlin
Ошибка №1: забыли import и решили, что «в Kotlin нет такого класса».
Чаще всего класс есть, просто компилятор не знает, откуда его взять. Если вы видите Unresolved reference, попробуйте сначала написать полное имя (java.util.UUID), а затем нажмите Alt+Enter в IDE и добавьте import. Это самый быстрый способ перестать гадать и начать жить.
Ошибка №2: перепутали вызов static‑метода и метода объекта.
Если вы пишете UUID.toString() и ждёте строку, компилятор будет не в восторге: toString() — это метод объекта, а не класса. Правильная цепочка: сначала получить объект (например, UUID.randomUUID()), а потом вызвать у него метод (.toString()).
Ошибка №3: ожидают, что Java‑коллекции «ведут себя как read‑only List».
Если вы создали ArrayList и передали его куда-то, другой код может его изменить — и это не баг, а стиль Java. На практике это означает, что с Java‑объектами нужно внимательнее относиться к мутабельности: если вам важен снимок данных, вы обычно делаете копию (сейчас достаточно просто понимать проблему).
Ошибка №4: пытаются «лечить» проблему через копипаст полного имени везде.
Полные имена удобны для разового примера, но превращают реальный код в простыню. Если вы используете класс больше одного раза, лучше честно добавить import. Код станет короче, и вы начнёте видеть логику программы, а не адреса домов на улице java.util.
Ошибка №5: думают, что Java API «не для Kotlin», и избегают его даже там, где оно упрощает жизнь.
Парадоксально, но иногда новичок принципиально не хочет трогать System или UUID, потому что «это Java». На JVM это не «чужое», а стандартная библиотека среды выполнения. Если задача решается проще через JDK — берите JDK и не мучайте себя (и будущего читателя кода).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ