1. Primary constructor: «вхідні двері» до класу
Коли ми починаємо писати класи, легко подумати: «Ну, конструктор — це просто спосіб передати значення всередину». Насправді ж конструктор — це публічний вхід у ваш тип, тобто частина API класу. Це як вхідні двері до офісу: їх можна зробити зручними й чітко позначеними, а можна поставити 12 однакових дверей без табличок — і потім дивуватися, чому співробітники заходять до серверної.
У Kotlin основний спосіб створювати обʼєкти — це primary constructor, який пишуть прямо в заголовку класу. Його вважають «головним маршрутом створення», а решту коду зазвичай вибудовують навколо нього. У документації Kotlin це сформульовано прямо: primary constructor оголошують у заголовку класу, а параметри можуть ставати властивостями через val/var.
Простий приклад «клас як функція створення обʼєкта»:
class User(val name: String, var age: Int)
fun main() {
val u = User(name = "Ann", age = 20)
println(u.name) // Ann
u.age += 1
println(u.age) // 21
}
Зверніть увагу на ще один «приємний бонус Kotlin»: під час створення обʼєкта не потрібен new — ви просто викликаєте клас як функцію.
Дрібниця, але вона робить код компактнішим: менше «ритуальних слів», більше змісту.
2. Параметр конструктора vs властивість: що стає станом
У цьому місці майже всі новачки наступають на ті самі граблі. Здається логічним: «Я написав class X(a: Int) — отже, в обʼєкта є a». Але ні: параметр конструктора без val/var — це лише вхідне значення, як параметр функції. Він не зобовʼязаний ставати частиною стану обʼєкта.
Kotlin уміє робити зручну річ: якщо параметр оголошено з val або var, він одночасно є і параметром конструктора, і властивістю обʼєкта. Це один із ключових елементів «kotlin-стилю»: менше шаблонного коду.
Порівняймо два варіанти.
Варіант A: параметр — це властивість
class Label(val text: String)
fun main() {
val l = Label("Hi")
println(l.text) // Hi
}
Варіант B: параметр — не властивість
class Label(text: String) {
val text: String = text
}
fun main() {
val l = Label("Hi")
println(l.text) // Hi
}
Обидва варіанти працюють, але сенс у них різний. У варіанті B ви ніби кажете: «Під час створення мені дали text, але зберігатиму я text (своє)». У документації Kotlin окремо підкреслюють: параметри без val/var не зберігаються як властивості і доступні лише в тілі класу — доки ви не перенесете їх у властивість.
Щоб не тримати це в голові «на відчуттях», корисно зафіксувати так:
| Запис у заголовку | Це властивість обʼєкта? | Можна звернутися ззовні (obj.x)? | Типовий сенс |
|---|---|---|---|
|
ні | ні | вхідне значення «на момент створення» |
|
так (лише для читання) | так | частина стану, змінювати не можна |
|
так (змінна) | так | частина стану, можна змінювати |
3. Проєктуємо Expense: тип, який приємно створювати
Зараз буде важлива практична думка: хороший конструктор — це той, на який приємно дивитися саме в місці створення обʼєкта. Бо справжній біль не в тому, щоб написати клас, а в тому, щоб за місяць відкрити код і зрозуміти, що означає Expense(1, "Taxi", 900, "transport") — це таксі за 9 USDT чи за 900 USDT, і що взагалі означає «1».
Уявімо, що в нашому консольному трекері витрат один запис — це витрата. У неї є ідентифікатор, назва, сума (у цілих одиницях, щоб не мучитися з Double) і категорія рядком (поки рядком: enum буде пізніше).
Почнімо з простого:
class Expense(
val id: Int,
val title: String,
val amount: Int,
val category: String
)
fun main() {
val e = Expense(1, "Coffee", 350, "food")
println(e.title) // Coffee
println(e.amount) // 350
}
Уже непогано: це один обʼєкт, у ньому все разом, і ми більше не «склеюємо» дані за індексами в різних списках.
Але тепер чесно: рядок Expense(1, "Coffee", 350, "food") читається посередньо. За чотирьох параметрів це ще терпимо, але щойно зʼявиться пʼятий — почнеться «вгадайка».
Ось тут конструктор як API проявляється особливо яскраво: ми не лише обираємо типи, а й проєктуємо читабельність виклику.
4. Значення за замовчуванням: менше хаосу під час створення
У якийсь момент вам захочеться: «Нехай категорія буде необовʼязковою — якщо не вказали, вважаємо "other"». І тут є два шляхи. Перший — зробити кілька варіантів створення (спойлер: secondary constructors будуть пізніше). Другий — використати значення за замовчуванням.
Primary constructor підтримує значення за замовчуванням так само, як і функції. Це описано в документації: можна задавати значення за замовчуванням прямо в параметрах конструктора, і якщо аргумент не передали — буде використано значення за замовчуванням.
Зробімо так:
class Expense(
val id: Int,
val title: String,
val amount: Int,
val category: String = "other"
)
fun main() {
val a = Expense(1, "Coffee", 350)
val b = Expense(2, "Taxi", 900, "transport")
println(a.category) // other
println(b.category) // transport
}
Важливий ефект: тепер у нашого класу менше «режимів створення», і це добре. Мозок читача не має тримати в памʼяті, що «якщо категорія не прийшла, то треба створити обʼєкт інакше». Клас сам уміє коректно стартувати.
На практиці значення за замовчуванням найчастіше дають два виграші. По‑перше, вони зменшують кількість обовʼязкових даних: обовʼязковим лишається тільки те, без чого обʼєкт не має сенсу. По‑друге, знижують кількість помилок під час створення: менше місць, де треба «щось не забути».
5. Іменовані аргументи: читабельність на боці виклику
Тепер зробимо те, що перетворює «чотири параметри — терпимо» на «шість параметрів — усе ще читабельно»: іменовані аргументи.
Ключова ідея проста: імена параметрів конструктора — це не просто «як ви їх назвали у себе». Це частина зовнішнього інтерфейсу: люди бачитимуть їх у викликах, особливо якщо використовують іменовані аргументи. Тому невдале імʼя параметра в конструкторі — це як поганий підпис на кнопці в застосунку.
Подивімося, наскільки зрозуміліше:
class Expense(
val id: Int,
val title: String,
val amount: Int,
val category: String = "other"
)
fun main() {
val e = Expense(
id = 10,
title = "Lunch",
amount = 550,
category = "food"
)
println(e.title) // Lunch
println(e.amount) // 550
}
Так, це трохи довше. Але «довжина» тут працює як страховка від помилок — особливо коли параметри одного типу повторюються. Два рядки String підряд — класичне джерело ситуацій «переплутав місцями й не помітив».
Щоб побачити, як легко помилитися без іменованих аргументів, уявіть клас:
class Client(val firstName: String, val lastName: String)
fun main() {
val c = Client("Ivan", "Petrov")
println("${c.firstName} ${c.lastName}") // Ivan Petrov
}
Якщо ви випадково поміняєте місцями аргументи — компілятор не врятує: типи однакові. А з іменованими аргументами шанс переплутати значно менший.
6. val чи var: «можна змінювати» — це частина API
Зараз буде момент, коли програміст дорослішає й починає говорити серйозні речі на кшталт «мутабельність треба заслужити». Але без фанатизму: var — не зло, просто вона відчиняє двері до змін, а зміни потрібно контролювати.
Різниця проста. val у конструкторі означає: обʼєкт створено — і цю властивість не можна перепризначити (хоча якщо всередині зберігається мутабельний обʼєкт, це вже окрема історія, але зараз ми туди не йдемо). var означає: значення властивості можна змінювати після створення.
Наприклад, у витрати зазвичай немає сенсу змінювати id після створення. Часто немає сенсу змінювати й amount (запис витрати — це факт). Отже, у моделі логічно тримати це як val. А якщо ми хочемо дозволити редагування запису (наприклад, виправити друкарську помилку в назві), можна зробити title як var. Важливо, щоб це рішення було усвідомленим, а не «поставлю всюди var, аби не думати».
Приклад «усвідомленої мутабельності»:
class Expense(
val id: Int,
var title: String,
val amount: Int,
val category: String = "other"
)
fun main() {
val e = Expense(id = 1, title = "Cofee", amount = 350) // друкарська помилка :)
e.title = "Coffee"
println(e.title) // Coffee
}
Тут var title читається як обіцянка класу: «Назву можна виправляти». А val amount — як обіцянка: «Суму не чіпаємо, це факт витрати». Саме це й означає «конструктор — частина API»: через val/var ви повідомляєте правила використання вашого типу.
7. Коли параметр не повинен бути властивістю
Іноді в конструктор хочеться передати щось тимчасове. Наприклад, «сирий текст», який потрібен лише для того, щоб обчислити нормалізоване значення. Або, скажімо, ви приймаєте рядок " Food " і хочете зберігати "food".
Сьогодні ми поки не робимо init (це буде в наступній лекції), але важливо зрозуміти ідею: інколи параметр конструктора — це просто матеріал, з якого ви робите стан. А сам матеріал зберігати не потрібно.
Ось мікроприклад на зрозумілій темі:
class Category(raw: String) {
val name: String = raw.trim().lowercase()
}
fun main() {
val c = Category(" FoOd ")
println(c.name) // food
}
Чому raw не val raw? Бо нам не потрібно зберігати «брудне введення». Ми хочемо зберігати вже підготовлене значення, придатне для логіки.
Цей підхід добре поєднується з ідеєю з документації: параметр без val/var не є властивістю і сам по собі не живе «всередині обʼєкта».
8. Практика і корисні нюанси
Коли потрібен constructor, а коли ні
Іноді ви побачите два варіанти запису:
class Person constructor(name: String)
і
class Person(name: String)
Різниця не в змісті, а в синтаксисі. Kotlin дозволяє опустити ключове слово constructor, якщо в конструктора немає анотацій і модифікаторів видимості. Це нормальна практика: більшість класів пишуть без constructor.
У нашому курсі в 99 % випадків ми писатимемо компактно:
class Expense(val id: Int, val title: String, val amount: Int)
А слово constructor знадобиться пізніше, коли почнуться більш «дорослі» випадки (наприклад, модифікатори видимості конструктора). Але це вже не тема цієї лекції.
Схема: primary constructor як контракт
Корисно подумки уявляти, що створення обʼєкта — це міні‑потік даних: зовнішній світ передає значення → клас приймає їх за контрактом → обʼєкт зʼявляється в програмі.
Ось проста схема:
flowchart LR
A["Код ззовні
Expense(...)"] --> B["Primary constructor
(параметри)"]
B --> C["Властивості обʼєкта
(val/var)"]
C --> D["Обʼєкт готовий до використання"]
У цій картині primary constructor — це місце, де ви визначаєте контракт: які дані потрібні, які необовʼязкові (значення за замовчуванням), які стануть частиною стану (val/var), а які — просто вхідними.
Міні‑крок: список витрат на обʼєктах
Щоб не залишати все на рівні «теорії про API», зробімо мікрокрок у нашому застосунку: у main() створимо кілька витрат і виведемо їх. Жодних складних команд — просто відчуємо, що модель стала читабельною.
class Expense(
val id: Int,
val title: String,
val amount: Int,
val category: String = "other"
)
fun main() {
val expenses = mutableListOf<Expense>()
expenses.add(Expense(id = 1, title = "Coffee", amount = 350, category = "food"))
expenses.add(Expense(id = 2, title = "Taxi", amount = 900, category = "transport"))
expenses.add(Expense(id = 3, title = "Notebook", amount = 120)) // category за замовчуванням
for (e in expenses) {
println("#${e.id}: ${e.title} — ${e.amount} (${e.category})")
// #1: Coffee — 350 (food)
// #2: Taxi — 900 (transport)
// #3: Notebook — 120 (other)
}
}
Тут одразу видно, що primary constructor справді «працює як API»: він визначає, які поля є у витрати, як вони називаються і які значення можна не передавати. А іменовані аргументи перетворюють створення на майже самодокументований код.
9. Типові помилки
Помилка № 1: плутати параметр конструктора з властивістю й забувати val/var.
Це виглядає так: ви пишете class Expense(id: Int, title: String), потім намагаєтеся звернутися e.title, і компілятор каже: «нема такого». Насправді все чесно: title був лише вхідним параметром, а не частиною стану. Якщо значення має жити всередині обʼєкта — оголошуйте val title: String прямо в конструкторі або присвойте його властивості в тілі класу.
Помилка № 2: робити всі властивості var, бо «раптом знадобиться».
Це одна з найдорожчих звичок. Коли ви ставите var, ви ніби лишаєте двері відчиненими: «це можна змінювати будь-коли». Потім виявляється, що десь значення змінили, логіка «поїхала», і ви шукаєте винуватця як детектив із лупою. Починайте з val і переводьте у var лише те, що справді має змінюватися за змістом.
Помилка № 3: надто багато параметрів у primary constructor і «магічні» позиції аргументів.
Якщо конструктор вимагає 6–8 параметрів, і ви викликаєте його позиційно (наприклад, A(1, "x", "y", 10, 20, true)), код стає схожим на заклинання, яке не можна чіпати: інакше викличеться демон. На рівні цієї лекції найпростіший порятунок — активніше використовувати іменовані аргументи та значення за замовчуванням, щоб обовʼязковими залишалися тільки справді потрібні речі.
Помилка № 4: погані імена параметрів у конструкторі.
Конструктор — це частина публічного інтерфейсу. Якщо ви називаєте параметри a, b, x1, s2, то ви буквально змушуєте читача вгадувати, що передавати. Особливо боляче стає, коли ви використовуєте іменовані аргументи: Expense(a = 1, b = "Coffee", x1 = 350). Імена параметрів мають відображати сенс, бо вони житимуть у коді створення обʼєктів.
Помилка № 5: не використовувати значення за замовчуванням там, де вони природні, і через це плодити складність.
Коли параметр часто однаковий (наприклад, категорія "other" або прапорець isActive = true), зручніше дати йому значення за замовчуванням у primary constructor, ніж змушувати кожен виклик повторювати одне й те саме. Kotlin підтримує це напряму: значення за замовчуванням у primary constructor працюють так само, як значення за замовчуванням у функціях.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ