Введение: «простая» тема, на которой сыпятся

«Что выведет этот код?» — и на доске 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() для копирования с изменением части свойств.

Вот как это выглядит в деле:

User.kt
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() делает возможной деструктуризацию:

Destructuring.kt
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() в теле класса, сгенерированная версия не создаётся — используется ваша.
Wrong.kt
// Ошибки компиляции:
data class Empty // первичный конструктор без параметров
open data class Base(val id: Int) // data class не может быть open

На JVM есть ещё один нюанс: если классу нужен конструктор без параметров, все свойства должны иметь значения по умолчанию.

Типичная ошибка: кандидат говорит «data class нельзя наследовать». Это неверно. Сам data class не может быть open — его нельзя унаследовать. Но он может наследовать другой класс или интерфейс: например, подкласс sealed class спокойно объявляется как data class. Разницу нужно формулировать чётко, иначе интервьюер записывает минус.

Свойства в теле класса и copy(): подводные камни

Главный подводный камень, который я вижу на собеседованиях, — свойства, объявленные в теле класса. Они не участвуют в сгенерированных методах. Работают только свойства первичного конструктора:

Person.kt
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, изменения через «копию» будут видны в оригинале, и наоборот.

Employee.kt
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 — тема маленькая, но меткая: она проверяет не заученные определения, а понимание того, что делает компилятор. Три тезиса, которые стоит забрать с собой:

  1. Компилятор генерирует четыре вещи из свойств первичного конструктора: equals()/hashCode(), toString(), componentN() и copy(). Свойства в теле класса в них не попадают.
  2. Ограничения жёсткие: минимум один параметр в конструкторе, все параметры val/var, сам класс не может быть abstract, open, sealed или inner.
  3. copy() — поверхностная: изменяемые коллекции внутри копии разделяются с оригиналом, и это частая причина багов.

Полное описание со всеми нюансами — в официальной документации Data classes на kotlinlang.org, там же примеры с деструктуризацией.

Главное, что нужно запомнить

Data class — это класс-«хранилище данных», для которого компилятор сам пишет equals/hashCode, toString, componentN и copy. Всё строится из свойств первичного конструктора — запомните это, и половина каверзных вопросов отпадает.

Если на пробном ответе вы поняли, что плаваете не только в data class, но и в остальных темах Kotlin — не нужно зубрить всё подряд в одиночку. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.