Data class kotlin что это
Data classes
It is not unusual to create classes whose main purpose is to hold data. In such classes, some standard functionality and some utility functions are often mechanically derivable from the data. In Kotlin, these are called data classes and are marked with data :
The compiler automatically derives the following members from all properties declared in the primary constructor:
equals() / hashCode() pair
toString() of the form «User(name=John, age=42)»
componentN() functions corresponding to the properties in their order of declaration.
copy() function (see below).
To ensure consistency and meaningful behavior of the generated code, data classes have to fulfill the following requirements:
The primary constructor needs to have at least one parameter.
Data classes cannot be abstract, open, sealed, or inner.
Additionally, the generation of data class members follows these rules with regard to the members’ inheritance:
If a supertype has componentN() functions that are open and return compatible types, the corresponding functions are generated for the data class and override those of the supertype. If the functions of the supertype cannot be overridden due to incompatible signatures or due to their being final, an error is reported.
Providing explicit implementations for the componentN() and copy() functions is not allowed.
Data classes may extend other classes (see Sealed classes for examples).
On the JVM, if the generated class needs to have a parameterless constructor, default values for the properties have to be specified (see Constructors).
Properties declared in the class body
The compiler only uses the properties defined inside the primary constructor for the automatically generated functions. To exclude a property from the generated implementations, declare it inside the class body:
Copying
Use the copy() function to copy an object, allowing you to alter some of its properties while keeping the rest unchanged. The implementation of this function for the User class above would be as follows:
You can then write the following:
Data classes and destructuring declarations
Component functions generated for data classes make it possible to use them in destructuring declarations:
Standard data classes
The standard library provides the Pair and Triple classes. In most cases, though, named data classes are a better design choice because they make the code more readable by providing meaningful names for the properties.
Ключевое слово data
Если у класса указать ключевое слово data, то автоматически будут созданы и переопределены методы toString(), equals(), hashCode(), copy(). Скорее всего вы будете использовать этот вариант для создания полноценного класса-модели с геттерами и сеттерами.
В конструкторе класса у параметров следует указывать val или var.
Подобные классы часто используются при работе с JSON.
Классы данных не могут объявляться абстрактными или открытыми, так что класс данных не может использоваться в качестве суперкласса. Однако классы данных могут реализовать интерфейсы, а также могут наследоваться от других классов.
toString()
Если нужно получить информацию о классе, то достаточно вызвать имя переменной класса. Вы получите строку со всеми значениями всех свойств на основе конструктора вместо непонятных символов @Cat5edea как в Java. Такой подход удобен при тестировании и отладке. Сразу понятно, о чём идёт речь.
Можно сразу определить значение по умолчанию у поля класса. При инициализации объекта можно не указывать поле, но оно будет доступно для вычислений.
equals()
При определении класса данных функция equals() (и оператор ==) по-прежнему возвращает true, если ссылки указывают на один объект. Но она также возвращает true, если объекты имеют одинаковые значения свойств, определённых в конструкторе:
Если вы переопределяете функцию equals(), также необходимо переопределить функцию hashCode().
Кстати, если вам нужно проверить, что две переменные ссылаются на один объект, то используйте оператор ===. В отличие от оператора ==, поведение оператора === не зависит от функции equals(), которое в разных классах может вести себя по разному. Оператор === всегда ведёт себя одинаково независимо от разновидности класса.
hashCode()
Если два объекта данных считаются равными (имеют одинаковые значения свойств), функция hashCode() возвращает для этих объектов одно и то же значение:
Если вам потребуется создать копию объекта данных, изменяя некоторые из его свойств, но оставить другие свойства в исходном состоянии, воспользуйтесь функцией copy(). Для этого функция вызывается для того объекта, который нужно скопировать, и ей передаются имена всех изменяемых свойств с новыми значениями.
Фактически мы создаём копию объекта, меняем значение нужного свойства и присваиваем новый объект переменной с новым именем. При этом исходный объект остаётся без изменений.
Деструктурирующее присваивание
Мы могли бы обратиться и привычным способом.
Деструктуризация позволяет разбить объект на несколько переменных.
Можно пропустить через цикл.
Можно пропустить какую-то переменную через символ подчёркивания.
Несколько конструкторов
Добавить второй конструктор к классу можно через ключевое слово constructor.
Класс данных – data class
Нередко в программах требуются объекты, предназначенные во многом для хранения данных. Например, для книг надо описывать их автора, название, год издания и т. д. В более старых языках программирования, таких как Паскаль и Си, для подобных целей существует такой тип данных как «запись». В более современных языках обычно для этих целей используют обычные классы, в которых методов может и не быть. Вот как мог бы выглядеть подобный класс и его объекты на языке Kotlin:
С объектами таких классов данных часто выполняются стандартные действия. Например, вывод значений свойств объекта, создание другого объекта с почти такими же значениями свойств, как у существующего. Поэтому Kotlin идет дальше и вводит в язык особый вариант класса – класс данных. Его объявление начинается со слова data.
Разница между этим вариантом и предыдущем в том, что к таким классам компилятор добавляет методы toString(), equals() и hashCode(), которые переопределяют эти методы, наследуемые по умолчанию всеми классами от Any. В результате в дата-классах эти функции-члены работают по-другому, они адаптированы под задачи, которые выполняют дата-классы. Также компилятор добавляет несколько других функций-членов, например, copy().
Метод toString() data-класса создает строку, содержащую перечень свойств и их значений.
Не забываем, что функция println() сама вызывает toString(). Конечно, мы можем переопределить метод в дата-классе, если нам не нравится его реализация по-умолчанию.
При этом мы переопределяем не тот toString(), который будет добавлен компилятором в связи с модификатором data. Мы переопределяем toString() класса Any. Поэтому если реализация метода toString() будет выглядеть как ниже, то это возврат к тому, что делает Any, несмотря на то, что класс data.
Функция-член equals() (перегружает оператор ==), которую добавляет компилятор к data-классам, сравнивает поля и на этом основании выносит суждение о том, равны ли объекты.
Если бы класс Book был объявлен без модификатора data, то результат обоих сравнений был бы false, потому что переменные a и c указывают на разные объекты. То есть сравнивались бы ссылки на объекты, а не значения полей объекта.
Функция copy() дата-класса, позволяет не просто создавать копию объекта, также на ходу изменять данные при необходимости:
Мультидекларация – это «распаковка» объекта таким образом, что значения его свойств присваиваются сразу нескольким переменным. В случае data-класса это выглядит так:
Чтобы подобное было возможно, компилятор добавляет в дата-класс функции, перегружающие операцию мультидекларации. Обычные классы по-умолчанию не поддерживают такую распаковку, но программист может добавить эту возможность в любой класс. Так бы выглядел обычный класс, но с поддержкой мультидекларации:
Операция мультидекларации также часто используется в цикле for. Если имеется список книг, можно легко пройтись по их свойствам:
Все вышеперечисленные функции по-умолчанию обрабатывают только свойства перечисленные в первичном конструкторе. Однако класс данных может содержать и другие.
В данном случае поле pages будет игнорироваться как при строковом представлении объекта, сравнении объектов, копировании, так и в мультидекларации.
В Котлин есть встроенные дата-классы – Triple и Pair, предназначенные для создания объектов с тремя или двумя свойствами. Тип свойств может быть любым.
Объекты класса Pair нередко используются при обработке коллекций в цикле for.
Сравнение Java-записей, Lombok @Data и Kotlin data-классов
Несмотря на то что все три решения позволяют бороться с бойлерплейт кодом, общего между ними довольно мало. У записей более сильная семантика, из которой вытекают их важные преимущества. Что часто делает их лучшим выбором, хотя и не всегда.
… в одну строчку кода:
Конечно, аннотации @Data и @Value из Lombok обеспечивают аналогичную функциональность с давних пор, хоть и с чуть большим количеством строк:
А если вы знакомы с Kotlin, то знаете, что то же самое можно получить, используя data-класс:
Получается, что это одно и то же? Нет. Уменьшение бойлерплейт кода не является целью записей, это следствие их семантики.
К сожалению, этот момент часто упускается. Об уменьшении бойлерплейт кода говорят много, так как это очевидно и легко демонстрируется, но семантика и вытекающие из нее преимущества остаются незамеченными. Официальная документация не помогает — в ней тоже все описывается под углом бойлерплейта. И хотя JEP 395 лучше объясняет семантику, но из-за своего объема все довольно расплывчато, когда дело доходит до описания преимуществ записей. Поэтому я решил описать их в этой статье.
Семантика записей (records)
В JEP 395 говорится:
Записи (records) — это классы, которые действуют как прозрачные носители неизменяемых данных.
Таким образом, создавая запись, вы говорите компилятору, своим коллегам, всему миру, что указанный тип хранит данные. А точнее, иммутабельные (поверхностно) данные с прозрачным доступом. Это основная семантика — все остальное вытекает из нее.
Если такая семантика не применима к нужному вам типу, то не используйте записи. А если вы все равно будете их использовать (возможно, соблазнившись отсутствием бойлерплейта или потому что вы думаете, что записи эквивалентны @Data / @Value и data-классам), то только испортите свою архитектуру, и велики шансы, что это обернется против вас. Так что лучше так не делать.
(Извините за резкость, но я должен был это сказать.)
Прозрачность и ограничения
Давайте подробнее поговорим о прозрачности (transparency). По этому поводу у записей есть даже девиз (перефразированный из Project Amber):
API записей моделирует состояние, только состояние и ничего, кроме состояния.
Для реализации этого необходимы ряд ограничений:
для всех компонент должны быть аксессоры (методы доступа) с именем, совпадающим с именем компонента, и возвращающие такой же тип, как у компонента (иначе API не будет моделировать состояние)
должен быть конструктор с параметрами, которые соответствуют компонентам записи (так называемый канонический конструктор; иначе API не будет моделировать состояние)
не должно быть никаких дополнительных полей (иначе API не будет моделировать состояние)
не должно быть наследования классов (иначе API не будет моделировать состояние, так как некоторые данные могут находиться в другом месте за пределами записи)
И Lombok и data-классы Kotlin позволяют создавать дополнительные поля, а также приватные «компоненты» (в терминах записей Java, а Kotlin называет их параметрами первичного конструктора). Так почему же Java относится к этому так строго? Чтобы ответить на этот вопрос, нам понадобится вспомнить немного математики.
Математика
Итак, как вы поняли, тип — это множество, значения которого допустимы для данного типа. Это также означает, что теория множеств — «раздел математики, в котором изучаются общие свойства множеств» (как говорит Википедия), — связана с теорией типов — «академическим изучением систем типов» (аналогично), — на которую опирается проектирование языков программирования.
Это здорово, потому что теория множеств может многое сказать о применении функций к произведениям. Одним из аспектов этого является то, как функции, работающие с одним операндом, могут комбинироваться с функциями, работающими с несколькими операндами, и какие свойства функций (инъективные, биективные и т. д.) остаются нетронутыми.
В общем случае, чтобы применить теорию множеств к типу так, как я упоминал выше, ко всем его операндам должен быть доступ и должен существовать способ превратить кортеж операндов в экземпляр. Если верно и то и другое, то теория типов называет такой тип «тип-произведение» (а его экземпляры кортежами), и с ними можно делать несколько интересных вещей.
На самом деле записи лучше кортежей. В JEP 395 говорится:
Записи можно рассматривать как номинативные кортежи.
Следствия
Я хочу донести до вас следующую мысль: записи стремяться стать типом-произведением и, чтобы это работало, все их компоненты должны быть доступны. То есть не может быть скрытого состояния, и должен быть конструктор, принимающий все компоненты. Именно поэтому записи являются прозрачными носителями неизменяемых данных.
Итак, если подытожить:
Аксессоры (методы доступа) генерируются компилятором.
Мы не можем изменять их имена или возвращаемый тип.
Мы должны быть очень осторожны с их переопределением.
Компилятор генерирует канонический конструктор.
Преимущества записей
Большинство преимуществ, которые мы получаем от алгебраической структуры, связаны с тем, что аксессоры вместе с каноническим конструктором позволяют разбирать и пересоздавать экземпляры записей структурированным образом без потери информации.
Деструктурирующие паттерны
Благодаря полной прозрачности записей мы можем быть уверены, что не пропустим скрытое состояние. Это означает, что разница между range и возвращаемым экземпляром — это именно то, что вы видите: low и high меняются местами — не более того.
Блок with
И, как и раньше, мы можем рассчитывать на то, что newRange будет точно таким же, как и range за исключением low : нет скрытого состояния, которое мы не перенесли. И синтаксически здесь все просто:
выполнить блок with
передать переменные в канонический конструктор
(Обратите внимание, что этот функционал далек от реальности и может быть не реализован или быть значительно изменен.)
Сериализация
Для представления объекта в виде потока байт, JSON / XML-документа или в виде любого другого внешнего представления и обратной конвертации, требуется механизм разбивки объекта на его значения, а затем сборки этих значений снова вместе. И вы сразу же можете увидеть, как это просто и хорошо работает с записями. Они не только раскрывают все свое состояние и предлагают канонический конструктор, но и делают это структурированным образом, что делает использование Reflection API очень простым.
Более подробно том, как записи изменили сериализацию, слушайте в подкасте Inside Java Podcast, episode 14 (также в Spotify). Если вы предпочитаете короткие тексты, то читайте твит.
Бойлерплейт код
Вернемся на секунду к бойлерплейту. Как говорилось ранее, чтобы запись была типом-произведением, должны выполняться следующие условия:
аксессоры (методы доступа)
И все это генерируется компилятором (а также еще toString ) не столько для того, чтобы избавить нас от написания этого кода, сколько потому, что это естественное следствие алгебраической структуры.
Недостатки записей
Так что же делать, если вам все это нужно? Тогда записи вам не подходят и вместо них следует использовать обычный класс. Даже если изменив только 10% функциональности, вы получите 90% бойлерплейта, от которого вы бы избавились с помощью записей.
Преимущества Lombok @Data/@Value
Lombok просто генерирует код. У него нет семантики, поэтому у вас есть полная свобода в изменении класса. Конечно, вы не получите преимуществ более строгих гарантий, хотя в будущем Lombok, возможно, сможет генерировать деструктурные методы.
(При этом я не рекламирую Lombok. Он в значительной степени полагается на внутренние API компилятора, которые могут измениться в любой момент, а это означает, что проекты, использующие его, могут сломаться при любом незначительном обновлении Java. То, что он много делает для скрытия технического долга от своих пользователей, тоже не очень хорошо.)
Преимущества data-классов Kotlin
Вы часто создаете классы, основной целью которых является хранение данных. Обычно в таких классах некоторый стандартный и дополнительный функционал можно автоматически получить из данных.
Некоторые указывали на @JvmRecord в Kotlin как на большую ошибку: «Видите, data-классы могут быть записями — шах и мат ответ» (я перефразировал, но смысл был такой). Если у вас возникли такие же мысли, то я прошу вас остановиться и подумать на секунду. Что именно это дает вам?
Data-класс должен соблюдать все правила записи, а это значит, что он не может делать больше, чем запись. Но Kotlin все еще не понимает концепции прозрачных кортежей и не может сделать с @JvmRecord data-классом больше, чем с обычным data-классом. Таким образом, у вас есть свобода записей и гарантии data-классов данных — худшее из обоих миров.
В Kotlin нет большого смысла использовать JVM-записи, за исключением двух случаев:
перенос существующей Java-записи на Kotlin с сохранением ее ABI;
генерация атрибута класса записи с информацией о компоненте записи для класса Kotlin для последующего чтения каким-либо фреймворком, использующим Java reflection для анализа записей.
Рефлексия
Записи не лучше и не хуже рассмотренных альтернатив или других вариантов с аналогичным подходом, таких как case-классы Scala. У них действительно сильная семантика с твердым математическим фундаментом, которая хотя и ограничивает возможности по проектированию классов, но приносит мощные возможности, которые, в противном, случае были бы невозможны или, по крайней мере, не столь надежны.
Это компромисс между свободой разработчика и мощью языка. И я доволен этим компромиссом и с нетерпением жду, когда он полностью раскроет свой потенциал в будущем.
В преддверии старта курса «Java Developer. Professional» приглашаю всех желающих на бесплатный демоурок по теме: «Система получения курсов валют ЦБ РФ».
Kotlin Data Class with examples
By Chaitanya Singh | Filed Under: Kotlin Tutorial
In Kotlin, you can create a data class to hold the data. The reason why would you want to mark a class as data is to let compiler know that you are creating this class for holding the data, compiler then creates several functions automatically for your data class which would be helpful in managing data. In this guide, we will learn data class and the functions that are automatically generated by compiler.
A data class Student:
Automatically generated functions for data class in Kotlin
For now I am just mentioning the name of the functions here, we will see each one of them with the help of examples.
1. equals()
2. hashCode()
3. toString()
4. copy()
5. componentN()
Kotlin Data Class Requirements
In order to mark a class as data, the class must fulfil certain requirements. The requirements are as follows:
1. The primary constructor of the data class must have at least one parameter. Also, the parameters are either marked val or var.
2. The class cannot be marked as open, abstract, sealed or inner.
3. The class can extend (inherit) other class and it can also implements other interfaces.
Kotlin Data Class Example
Output:
Data class hashCode() and equals() methods
If two objects are equal in kotlin then they have the same hash code which we can get using the hashCode() method.
The method equals() returns true or false. If the hashCode() of two objects are equal then equals() returns true else it returns false.
Output:
Data class copy() method
By using copy() method in data class, we can copy few of the properties of other objects. Lets take an example. Here we have an object stu of Student class that contains the name, age and subject details of a Student “Steve” and we have created another object stu2 with the name “Lucy” and copying the age and subject details of object stu using the copy() method of data class.
Output:
Data class toString() method
The toString() method of data class returns the String representation of an object.
Output:
Data class componentN() method
The componentN() method of data class destructure an object into a number of variables. In the following example we have an object stu of Student class and we are destructuring the object into number of variables using the componentN() method. The component1() method returns the value of the first property of the object, component2() returns the value of second property and so on.
Output: