Введение: почему классы — половина собеседования по Kotlin
«Расскажите про классы в Kotlin» — универсальный вопрос, который раскрывает кандидата быстрее любого другого: конструкторы, свойства, наследование, data class, sealed class — и всё это укладывается в один ответ.
Классы — это то, как вы моделируете предметную область: ViewModel — класс, Repository — класс, экранные состояния — data class или sealed class. На собеседовании тему проверяют с двух сторон: синтаксис (что и как объявляется) и дизайн (почему именно такая конструкция). Разберём оба уровня. Все факты — из официальной документации Kotlin.
Конструкторы и init-блоки
Класс объявляется ключевым словом class. Primary constructor (основной конструктор) пишется прямо в заголовке класса. Если параметр помечен val или var, он автоматически становится свойством класса:
class UserProfile(
val name: String,
var age: Int,
) {
init {
require(age >= 0) { "age must be non-negative" }
}
}
fun main() {
val profile = UserProfile("Анна", 29)
println(profile.name) // Анна
}Моменты, которые нужно знать:
- Экземпляр создаётся без
new— простоUserProfile("Анна", 29), как вызов функции. init-блоки выполняются при создании экземпляра в порядке объявления — вместе с инициализаторами свойств. Это стандартное место для валидации черезrequire.- Secondary constructor (дополнительный конструктор) объявляется внутри тела класса и обязан делегировать основному через
this— вся логика primary выполнится первой:
class Person(val name: String, var age: Int) {
constructor(name: String, ageText: String) : this(name, ageText.toIntOrNull() ?: 0) {
println("Создан $name с возрастом $age")
}
}- Конструктор без конструктора. Если класс не объявляет конструкторов, у него есть неявный public-конструктор без параметров. А на JVM, если все параметры primary имеют значения по умолчанию, компилятор дополнительно генерирует конструктор без параметров — это важно для библиотек вроде Jackson, которые создают объекты через пустой конструктор.
Что спросят
Primary constructor — в заголовке класса, параметры с val/var становятся свойствами. Secondary — в теле, делегирует primary через this. init-блоки выполняются в порядке объявления.
Свойства и видимость
Свойства — это поля с логикой доступа. В Kotlin у свойства есть автоматический геттер (и сеттер для var), который можно переопределить:
class Counter {
private var count = 0
val current: Int
get() = count
fun increment() {
count++
}
}
fun main() {
val counter = Counter()
counter.increment()
println(counter.current) // 1
}Модификаторы видимости — четыре штуки: private, protected, internal и public. По умолчанию — public. По документации:
private— виден только внутри класса (для членов) или внутри файла (для top-level объявлений).protected— какprivate, плюс виден в подклассах.internal— виден в пределах модуля. Это аналог «public, но только для моего модуля»: идеально для библиотек и многомодульных проектов.public— виден везде.
Ошибка-классика: кандидат говорит, что protected в Kotlin — «как в Java». Не совсем: в Kotlin protected не виден из того же пакета — только в классе и его подклассах. А internal в Java вообще нет.
Наследование и абстракция
Ключевой факт: в Kotlin классы закрыты для наследования по умолчанию. Чтобы класс можно было наследовать, его нужно пометить open. Для переопределения методов — override:
open class Animal(val name: String) {
open fun speak(): String = "..."
fun eat() = println("$name ест")
}
class Dog(name: String) : Animal(name) {
override fun speak(): String = "Woof!"
}
fun main() {
val dog = Dog("Барсик")
println(dog.speak()) // Woof!
dog.eat() // Барсик ест
}Если метод не помечен open, переопределить его нельзя; если переопределение не помечено override — ошибка компиляции. Это жёстче, чем в Java, и это осознанный дизайн: наследование — дорогая операция, её нужно включать явно.
Абстрактные классы — наоборот, открыты по умолчанию: abstract-члены не имеют реализации, их обязаны реализовать подклассы, а сам абстрактный класс инстанцировать нельзя:
abstract class Shape {
abstract fun area(): Double
}
class Circle(val radius: Double) : Shape() {
override fun area(): Double = Math.PI * radius * radius
}
fun main() {
val circle = Circle(2.0)
println(circle.area()) // 12.566370614359172
}По документации о наследовании, все классы в Kotlin неявно наследуются от Any — аналога java.lang.Object, у которого есть equals(), hashCode() и toString().
Сравнение с Java, которое любят на собеседованиях: в Java классы открыты по умолчанию (effective final — ручная дисциплина), в Kotlin закрыты и открываются явным open. В Kotlin переопределить можно только open-члены и только с override — компилятор не даст «случайного» полиморфизма.
Data class и sealed class
Data class — класс для хранения данных: компилятор автоматически генерирует equals(), hashCode(), toString(), copy() и componentN() из свойств primary-конструктора. По документации, требования: минимум один параметр в primary, все параметры — val или var, и класс не может быть abstract, open, sealed или inner.
data class User(val name: String, val age: Int)
fun main() {
val user = User("Анна", 29)
val older = user.copy(age = 30) // копия с одним изменённым полем
println(user) // User(name=Анна, age=29)
println(user == older) // false
val (name, age) = older // деструктуризация через componentN()
println("$name, $age") // Анна, 30
}copy() делает поверхностную копию: ссылки на вложенные объекты не дублируются. А свойства, объявленные в теле класса (не в primary), в equals/toString/copy не участвуют.
Sealed class — ограниченная иерархия: все прямые подклассы известны компилятору, поэтому when по sealed-классу не требует else. Классический сценарий — состояния экрана:
sealed class UiState {
data object Loading : UiState()
data class Success(val items: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
fun render(state: UiState) {
when (state) {
is UiState.Loading -> println("Загрузка...")
is UiState.Success -> println("Элементов: ${state.items.size}")
is UiState.Error -> println("Ошибка: ${state.message}")
}
}В примере Loading объявлен как data object — синглтон без данных; Success и Error — data class, потому что несут данные. Эта связка «sealed + data + object» — стандарт в реальных проектах, и интервьюеры это очень любят.
Где кандидаты ошибаются
Соберу типичные промахи, которые я вижу на собеседованиях.
- Классы и члены по умолчанию
final: наследование и переопределение — только черезopen+override. - Primary-конструктор в заголовке,
val/varсоздают свойства; параметры без модификатора — просто параметры. - Secondary-конструктор обязан делегировать primary через
this. - Видимость:
publicпо умолчанию,protected— класс и подклассы (без «того же пакета»),internal— модуль. - Data class: только свойства primary участвуют в
equals/hashCode/toString/copy;copy()— поверхностная копия. - Sealed-класс: подклассы известны компилятору,
whenбезelse.
Ошибка-классика №1: кандидат пишет class Dog : Animal() без open у Animal и удивляется ошибке компиляции. Ошибка-классика №2: data class, у которого «странно работает equals» — потому что изменяемое поле объявлено в теле класса и не участвует в сравнении. Обе ловятся за минуту, если правила держать в голове.
Когда я провожу собеседования, тема классов идёт сразу после базового Kotlin. По ответу видно, как человек мыслит: называет только синтаксис или объясняет, зачем нужен каждый механизм — sealed вместо enum для состояний, data для моделей, internal для границ модуля. В менторстве я вижу ту же картину: разработчики с опытом 1–2 года обычно знают синтаксис, но плавают в дизайне — а именно на дизайне и проверяют middle-уровень.
Плаваете в дизайне, хотя синтаксис знаете?
Бесплатный разбор: пришлите код или резюме — посмотрю и скажу, что мешает перейти на следующий грейд.
Итоги
Классы в Kotlin — это не «ещё один способ написать класс», а другой дизайн объектной модели.
- Конструкторы: primary в заголовке со свойствами, secondary с делегацией,
init-блоки в порядке объявления. - Наследование: закрыто по умолчанию,
open+overrideявно; абстрактные классы открыты. - Специальные классы:
dataдля моделей данных,sealedдля ограниченных иерархий,internalдля границ модуля.
Главное
В Kotlin классы закрыты по умолчанию, а специальные формы — data class и sealed class — закрывают типовые задачи: модели данных и ограниченные состояния. Уверенно отвечаете про конструкторы, open/override и data class — тема закрыта.
Если чувствуете, что знания Kotlin «на уровне синтаксиса», а вопросы про дизайн выбивают из колеи, — не нужно готовиться в одиночку. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрыть в первую очередь.