В Java значительную часть кода занимают церемонии вокруг классов: поля, геттеры, сеттеры, конструкторы, equals/hashCode, статические фабрики. Kotlin сокращает это в разы — но не магией, а другими конструкциями по умолчанию. Разберём четыре: свойства, data classes, sealed-иерархии и object.

Свойства вместо полей

class Account(val id: String, var balance: BigDecimal)

Одна строка объявляет класс, конструктор и два свойства: id — только чтение, balance — чтение и запись. Обращение — как к полю (account.balance), но под капотом это геттер/сеттер, и их можно переопределить, не меняя вызывающий код:

var balance: BigDecimal = BigDecimal.ZERO
    set(value) {
        require(value >= BigDecimal.ZERO) { "Баланс не может быть отрицательным" }
        field = value
    }

Классы по умолчанию final — наследоваться нельзя, пока не написано open. Это перевёрнутое java-умолчание: расширяемость — осознанное решение, а не побочный эффект. Для Spring это создаёт нюанс с прокси — решается плагином.

data class: носитель данных за одну строку

data class Money(val amount: BigDecimal, val currency: String)

Компилятор генерирует equals, hashCode, toString, componentN для деструктуризации и copy:

val price = Money(BigDecimal("99.90"), "RUB")
val discounted = price.copy(amount = BigDecimal("79.90"))   // новый объект, старый не тронут
val (amount, currency) = price                              // деструктуризация

copy — ключ к работе с неизменяемыми объектами: меняете одно свойство — получаете новый экземпляр. Сравнение с альтернативами из Java-мира:

  • record (Java 16+) — ближайший аналог, но без copy и с позиционным конструктором без значений по умолчанию;
  • Lombok @Value/@Builder — закрывает то же, но через кодогенерацию аннотациями; в Kotlin надобность отпадает — data class плюс именованные аргументы заменяют и Value, и Builder.

sealed: закрытые иерархии

sealed interface PaymentResult
data class Success(val transactionId: String) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data object GatewayTimeout : PaymentResult

sealed означает: все наследники известны компилятору. Поэтому when по такому типу проверяется на полноту — забыли ветку, получили ошибку компиляции:

val message = when (result) {
    is Success -> "Платёж ${result.transactionId} проведён"
    is Declined -> "Отказ: ${result.reason}"
    GatewayTimeout -> "Шлюз не ответил, попробуйте позже"
}   // ветка else не нужна — и не пишите её: она отключит проверку полноты

Это рабочая лошадка моделирования результатов и состояний: вместо исключений на каждый исход или флагов-строк — типизированная иерархия, где добавление нового исхода подсвечивает все места, которые нужно обновить. Java добавила sealed в 17-й версии — идея та же.

object: синглтон и «статика»

object Clock {
    fun now(): Instant = Instant.now()
}

object — класс с единственным экземпляром, создаётся лениво и потокобезопасно. Для «статических» членов обычного класса — companion object:

class Order private constructor(val id: String) {
    companion object {
        fun create(): Order = Order(UUID.randomUUID().toString())
    }
}

val order = Order.create()

Статических методов в Kotlin нет — их роль играют функции верхнего уровня (для утилит) и companion object (для фабрик и констант класса).

Коротко

  • Свойства объявляются прямо в конструкторе: class Account(val id: String, var balance: BigDecimal) — это класс, конструктор, геттеры и сеттер разом.
  • Классы по умолчанию final: наследование — через явный open.
  • data class генерирует equals/hashCode/toString/copy/деструктуризацию; с именованными аргументами заменяет и record, и Lombok-билдеры.
  • sealed — закрытая иерархия: when проверяется на полноту веток, else не нужен и вреден.
  • object — синглтон, companion object — фабрики и константы; статических методов нет, утилиты — функции верхнего уровня.

Что почитать дальше

  • Функции и коллекции — следующий шаг маршрута.
  • Java: современные возможности — records и sealed на стороне Java.
  • Null-safety: типы против NPE — если пропустили: свойства и nullability работают вместе.