Урок 22. Data class (дата классы) copy, toString, equals, hashCode в Kotlin

Я уже рассказывал вам про классы и ООП, начиная с 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

Обучающийбот
Обучающий
бот

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

Поделиться уроком

Ответить

Ваш адрес email не будет опубликован.