1. Kotlin/JVM і стандартна бібліотека Java: що взагалі відбувається
Коли ви пишете 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. Конструктори та «статичні» методи 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 «статичні» методи: виклик через імʼя типу
У 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): там список доступний лише для читання (за посиланням), а тут — «живий» і такий, що змінюється.
Навіщо взагалі чіпати Java‑колекції, якщо є Kotlin‑колекції?
У цій лекції ми не заглиблюватимемося в тонкощі сумісності (це окрема велика тема). Але практична причина проста: величезна кількість API у світі JVM історично приймає й повертає Java‑колекції. Тож якщо ви хочете впевнено користуватися бібліотеками, вам потрібно принаймні не лякатися ArrayList.
5. Міні‑проєкт: «Text Analyzer» дорослішає
Досі ми писали доволі автономні застосунки: ввели текст → порахували → вивели звіт. Сьогодні зробимо невеликий, але дуже «реальний» крок: додамо у звіт службову інформацію, яку зазвичай хочеться бачити в логах і звітах. Наприклад, ідентифікатор запуску, імʼя користувача та робочий каталог. Усе це дасть нам Java API з JDK — безплатно, без залежностей і без магії.
Щоб приклади склалися в єдине ціле, вважатимемо, що в нас уже є спрощений аналізатор тексту (у стилі лекцій про пайплайни): токенізація, підрахунок частот, виведення top‑N.
Генеруємо ідентифікатор запуску через UUID.randomUUID()
У довгих сценаріях корисно вміти сказати: «Оцей звіт стосується конкретного запуску застосунку». Особливо якщо користувач надсилає вам скриншот або фрагмент лога. UUID — це просто унікальний рядок, який легко згенерувати й майже неможливо випадково повторити.
Зробимо функцію newRunId(), яка повертає рядок. Усередині використаємо Java‑клас UUID та його «статичний» метод.
import java.util.UUID
fun newRunId(): String {
return UUID.randomUUID().toString()
}
fun main() {
println(newRunId()) // наприклад: 9a9dbb6c-2c92-40bb-9c5c-2ee7d4c8d77b
}
Ми поки не обговорюємо «чому так влаштовано» — зараз важливіше інше: ви побачили, що Java «статичний» метод у 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 ===")
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("рядок 1")
lines.add("рядок 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». Насправді Kotlin просто стоїть на плечах 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: переплутали виклик «статичного» методу й методу обʼєкта.
Якщо ви пишете UUID.toString() і очікуєте рядок, компілятор буде не в захваті: toString() — це метод обʼєкта, а не класу. Правильний ланцюжок такий: спочатку отримати обʼєкт (наприклад, UUID.randomUUID()), а потім викликати в нього метод (.toString()).
Помилка №3: очікують, що Java‑колекції «поводяться як список лише для читання List».
Якщо ви створили ArrayList і передали його кудись, інший код може його змінити — і це не баг, а стиль Java. На практиці це означає, що з Java‑обʼєктами треба уважніше ставитися до змінюваності: якщо вам важливий «знімок» даних, зазвичай роблять копію (зараз достатньо просто розуміти проблему).
Помилка №4: намагаються «лікувати» проблему тим, що копіюють повне імʼя всюди.
Повні імена зручні для разового прикладу, але в реальному коді швидко перетворюють усе на простирадло. Якщо ви використовуєте клас більше одного разу, краще чесно додати import. Код стане коротшим, і ви почнете бачити логіку застосунку, а не адреси будинків на вулиці java.util.
Помилка №5: думають, що Java API «не для Kotlin», і уникають його навіть там, де воно спрощує життя.
Парадоксально, але іноді новачок принципово не хоче чіпати System або UUID, бо «це Java». На JVM це не «чуже», а стандартна бібліотека середовища виконання. Якщо задачу простіше розвʼязати через JDK — беріть JDK і не ускладнюйте собі життя (і майбутньому читачеві коду теж).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ