В 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 работают вместе.