Data-классы
Data-классы, сгенерированные equals/hashCode/copy и их использование как ключей map.
8 вопросов
JuniorТеорияОчень частоКакие члены компилятор генерирует для data class в Kotlin?
Какие члены компилятор генерирует для data class в Kotlin?
Для data class компилятор генерирует equals, hashCode, toString, copy и функции componentN. Все они выводятся только из свойств первичного конструктора — свойство, объявленное в теле класса, не участвует ни в одном из них.
Типичные ошибки
- ✗Думать, что свойство из тела участвует в сгенерированных
equals/hashCode - ✗Ожидать, что у
copyбудет параметр для свойства, объявленного в теле - ✗Считать, что обычный класс тоже бесплатно получает структурный
equals
Уточняющие вопросы
- →Какие из сгенерированных членов вы всё ещё можете переопределить вручную?
- →Что генерируемая функция
copyделает со свойством, объявленным в теле?
JuniorТеорияЧастоЧто делает тип пригодным для деструктуризации в val (a, b) = x?
Что делает тип пригодным для деструктуризации в val (a, b) = x?
Деструктуризация — не магия кортежей: компилятор переписывает val (a, b) = x в x.component1() и x.component2(). Любой тип пригоден для неё, как только объявит эти operator fun componentN(); data class получает их сгенерированными из свойств первичного конструктора.
Типичные ошибки
- ✗Думать, что деструктуризация связывает по имени свойства, а не по позиции
- ✗Считать, что деструктурировать можно только
data class - ✗Забывать, что
componentNдолжна быть объявлена какoperator
Уточняющие вопросы
- →Как пропустить ненужную позицию внутри деструктурирующего объявления?
- →Почему перестановка параметров первичного конструктора
data classмолча ломает вызывающий код?
JuniorДебаггингЧастоПочему этот data class ведёт себя неожиданно как ключ map?
Почему этот data class ведёт себя неожиданно как ключ map?
data class генерирует equals/hashCode только из свойств первичного конструктора, исключая id из тела. Виноват Address: обычный класс сравнивается по ссылке, поэтому пользователи не равны. Вывод — 2, затем null. Решение — сделать Address тоже data class.
Типичные ошибки
- ✗Думать, что равенство
data classохватывает свойства из тела, такие какid - ✗Считать, что вложенное не-
dataполе сравнивается по значению автоматически - ✗Полагать, что
mapOfдедуплицирует ключи иначе, чемHashMap
Уточняющие вопросы
- →Для каких свойств генерируемая функция
copyпринимает параметры? - →Почему
hashCodeобязан согласовываться сequalsдля корректного поиска в map?
MiddleТеорияИногдаПочему data class не может быть open, и что делать вместо этого?
Почему data class не может быть open, и что делать вместо этого?
data class неявно final: подкласс с новым состоянием сломал бы сгенерированные equals/hashCode/copy, которые видят лишь свойства первичного конструктора. Сам он может реализовывать интерфейсы и наследовать класс — для переиспользования берите композицию или sealed-иерархию.
Типичные ошибки
- ✗Думать, что
openуdata classдостаточно просто явно написать - ✗Считать, что
data classтакже не может реализовывать интерфейсы и наследовать класс - ✗Ожидать, что свойство подкласса попадёт в унаследованный сгенерированный
equals
Уточняющие вопросы
- →Как sealed-иерархия из data-классов заменяет наследование от одного из них?
- →Почему делегирование интерфейса подходит
data classлучше базового класса?
MiddleТеорияИногдаКогда equals/hashCode у data class нужно писать вручную?
Когда equals/hashCode у data class нужно писать вручную?
Всегда, когда идентичность — не ровно свойства первичного конструктора. Свойство типа Array сравнивается по ссылке, поэтому равное содержимое выглядит неравным; поле-timestamp разводит объекты, которые суть одно и то же. Написанная вами пара обязана быть согласованной — равные объекты возвращают одинаковый hashCode.
Типичные ошибки
- ✗Считать, что свойство
Arrayсравнивается по содержимому в сгенерированномequals - ✗Переопределять один
equals, оставляя сгенерированныйhashCodeнесогласованным - ✗Класть timestamp или кэш в первичный конструктор
data class
Уточняющие вопросы
- →Какая функция stdlib сравнивает два массива по содержимому, а не по ссылке?
- →Что ломается в
HashMap, когдаhashCodeне согласован сequals?
MiddleКодИногдаОтформатируйте Map, деструктурируя записи в заголовке for
Отформатируйте Map, деструктурируя записи в заголовке for
Цикл обходит значения Map.Entry и деструктурирует каждую, поэтому k — это component1(), а v — component2(): это operator-расширения stdlib для Map.Entry, так что запись не обязана быть data class. Записи идут в собственном порядке обхода map; пустая map даёт пустую строку.
Типичные ошибки
- ✗Думать, что
Map.Entry— этоdata class, а не интерфейс с расширениямиcomponentN - ✗Считать, что деструктурирующий заголовок
forаллоцируетPairна каждую запись - ✗Ожидать, что порядок записей не определён для любой реализации
Map
Уточняющие вопросы
- →Как в этом цикле деструктурировать только значение, игнорируя ключ?
- →Какая реализация
Mapгарантирует порядок вставки, на который вы здесь опираетесь?
MiddleТеорияИногдаКогда Pair/Triple уместнее специально написанного data class?
Когда Pair/Triple уместнее специально написанного data class?
Pair и Triple — обобщённые data-классы с компонентами first, second, third: годятся для короткоживущей локальной пары или приватного возвращаемого значения. В публичном API эти имена ничего не значат — специальный data class называет свои поля и умеет проверять их в init.
Типичные ошибки
- ✗Думать, что у
Pairнет структурного равенства и он не годится в ключи map - ✗Возвращать
Pairиз публичного API, где имена компонентов ничего не значат - ✗Считать
Pair/Tripleкортежами без аллокации, а не data-классами
Уточняющие вопросы
- →Что на самом деле создаёт
toи почему это всего лишь infix-функция? - →Как именованный
data classв возвращаемом типе облегчает будущее изменение формы результата?
SeniorТеорияРедкоЧем равенство Kotlin data class отличается от Java record?
Чем равенство Kotlin data class отличается от Java record?
Состояние record — ровно его компоненты, поэтому сгенерированные equals/hashCode покрывают его целиком. data class может нести дополнительные свойства из тела, и они исключены — два объекта могут быть равны, различаясь состоянием в теле, а copy молча его теряет.
Типичные ошибки
- ✗Считать, что у
data classиrecordодинаковая семантика равенства - ✗Забывать, что
recordнекуда положить состояние вне своих компонентов - ✗Ожидать, что
copyперенесёт свойство, объявленное в теле класса
Уточняющие вопросы
- →Почему
recordне может объявить лишнее поле экземпляра вне своих компонентов? - →Что компилятор Kotlin выдаёт, когда вы помечаете
data classкак@JvmRecord?