1. Primary constructor и свойства
Когда мы впервые видим класс в Kotlin, кажется, что это какой-то «особый вид функции» с фигурными скобками. И это почти правда: создание объекта реально похоже на вызов функции. Primary constructor — это и есть та самая «входная дверь», через которую мы передаём стартовые значения в новый объект. Он пишется прямо в заголовке класса, сразу после имени.
В Kotlin primary constructor объявляется в заголовке класса: после имени в круглых скобках. Если нет аннотаций и модификаторов видимости, ключевое слово constructor можно не писать.
Посмотрим на самый «в лоб» вариант — с явным constructor (так проще увидеть, где он начинается и заканчивается):
class Expense constructor(title: String, amount: Int)
fun main() {
val e = Expense("Coffee", 300)
println(e) // Expense@<какой-то_хэш>
}
Да, этот код скомпилируется, объект создастся. Но тут есть важная деталь: title и amount сейчас не стали свойствами. Это просто параметры, которые пришли «на вход», а дальше… а дальше они никуда не записались, потому что мы не сказали Kotlin, что хотим их хранить.
Чтобы показать проблему чуть нагляднее, представим, что мы пытаемся так сделать:
class Expense constructor(title: String, amount: Int)
fun main() {
val e = Expense("Coffee", 300)
// println(e.title) // так нельзя: title не является свойством
}
Компилятор будет прав: у объекта нет свойства title, потому что мы его не объявили.
Параметр конструктора и свойство
На этом месте у новичков обычно начинается маленькая драма: «Я же передал title в класс — почему я не могу его прочитать через точку?» Ответ прагматичный: потому что Kotlin различает «значение, пришедшее при создании» и «значение, которое хранится в объекте». Если вы хотите, чтобы параметр превратился в часть объекта, его нужно объявить как свойство — то есть добавить val или var.
В Kotlin primary constructor может объявлять параметры как свойства: val создаёт read-only свойство, var — изменяемое. Такие свойства хранятся в экземпляре и доступны снаружи через точку.
Сделаем нормальную версию Expense:
class Expense(val title: String, val amount: Int)
fun main() {
val e = Expense("Coffee", 300)
println(e.title) // Coffee
println(e.amount) // 300
}
Теперь title и amount — свойства, они живут внутри объекта, и мы можем их читать.
Чтобы закрепить разницу, удобна маленькая табличка:
| Запись в конструкторе | Это свойство объекта? | Можно обратиться как obj.x? | Где можно использовать |
|---|---|---|---|
|
нет | нет | только внутри тела класса (если вообще есть тело) |
|
да (read-only) | да | и внутри класса, и снаружи |
|
да (mutable) | да | и внутри класса, и снаружи |
Теперь пример, который часто встречается в реальности: мы хотим принять «сырой ввод», обработать его (например, trim()), и только потом сохранить в свойство. Тогда параметр конструктора не обязан быть свойством — он может быть просто «входным аргументом»:
class User(nameInput: String) {
val name: String = nameInput.trim()
}
fun main() {
val u = User(" Ann ")
println(u.name) // Ann
// println(u.nameInput) // нельзя: это не свойство
}
Это полезный шаблон мышления: параметр — это то, что пришло при создании, а свойство — то, что мы решили оставить внутри объекта.
2. Создание объектов без new
Когда мы создаём объект, мы пишем имя класса и круглые скобки, как будто вызываем функцию. И это не случайно: конструктор действительно вызывается как «функция создания экземпляра». В Kotlin нет ключевого слова new — объект создаётся просто через ClassName(...).
Давайте сделаем пример в стиле нашего приложения (консольный трекер расходов). Пока просто создадим пару расходов и выведем их:
class Expense(val title: String, val amount: Int)
fun main() {
val coffee = Expense("Coffee", 300)
val taxi = Expense("Taxi", 1200)
println("${coffee.title}: ${coffee.amount}") // Coffee: 300
println("${taxi.title}: ${taxi.amount}") // Taxi: 1200
}
Обратите внимание на тонкость из модели «ссылок на объект». Даже если переменная объявлена как val, это означает «ссылку нельзя переназначить», но не «объект магически заморожен». Просто сейчас у нас свойства тоже val, поэтому объект получается «стабильный».
Вот мини-демонстрация «двух имён одного объекта», чтобы мозг не забывал модель:
class Expense(val title: String, val amount: Int)
fun main() {
val a = Expense("Coffee", 300)
val b = a
println(a === b) // true
}
a === b истинно, потому что это одна и та же ссылка. Это знание пригодится, когда появятся var-свойства, но сегодня наша цель — именно синтаксис конструктора.
3. Значения по умолчанию
Как только у класса появляется больше двух полей, у нас появляется и новая боль: «я каждый раз обязан передавать всё, даже если часть значений типично одинаковая». Kotlin решает это ровно так же, как в функциях: значения по умолчанию.
В primary constructor можно задавать дефолтные значения прямо рядом с параметрами/свойствами. Если значение не передано при создании, будет использован дефолт.
Добавим в наш Expense категорию и сделаем её дефолтной (строкой, без усложнений):
class Expense(
val title: String,
val amount: Int = 0,
val category: String = "OTHER"
)
fun main() {
val e1 = Expense("Coffee")
val e2 = Expense("Taxi", 1200)
val e3 = Expense("Lunch", 900, "FOOD")
println("${e1.title}, ${e1.amount}, ${e1.category}") // Coffee, 0, OTHER
println("${e2.title}, ${e2.amount}, ${e2.category}") // Taxi, 1200, OTHER
println("${e3.title}, ${e3.amount}, ${e3.category}") // Lunch, 900, FOOD
}
Смысл дефолтов здесь не в том, чтобы «скрыть проблему». Смысл в том, чтобы сделать создание объекта удобным там, где значения действительно часто одинаковые, и при этом не размазывать волшебные числа и строки по всему коду.
Ещё один практичный момент: дефолтные значения — это способ сделать «минимально валидный» объект без лишнего шума. Например, если у нас в команде add пользователь может не указать категорию, мы спокойно создаём объект с "OTHER".
4. Именованные аргументы
Когда у конструктора несколько параметров одного типа (например, две строки подряд), очень легко перепутать их местами. У человека в голове «ну это же очевидно», у компьютера в голове «ха-ха, ты передал строку в строку — всё законно». Чтобы снизить такие ошибки, Kotlin позволяет вызывать конструктор с именованными аргументами — как функции.
Именованные аргументы особенно полезны в двух ситуациях: когда много параметров и когда есть дефолты. Тогда вызов становится самодокументируемым: его можно читать почти как предложение на человеческом языке.
class Expense(
val title: String,
val amount: Int = 0,
val category: String = "OTHER"
)
fun main() {
val e = Expense(
title = "Books",
amount = 1500,
category = "EDUCATION"
)
println("${e.title}: ${e.amount} (${e.category})") // Books: 1500 (EDUCATION)
}
Теперь вызов сложно «случайно сломать», потому что смысл параметров написан прямо в коде.
Именованные аргументы прекрасно сочетаются с дефолтами: можно указать только то, что нужно, и не думать о «дырках» в середине списка аргументов:
class Expense(
val title: String,
val amount: Int = 0,
val category: String = "OTHER"
)
fun main() {
val e = Expense(title = "Coffee", category = "FOOD")
println("${e.title}, ${e.amount}, ${e.category}") // Coffee, 0, FOOD
}
Обратите внимание: мы пропустили amount, и это нормально, потому что у него есть значение по умолчанию.
5. Читаемый заголовок класса
Первые два поля обычно помещаются в одну строку, и всё выглядит мило. Но жизнь быстро приносит нам третий параметр, потом четвёртый… и внезапно заголовок класса становится шире экрана, а читать его можно только с горизонтальным скроллом. Поэтому полезно заранее договориться со своим будущим «я»: как мы будем форматировать конструктор.
Хороший стиль для начинающих — переносить параметры на отдельные строки, если их больше двух или если они длинные. Это не про «красоту ради красоты», а про уменьшение когнитивной нагрузки: когда код легко сканируется глазами, в нём меньше ошибок.
class Expense(
val title: String,
val amount: Int = 0,
val category: String = "OTHER"
)
Ещё один важный момент: если параметр не должен быть доступен снаружи, не делайте его свойством «по привычке». Иногда хочется поставить val везде — «пусть будет». Но так вы расширяете API класса без необходимости. Старайтесь, чтобы в заголовке оставались только действительно нужные поля модели.
Небольшая схема того, что происходит при создании объекта (очень упрощённо, но полезно для головы):
flowchart TD
A["Expense(...)"] --> B["Primary constructor получает аргументы"]
B --> C["val/var параметры становятся свойствами"]
C --> D["Объект создан, доступ через точку: e.title, e.amount"]
Эта схема — про ту часть, которую мы изучаем сегодня: «передали → сохранили в свойства → читаем через точку».
6. Миграция от Pair к классу Expense
До классов мы часто делали так: один расход — это Pair<String, Int>, где first — название, second — сумма. Это работало, но читалось как «записка на холодильнике»: смысл есть, но его нужно помнить. Сейчас мы заменим это на Expense(title, amount, category).
Покажу очень маленький фрагмент «как было» (условно, в нашем CLI-трекере расходов):
fun main() {
val expenses = mutableListOf<Pair<String, Int>>()
expenses.add("Coffee" to 300)
val (title, amount) = expenses[0]
println("$title: $amount") // Coffee: 300
}
Теперь «как станет». Создадим класс и список объектов:
class Expense(val title: String, val amount: Int)
fun main() {
val expenses = mutableListOf<Expense>()
expenses.add(Expense("Coffee", 300))
val e = expenses[0]
println("${e.title}: ${e.amount}") // Coffee: 300
}
С точки зрения операций со списком ничего магически не изменилось: MutableList как был, так и остался. Но чтение стало человеческим: e.title вместо pair.first. Это и есть та самая «прибыль» от модели предметной области.
Теперь добавим дефолт категории, чтобы наша команда add могла быть проще:
class Expense(
val title: String,
val amount: Int,
val category: String = "OTHER"
)
fun main() {
val expenses = mutableListOf<Expense>()
expenses.add(Expense("Coffee", 300, category = "FOOD"))
expenses.add(Expense("Taxi", 1200)) // category = OTHER
for (e in expenses) {
println("${e.title}: ${e.amount} (${e.category})")
// Coffee: 300 (FOOD)
// Taxi: 1200 (OTHER)
}
}
Это уже выглядит как заготовка для нормального приложения: мы можем хранить данные структурированно, а ввод/команды в main будут просто создавать объекты.
7. Типичные ошибки при объявлении классов и primary constructor
Ошибка №1: забыли val/var и ожидали, что параметр станет свойством.
Очень распространённая ситуация: написали class Expense(title: String, amount: Int) и потом удивились, почему e.title не существует. Лекарство одно: если поле должно быть доступно через точку и храниться в объекте — объявляйте его как val title: String или var title: String в заголовке. Если поле нужно только как «вход», оставляйте без val/var, но тогда внутри класса явно сохраните его в свойство.
Ошибка №2: сделали всё var, потому что «вдруг пригодится».
Это не синтаксическая ошибка, но логическая мина замедленного действия. Чем больше var, тем больше мест, где объект может измениться неожиданно, и тем сложнее отлаживать «почему сумма стала 0». На старте лучше выбирать val как дефолт и добавлять var только тогда, когда вы можете честно ответить: «Да, по смыслу это поле должно меняться после создания объекта».
Ошибка №3: перепутали порядок аргументов в конструкторе.
Когда в конструкторе несколько параметров, особенно одинаковых типов, позиционные аргументы легко перепутать. Компилятор не спасёт: String на месте String выглядит законно. Поэтому там, где смысл важнее скорости набора, используйте именованные аргументы: Expense(title = "...", amount = ...). Это заметно снижает количество тихих ошибок.
Ошибка №4: злоупотребили значениями по умолчанию и «спрятали смысл».
Дефолты удобны, но ими можно переборщить: когда у класса 8 параметров, и 7 из них имеют значения по умолчанию, объект начинает создаваться «как-то сам», и становится неясно, что реально важно. Хорошая проверка: если параметр важен всегда (например, сумма расхода), вероятно, он не должен иметь дефолт, чтобы ошибка ловилась сразу при создании.
Ошибка №5: слишком длинный заголовок класса в одну строку.
Синтаксис Kotlin позволяет, но глаза — не обязаны. Если конструктор не помещается в одну строку без горизонтального скролла, переносите параметры на отдельные строки. Это не «косметика»: читаемость напрямую влияет на количество багов, особенно когда вы только учитесь и держите в голове сразу много нового.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ