Введение: «простая» тема, на которой сыпятся
«Что выведет этот код?» — и на доске data class User(val name: String, val age: Int) с парой println. Вопрос выглядит тривиальным, но именно здесь кандидаты начинают плавать: кто-то забывает формат toString(), кто-то не знает про copy(), а кто-то уверен, что data class можно унаследовать.
Data class в Kotlin — класс, который в основном хранит данные. Компилятор сам генерирует для него стандартный набор методов, и это избавляет от боilerplate-кода, который в Java писали руками. В Android-проекте data class встречается на каждом шагу: DTO для ответов сервера, UiState, результаты запросов, модели в слое данных.
Почему тему любят на собеседованиях? Она проверяет не знание синтаксиса, а внимательность: что именно попадает в сгенерированные методы, какие ограничения накладывает data, как работает копирование. Разберём по порядку.
Что генерирует компилятор
Когда вы пишете data class, компилятор берёт все свойства первичного конструктора и генерирует четыре вещи:
- пару
equals()иhashCode(); toString()в форматеUser(name=John, age=42);- функции
componentN()в порядке объявления свойств — для деструктуризации; - функцию
copy()для копирования с изменением части свойств.
Вот как это выглядит в деле:
data class User(val name: String, val age: Int)
fun main() {
val john = User("John", 30)
val olderJohn = john.copy(age = 31)
println(john) // User(name=John, age=30)
println(olderJohn) // User(name=John, age=31)
println(john == User("John", 30)) // true — структурное равенство
}Сравнение через == в Kotlin транслируется в equals(), поэтому два разных объекта с одинаковыми свойствами считаются равными — это главное отличие от ===, который сравнивает ссылки.
componentN() делает возможной деструктуризацию:
val (name, age) = User("Anna", 28)
println("$name, $age") // Anna, 28Что назвать первым
Ответ на «что генерирует data class» должен звучать как выстрел: equals/hashCode, toString, componentN и copy. Дальше интервьюер обычно копает в ограничения — и вот тут начинается самое интересное.
Ограничения: где компилятор скажет «нет»
Data class — не волшебная палочка, у него жёсткие правила. Их стоит выучить дословно, потому что проверяют именно их:
- первичный конструктор обязан иметь хотя бы один параметр;
- все параметры первичного конструктора должны быть
valилиvar; - data class не может быть
abstract,open,sealedилиinner; - реализовать
componentN()иcopy()вручную нельзя — компилятор сам; - если вы написали свой
equals(),hashCode()илиtoString()в теле класса, сгенерированная версия не создаётся — используется ваша.
// Ошибки компиляции:
data class Empty // первичный конструктор без параметров
open data class Base(val id: Int) // data class не может быть openНа JVM есть ещё один нюанс: если классу нужен конструктор без параметров, все свойства должны иметь значения по умолчанию.
Типичная ошибка: кандидат говорит «data class нельзя наследовать». Это неверно. Сам data class не может быть open — его нельзя унаследовать. Но он может наследовать другой класс или интерфейс: например, подкласс sealed class спокойно объявляется как data class. Разницу нужно формулировать чётко, иначе интервьюер записывает минус.
Свойства в теле класса и copy(): подводные камни
Главный подводный камень, который я вижу на собеседованиях, — свойства, объявленные в теле класса. Они не участвуют в сгенерированных методах. Работают только свойства первичного конструктора:
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println(person1 == person2) // true — age не участвует в equals
println(person1) // Person(name=John) — age нет в toString
}Второй камень — copy(). Он делает поверхностное копирование: ссылки на объекты внутри разделяются. Если у data class есть свойство-MutableList, изменения через «копию» будут видны в оригинале, и наоборот.
data class Employee(val name: String, val roles: MutableList<String>)
fun main() {
val original = Employee("Jamie", mutableListOf("developer"))
val duplicate = original.copy()
duplicate.roles.add("team lead")
println(original.roles) // [developer, team lead] — один и тот же список
}Практический вывод: если в data class есть изменяемые коллекции, копирование не изолирует состояние. В реальном проекте это источник багов: «я скопировал объект и поменял — а изменилось везде». На собеседовании этот пример мгновенно отделяет тех, кто читал документацию, от тех, кто просто писал код по шаблону.
Ещё один вопрос, который любят задавать: «Почему нельзя написать свою copy()?». Потому что компилятор генерирует её из свойств первичного конструктора — и если позволить свою версию, структура сломается. Если вам нужна «своя» копия с особой логикой — пишите обычный метод с другим именем, например copyWithAge(age: Int).
Типичные ошибки на собеседовании
Соберу ошибки, которые я регулярно слышу, в один чек-лист. Пройдитесь по нему перед собеседованием — и тема закрыта.
- «Все свойства класса участвуют в equals» — нет, только свойства первичного конструктора.
- «Data class нельзя наследовать» — он не может быть open, но может наследовать сам.
- «copy() копирует глубоко» — нет, это поверхностная копия, ссылки разделяются.
- «== сравнивает ссылки» — в Kotlin
==это структурное равенство, ссылки сравнивает===. - «componentN можно переопределить» — нельзя, это генерируемая функция.
Что важно добавить в ответ, чтобы показать глубину: в стандартной библиотеке есть готовые Pair и Triple, но в большинстве случаев осмысленный именованный data class читается лучше. И ещё: если data class нужен без параметров в конструкторе — например, для сериализации — задайте значения по умолчанию для всех свойств.
Метода
Ровно четыре вещи генерирует компилятор из свойств первичного конструктора: equals/hashCode, toString, componentN, copy. Назвали все четыре — база есть, дальше вопросы про ограничения.
Готовитесь к собеседованию на Android-разработчика?
Разбор темы Kotlin — это один из этапов подготовки к переходу на middle. На менторстве мы закрываем пробелы в языке, корутинах и архитектуре и тренируем ответы на реальных вопросах.
Итоги
Data class — тема маленькая, но меткая: она проверяет не заученные определения, а понимание того, что делает компилятор. Три тезиса, которые стоит забрать с собой:
- Компилятор генерирует четыре вещи из свойств первичного конструктора:
equals()/hashCode(),toString(),componentN()иcopy(). Свойства в теле класса в них не попадают. - Ограничения жёсткие: минимум один параметр в конструкторе, все параметры
val/var, сам класс не может бытьabstract,open,sealedилиinner. copy()— поверхностная: изменяемые коллекции внутри копии разделяются с оригиналом, и это частая причина багов.
Полное описание со всеми нюансами — в официальной документации Data classes на kotlinlang.org, там же примеры с деструктуризацией.
Главное, что нужно запомнить
Data class — это класс-«хранилище данных», для которого компилятор сам пишет equals/hashCode, toString, componentN и copy. Всё строится из свойств первичного конструктора — запомните это, и половина каверзных вопросов отпадает.
Если на пробном ответе вы поняли, что плаваете не только в data class, но и в остальных темах Kotlin — не нужно зубрить всё подряд в одиночку. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.