JavaRush /Курсы /Kotlin SELF /Primary constructor как API класса: параметры, свойства и...

Primary constructor как API класса: параметры, свойства и дефолты

Kotlin SELF
31 уровень , 0 лекция
Открыта

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)? Типичный смысл
class A(x: Int)
нет нет входное значение «на момент создания»
class A(val x: Int)
да (read-only) да часть состояния, менять нельзя
class A(var x: Int)
да (mutable) да часть состояния, можно менять

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 поддерживает default values так же, как функции. Это официально описано: можно задавать дефолты прямо в параметрах конструктора, и если аргумент не передали — будет использовано значение по умолчанию.

Сделаем так:

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. Именованные аргументы: читаемость на стороне вызова

Теперь мы сделаем то, что превращает «четыре параметра — терпимо» в «шесть параметров — всё ещё читаемо»: именованные аргументы.

Ключевая идея простая: имена параметров конструктора — это не просто «как вы их назвали у себя». Это часть внешнего интерфейса: люди будут их видеть в вызовах, особенно если используют named arguments. Поэтому плохое имя параметра в конструкторе — это как плохая подпись на кнопке в приложении.

Смотрим, насколько понятнее:

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
}

Если вы случайно поменяете местами — компилятор не спасёт: типы одинаковые. А вот с named arguments шанс перепутать намного меньше.

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: не использовать default values там, где они естественны, и из-за этого плодить сложность.
Когда параметр часто одинаковый (например, категория "other" или флаг isActive = true), удобнее дать ему значение по умолчанию в primary constructor, чем заставлять каждый вызов повторять одно и то же. Kotlin это поддерживает напрямую: дефолты в primary constructor работают так же, как дефолты в функциях.

1
Задача
Kotlin SELF, 31 уровень, 0 лекция
Недоступна
Бейдж волонтёра
Бейдж волонтёра
1
Задача
Kotlin SELF, 31 уровень, 0 лекция
Недоступна
Настройки подключения
Настройки подключения
1
Задача
Kotlin SELF, 31 уровень, 0 лекция
Недоступна
Карточка клиента
Карточка клиента
1
Задача
Kotlin SELF, 31 уровень, 0 лекция
Недоступна
Метка категории
Метка категории
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ