Классы в Kotlin короче, чем в Java, но под капотом те же JVM-классы. На собесе спрашивают, что генерирует компилятор и как выбирать между data, sealed, enum и object.
Конструкторы и свойства
class User(val id: Long, var name: String = "Гость") {
init { require(id > 0) { "id должен быть положительным" } }
constructor(name: String) : this(1, name) // вторичный конструктор
val isGuest: Boolean
get() = name == "Гость" // вычисляемое свойство
}
val/var в первичном конструкторе сразу объявляют свойства. Свойство это не поле: у него есть геттер (и сеттер у var), а поле (field) создаётся, только если оно нужно. Блоки init и инициализаторы свойств выполняются сверху вниз в порядке объявления.
final по умолчанию
Классы и методы в Kotlin final. Чтобы наследоваться, нужен open (или abstract), для переопределения override. Это защищает от хрупкого базового класса. Spring и подобные фреймворки, которым нужны прокси, решают это плагином kotlin-allopen (kotlin-spring).
Видимость: public по умолчанию, private, protected, и internal (виден в пределах модуля Gradle). Пакетной видимости, как в Java, нет.
data class
data class Point(val x: Int, val y: Int)
val p = Point(1, 2)
val q = p.copy(y = 5) // Point(x=1, y=5)
val (x, y) = p // деструктуризация
println(p == Point(1, 2)) // true
Компилятор генерирует equals, hashCode, toString, copy и componentN только по свойствам первичного конструктора. Свойство, объявленное в теле класса, в equals не участвует. Требования: в первичном конструкторе хотя бы один параметр, все параметры val или var, класс не может быть open, abstract, sealed или inner.
copy делает поверхностную копию: вложенный MutableList окажется общим у оригинала и копии.
Частая ошибка: data class с var-полями как ключ HashMap. Изменили поле, хеш изменился, и элемент «потерялся» в мапе.
sealed-классы и интерфейсы
Закрытая иерархия: все прямые наследники известны компилятору (они в том же модуле и пакете).
sealed interface UiState {
data object Loading : UiState
data class Content(val items: List<String>) : UiState
data class Error(val message: String) : UiState
}
fun render(s: UiState) = when (s) {
UiState.Loading -> "..."
is UiState.Content -> "${s.items.size} шт."
is UiState.Error -> s.message
} // else не нужен: when полный
Чем sealed лучше enum: у каждого варианта могут быть свои поля и сколько угодно экземпляров. Чем лучше обычного наследования: when проверяется на полноту, добавили вариант, и компилятор покажет все места, где его забыли обработать.
enum
enum class Status(val code: Int) {
ACTIVE(1), BLOCKED(2);
fun isOk() = this == ACTIVE
}
Status.valueOf("ACTIVE"); Status.entries // entries вместо values() с Kotlin 1.9
Enum подходит, когда варианты одинаковы по структуре и это синглтоны.
object и companion object
object Config { }это синглтон, создаётся лениво и потокобезопасно при первом обращении.companion objectэто объект внутри класса, аналог статических членов:User.create(). На JVM его члены не статические, если не добавить@JvmStatic.object : Listener { }это выражение-объект, анонимный класс.data object(Kotlin 1.9+) даёт синглтону красивыйtoStringи корректныйequals, удобно в sealed-иерархиях.
value class
@JvmInline
value class UserId(val value: Long)
Обёртка для типобезопасности без накладных расходов: в большинстве мест компилируется в голый Long. Упаковка всё же происходит, когда значение используется как nullable, как дженерик или как интерфейс.
Вложенные и внутренние классы
class Outer { class Nested } это статический вложенный класс, ссылки на внешний нет. inner class хранит ссылку на внешний объект (как нестатический внутренний в Java), и это частая причина утечек памяти в Android.
Частые ошибки
- Ожидать, что свойство из тела
data classучаствует вequals. - Использовать изменяемый
data classкак ключHashMapили элементHashSet. - Думать, что
copyглубокая. - Забыть
openи удивляться ошибке «This type is final». - Писать
inner classтам, где хватает вложенного.
Что спросят на собесе
- Что генерирует
data classи какие у него ограничения? - Чем sealed-класс отличается от enum и abstract-класса?
- Как в Kotlin сделать синглтон и статический метод?
- Почему классы по умолчанию
final? - Что такое value class и когда он всё-таки упаковывается?