JavaRush /Курсы /Kotlin SELF /Синтаксис класса: primary constructor, свойства в заголов...

Синтаксис класса: primary constructor, свойства в заголовке, значения по умолчанию

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

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? Где можно использовать
name: String
нет нет только внутри тела класса (если вообще есть тело)
val name: String
да (read-only) да и внутри класса, и снаружи
var name: String
да (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 позволяет, но глаза — не обязаны. Если конструктор не помещается в одну строку без горизонтального скролла, переносите параметры на отдельные строки. Это не «косметика»: читаемость напрямую влияет на количество багов, особенно когда вы только учитесь и держите в голове сразу много нового.

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