Я уже рассказывал вам про классы и ООП, начиная с 11 урока. И вы понимаете насколько это мощный инструмент, позволяющий писать код приближенный к реальности.
Классические классы используются для организации кода, реализации ООП подходов. Создатели языка Kotlin сделали свой тип классов, специально для хранения данных – Data классы. Строго говоря, это обычные классы у которых есть дополнительные возможности. Со временем вы полностью почувствуете разницу, пока лишь сосредоточусь на основных отличиях, которые легко понять и запомнить.
Что значит для хранения данных? Все то, что мы делали раньше, когда создавали и описывали сущности телефонных справочников, космических кораблей, словарей для переводчиков и так далее.
Какими дополнительными возможностями обладают Data классы в сравнении с обычными? Если совсем коротко, то для объектов этих классов можно вызвать новые методы. Такие как toString(), equals(), hashCode() и copy().
- equals() – производит сравнение двух объектов;
- hashCode() – позволяет получить хэш-код объекта (уникальный целочисленный номер объекта);
- toString() – возвращает представление объекта в виде понятной строки (именно понятной, об этом будет подробнее ниже);
- copy() – копирует данные объекта в другой объект.
Начнем с метода toString(). И понятнее всего будет показать его ценность, сперва обозначив проблему. При разработке телеграм бота для изучения иностранных слов мы выделили сущность, которая должна хранить слово на одном языке и его перевод. Создадим классический класс Word с полями text и translate. Тут же создадим объект “слова” и попытаемся распечатать его в консоль.
class Word(
val text: String,
val translate: String,
)
fun main(){
val word = Word("Red", "Красный")
println(word)
}
Получаем непонятный набор символов additional.Word@452b3a41. Так выглядит строковое представление объекта по умолчанию. Но мы хотим увидеть в строке понятные данные объекта. Как решить эту проблему?
Вариант 1. Используя классический класс. Может быть к объекту добавить принудительное приведение к строке? Нет, *println*(word.toString())
не сработает. Так как метод println() под капотом использует тот же toString() и результат будет абсолютно таким же. Окей. Но можно переопределить стандартный toString(), добавив в него нужное нам поведение.
Для этого в теле класса сочетанием клавиш cmd+N сгенерируем toString(). В следующем окне IDEA предложит выбрать поля объекта, для которых будет создано новое текстовое представление. Оставляем оба.
Получаем переопределенную функцию toString(). По кружочку со стрелкой можно переместиться к исходной функции, которую переопределили. Теперь метод возвращает красивую строку с полями объекта. Запускаем и видим в консоли ожидаемый результат Word(text=’Red’, translate=’Красный’).
class Word(
val text: String,
val translate: String,
) {
override fun toString(): String {
return "Word(text='$text', translate='$translate')"
}
}
Вариант решения проблемы 2. Добавить ключевое слово data к классу и просто вызвать печать объекта. Тело класса уже не нужно. Запускаем и получаем точно такой же результат.
fun main(){
val word = Word("Red", "Красный")
println(word)
}
data class Word(
val text: String,
val translate: String,
)fun main(){
val word = Word("Red", "Красный")
println(word)
}
data class Word(
val text: String,
val translate: String,
)
При необходимости здесь тоже можно переопределять toString(). Если вдруг нас не будет устраивать реализация по умолчанию.
Далее перейдем к методу equals(). Эта функция занимается сравнением и аналогична оператору “двойное равно” (==) и по аналогии с тустринг ее можно сгенерировать с помощью среды разработки. Можете самостоятельно посмотреть ее реализацию.
Добавим несколько объектов для сравнения. Если попробовать сравнить пару через equals(), среда разработки предложит заменить функцию на оператор. Так и сделаем.
fun main(){
val word1 = Word("Red", "Красный")
val word2 = Word("Red", "Красный")
val word3 = Word("White", "Белый")
println(word1 == word2)
println(word2 == word3)
}
При сравнении обычных классов оба вывода будет false. Потому, что сравниваются ссылки на объекты в памяти, а не значения. Но Data класс позволяет сравнить именно проинициализированные данные. Поэтому видим закономерный результат true и false.
И еще, помните про оператор ссылочного сравнения? Тройное равно (===). Он никак не относится к equals() и ведет себя одинаково в не зависимости от типа класса.
Еще одна интересная функция из арсенала Data классов – copy(). Во-первых – она умеет копировать объекты целиком.
fun main(){
val word1 = Word("Red", "Красный")
val word2 = Word("Red", "Красный")
val word3 = Word("White", "Белый")
// println(word1 == word2)
// println(word2 == word3)
val word4 = word3.copy()
println(word4)
}
Во-вторых, и эта возможность довольно часто используется, метод умеет копировать объекты с выборочным изменением полей.
fun main(){
val word1 = Word("Red", "Красный")
val word2 = Word("Red", "Красный")
val word3 = Word("White", "Белый")
// println(word1 == word2)
// println(word2 == word3)
val word4 = word3.copy(translate = "Правильный перевод: Белый")
println(word3)
println(word4)
}
Это может применяться, например, в мобильной разработке. Когда есть экран, для его полей есть какой-то Data класс. И в определенный момент в состоянии экрана меняется только одно конкретное поле. И чтобы отрисовать это новое измененное состояние, с сохранением остальных полей, применяется функция копирования объекта. Повторюсь, метод copy() доступен только для Data классов.
Наконец, рассмотрим hashCode(). Этот метод есть в обоих разновидностях класса, но в Data классе он переопределен и имеет другую реализацию. Метод возвращает уникальный код объекта. Он также может использоваться для сравнения, как и equals(), но работает быстрее. Потому, что два числа сравнивать быстрее, чем все свойства и значения объекта. Если два объекта Data класса имеют одинаковые свойства, то и хэш-код у них будет одинаковый.
println(word1.hashCode())
println(word2.hashCode())
У одинаковых data-классов одинаковый хэшкод, потому что для них генерируется hashcode() функция основанная на полях внутри классов.
public int hashCode() {
String var10000 = this.original;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.translate;
return var1 + (var10001 != null ? var10001.hashCode() : 0);
}
Обычные же, не дата классы использую hashcode() базового класса Object, реализация которого игнорирует поля класса, и для упрощения можно сказать, что hashcode генерируется случайным образом для каждого объекта (зависит от реализации JVM).
public class Object {
//...
@IntrinsicCandidate
public native int hashCode();
//...
}
Подробнее мы не будем закапываться на этом уровне изучения языка. Если вам интересен более глубокий разбор исходников и поиск причинно-следственных связей – ссылку на материал оставлю в описании.
И это конец. Data классы, их возможности и другие продвинутые темы (которые не освещены на канале) мы используем в курсовой работе по написанию своего телеграм бота на Kotlin.
Полезные инструменты
Для тех, кто собрался стать Android-разработчиком

Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика

Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin

Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика