Как стать автором
Обновить

Книга «Head First. Kotlin»

Время на прочтение9 мин
Количество просмотров14K
imageПривет, Хаброжители! У нас вышла книга для изучения Kotlin по методике Head First, выходящей за рамки синтаксиса и инструкций по решению конкретных задач. Эта книга даст вам все необходимое — от азов языка до продвинутых методов. А еще вы сможете попрактиковаться в объектно-ориентированном и функциональном программировании.

Под катом представлен отрывок «Классы данных»

Работа с данными


Никому не хочется тратить время и заново делать то, что уже было сделано. В большинстве приложений используются классы, предназначенные для хранения данных. Чтобы упростить работу, создатели Kotlin предложили концепцию класса данных. В этой главе вы узнаете, как классы данных помогают писать более элегантный и лаконичный код, о котором раньше можно было только мечтать. Мы рассмотрим вспомогательные функции классов данных и узнаем, как разложить объект данных на компоненты. Заодно расскажем, как значения параметров по умолчанию делают код более гибким, а также познакомим вас с Any — предком всех суперклассов.

Оператор == вызывает функцию с именем equals


Как вы уже знаете, оператор == может использоваться для проверки равенства. Каждый раз, когда выполняется оператор ==, вызывается функция с именем equals. Каждый объект содержит функцию equals, а реализация этой функции определяет поведение оператора ==.

По умолчанию функция equals для проверки равенства проверяет, содержат ли две переменные ссылки на один и тот же объект.

Чтобы понять принцип работы, представьте две переменные Wolf с именами w1 и w2. Если w1 и w2 содержат ссылки на один объект Wolf, при сравнении их оператором == будет получен результат true:

image

Но если w1 и w2 содержат ссылки на разные объекты Wolf, сравнение их оператором == дает результат false, даже если объекты содержат одинаковые значения свойств.

image

Как говорилось ранее, в каждый объект, который вы создаете, автоматически включается функция equals. Но откуда берется эта функция?

equals наследуется от суперкласса Any


Каждый объект содержит функцию с именем equals, потому что его класс наследует функцию от класса с именем Any. Класс Any является предком всех классов: итоговым суперклассом всего. Каждый класс, который вы определяете, является подклассом Any, и вам не нужно указывать на это в программе. Таким образом, если вы напишете код класса с именем myClass, который выглядит так:

class MyClass {
    ...
}

Компилятор автоматически преобразует его к следующему виду:
image

Каждый класс является подклассом класса Any и наследует его поведение. Каждый класс ЯВЛЯЕТСЯ подклассом Any, и вам не придется сообщать об этом в программе.

Важность наследования от Any


Включение Any как итогового суперкласса обладает двумя важными преимуществами:

  • Оно гарантирует, что каждый класс наследует общее поведение. Класс Any определяет важное поведение, от которого зависит работа системы. А поскольку каждый класс является подклассом Any, это поведение наследуется всеми объектами, которые вы создаете. Так, класс Any определяет функцию с именем equals, а следовательно, эта функция автоматически наследуется всеми объектами.
  • Оно означает, что полиморфизм может использоваться с любыми объектами. Каждый класс является подклассом Any, поэтому у любого объекта, который вы создаете, класс Any является его итоговым супертипом. Это означает, что вы можете создать функцию с параметрами Any или возвращаемый тип Any, который будет работать с объектами любых типов. Также это означает, что вы можете создавать полиморфные массивы для хранения объектов любого типа, кодом следующего вида:

val myArray = arrayOf(Car(), Guitar(), Giraffe())

Компилятор замечает, что каждый объект в массиве имеет общий прототип Any, и поэтому создает массив типа Array.

Общее поведение, наследуемое классом Any, стоит рассмотреть поближе.

Общее поведение, наследуемое от Any


Класс Any определяет несколько функций, наследуемых каждым классом. Вот примеры основных функций и их поведения:

  • equals(any: Any): Boolean
    Проверяет, считаются ли два объекта «равными». По умолчанию функция возвращает true, если используется для проверки одного объекта, или false — для разных объектов. За кулисами функция equals вызывается каждый раз, когда оператор == используется в программе.

val w1 = Wolf()                                 val w1 = Wolf()
val w2 = Wolf()                                 val w2 = w1
println(w1.equals(w2))                          println(w1.equals(w2))

false (equals возвращает false,                 true (equals возвращает true,
потому что w1 и w2                              потому что w1 и w2
содержат ссылки                                 содержат ссылки на
на разные объекты.)                             один и тот же объект — то же самое,
                                                что проверка w1 == w2.

  • hashCode(): Int
    Возвращает хеш-код для объекта. Хеш-коды часто используются некоторыми структурами данных для эффективного хранения и выборки значений.

val w = Wolf()
println(w.hashCode())

523429237 (Значение хеш-кода w)

  • toString(): String
    Возвращает сообщение String, представляющее объект. По умолчанию сообщение содержит имя класса и число, которое нас обычно не интересует.

val w = Wolf()
println(w.toString())

Wolf@1f32e575

По умолчанию функция equals проверяет, являются ли два объекта одним фактическим объектом.

Функция equals определяет поведение оператора ==.

Класс Any предоставляет реализацию по умолчанию для всех перечисленных функций, и эти реализации наследуются всеми классами. Однако вы можете переопределить эти реализации, чтобы изменить поведение по умолчанию всех перечисленных функций.

Простая проверка эквивалентности двух объектов


В некоторых ситуациях требуется изменить реализацию функции equals, чтобы изменить поведение оператора ==.

Допустим, что у вас имеется класс Recipe, который позволяет создавать объекты для хранения кулинарных рецептов. В такой ситуации вы, скорее всего, будете считать два объекта Recipe равными (или эквивалентными), если они содержат описание одного рецепта. Допустим, класс Recipe определяется с двумя свойствами — title и isVegetarian:

class Recipe(val title: String, val isVegetarian: Boolean) {
}

Оператор == будет возвращать результат true, если он используется для сравнения двух объектов Recipe с одинаковыми свойствами, title и isVegetarian:

val r1 = Recipe("Chicken Bhuna", false)
val r2 = Recipe("Chicken Bhuna", false)

image

Хотя вы можете изменить поведение оператора ==, написав дополнительный код для переопределения функции equals, разработчики Kotlin предусмотрели более удобное решение: они создали концепцию класса данных. Посмотрим, что собой представляют эти классы и как они создаются.

Класс данных позволяет создавать объекты данных


Классом данных называется класс для создания объектов, предназначенных для хранения данных. Он включает средства, полезные при работе с данными, — например, новую реализацию функции equals, которая проверяет, содержат ли два объекта данных одинаковые значения свойств. Если два объекта содержат одинаковые данные, то они могут считаться равными.

Чтобы определить класс данных, поставьте перед обычным определением класса ключевое слово data. Следующий код преобразует класс Recipe, созданный ранее, в класс данных:

data class Recipe(val title: String, val isVegetarian: Boolean) {
}

Префикс data преобразует обычный класс в класс данных.

Как создаются объекты на основе класса данных


Объекты классов данных создаются так же, как и объекты обычных классов: вызовом конструктора этого класса. Например, следующий код создает новый объект данных Recipe и присваивает его новой переменной с именем r1:

val r1 = Recipe("Chicken Bhuna", false)

Классы данных автоматически переопределяют свои функции equals для изменения поведения оператора ==, чтобы равенство объектов проверялось на основании значений свойств каждого объекта. Если, например, вы создадите два объекта Recipe с одинаковыми значениями свойств, сравнение двух объектов оператором == даст результат true, потому что в них хранятся одинаковые данные:

val r1 = Recipe("Chicken Bhuna", false)
val r2 = Recipe("Chicken Bhuna", false)
//r1 == r2 равно true

r1 и r2 считаются «равными», потому что два объекта Recipe содержат одинаковые данные.

Кроме новой реализации функции equals, унаследованной от суперкласса Any, классы данных
также переопределяют функции hashCode и toString. Посмотрим, как реализуются эти функции.

Объекты классов переопределяют свое унаследованное поведение


Для работы с данными классу данных необходимы объекты, поэтому он автоматически предоставляет следующие реализации для функций equals, hashCode и toString, унаследованных от суперкласса Any:

Функция equals сравнивает значения свойств


При определении класса данных его функция equals (а следовательно, и оператор == ) по-прежнему возвращает true, если ссылки указывают на один объект. Но она также возвращает true, если объекты имеют одинаковые значения свойств, определенных в конструкторе:

val r1 = Recipe("Chicken Bhuna", false)
val r2 = Recipe("Chicken Bhuna", false)
println(r1.equals(r2))
       true

Объекты данных считаются равными, если их свойства содержат одинаковые значения.

Для равных объектов возвращаются одинаковые значения hashCode


Если два объекта данных считаются равными (другими словами, они имеют одинаковые значения свойств), функция hashCode возвращает для этих объектов одно и то же значение:

val r1 = Recipe("Chicken Bhuna", false)
val r2 = Recipe("Chicken Bhuna", false)
println(r1.hashCode())
println(r2.hashCode())

241131113
241131113


toString возвращает значения всех свойств


Наконец, функция toString уже не возвращает имя класса, за которым следует число, а возвращает полезную строку со значениями всех свойств, определенными в конструкторе класса данных:

val r1 = Recipe("Chicken Bhuna", false)
println(r1.toString())
       Recipe(title=Chicken Bhuna, isVegetarian=false)

Кроме переопределения функций, унаследованных от суперкласса Any, класс данных также предоставляет дополнительные средства, которые обеспечивают более эффективную работу с данными, например, возможность копирования объектов данных. Посмотрим, как работают эти средства.

Копирование объектов данных функцией copy


Если вам потребуется создать копию объекта данных, изменяя некоторые из его свойств, но оставить другие свойства в исходном состоянии, воспользуйтесь функцией copy. Для этого функция вызывается для того объекта, который нужно скопировать, и ей передаются имена всех изменяемых свойств с новыми значениями.

Предположим, имеется объект Recipe с именем r1, который определяется в коде примерно так:

val r1 = Recipe("Thai Curry", false)

image

Если вы хотите создать копию объекта Recipe, заменяя значение свойства isVegetarian на true, это делается так:

image

По сути это означает «создать копию объекта r1, изменить значение его свойства isVegetarian на true и присвоить новый объект переменной с именем r2». При этом создается новая копия объекта, а исходный объект остается без изменений.

Кроме функции copy, классы данных также предоставляют набор функций для разбиения объекта данных на набор значений его свойств — этот процесс называется деструктуризацией. Посмотрим, как это делается.

Классы данных определяют функции componentN...


При определении класса данных компилятор автоматически добавляет в класс набор функций, которые могут использоваться как альтернативный механизм обращения к значениям свойств объекта. Эти функции известны под общим названием функций componentN, где N — количество извлекаемых свойств (в порядке объявления).

Чтобы увидеть, как работают функции componentN, предположим, что имеется следующий объект Recipe:

val r = Recipe("Chicken Bhuna", false)

Если вы хотите получить значение первого свойства объекта (свойство title), для этого можно вызвать функцию component1() объекта:

val title = r.component1()

component1() возвращает ссылку, которая содержится в первом свойстве, определенном в конструкторе класса данных.

Функция делает то же самое, что следующий код:

val title = r.title

Код с функцией получается более универсальным. Чем же именно функции ComponentN так полезны в классах данных?

… предназначенные для деструктуризации объектов данных


Обобщенные функции componentN полезны прежде всего тем, что они предоставляют простой и удобный способ разбиения объекта данных на значения свойств, или его деструктуризации.

Предположим, что вы хотите взять значения свойств объекта Recipe и присвоить значение каждого его свойства отдельной переменной. Вместо кода

val title = r.title
val vegetarian = r.isVegetarian

с поочередной обработкой каждого свойства можно использовать следующий код:

val (title, vegetarian) = r

Присваивает первому свойству r значение title, а второму свойству значение vegetarian.

Этот код означает «создать две переменные, title и vegetarian, и присвоить значение одного из свойств r каждой переменной». Он делает то же самое, что и следующий фрагмент

val title = r.component1()
val vegetarian = r.component2()

но получается более компактным.

Оператор === всегда проверяет, ссылаются ли две переменные на один объект.

Если вы хотите проверить, ссылаются ли две переменные на один объект независимо от их типа, используйте оператор === вместо ==. Оператор === дает результат true тогда (и только тогда), когда две переменные содержат ссылку на один фактический объект. Если у вас имеются две переменные, x и y, и следующее выражение:

x === y

дает результат true, то вы знаете, что переменные x и y должны ссылаться на один объект.

В отличие от оператора ==, поведение оператора === не зависит от функции equals. Оператор === всегда ведет себя одинаково независимо от разновидности класса.

Теперь, когда вы узнали, как создавать и использовать классы данных, создадим проект для кода Recipe.

Создание проекта Recipes


Создайте новый проект Kotlin для JVM и присвойте ему имя «Recipes». Затем создайте новый
файл Kotlin с именем Recipes.kt: выделите папку src, откройте меню File и выберите команду
New → Kotlin File/Class. Введите имя файла «Recipes» и выберите вариант File в группе Kind.

Мы добавим в проект новый класс данных с именем Recipe и создадим объекты данных Recipe. Ниже приведен код. Обновите свою версию Recipes.kt и приведите ее в соответствие с нашей:

data class Recipe(val title: String, val isVegetarian: Boolean) (Фигурные скобки {} опущены, так как наш класс данных не имеет тела.)

fun main(args: Array<String>) {
      val r1 = Recipe("Thai Curry", false)
      val r2 = Recipe("Thai Curry", false)
      val r3 = r1.copy(title = "Chicken Bhuna") (Создание копии r1 с изменением свойства title)
      println("r1 hash code: ${r1.hashCode()}")
      println("r2 hash code: ${r2.hashCode()}")
      println("r3 hash code: ${r3.hashCode()}")
      println("r1 toString: ${r1.toString()}")
      println("r1 == r2? ${r1 == r2}")
      println("r1 === r2? ${r1 === r2}")
      println("r1 == r3? ${r1 == r3}")
      val (title, vegetarian) = r1 (Деструктуризация r1)
      println("title is $title and vegetarian is $vegetarian")
}

Когда вы запустите свой код, в окне вывода IDE отобразится следующий текст:

r1 hash code: -135497891
r2 hash code: -135497891
r3 hash code: 241131113
r1 toString: Recipe(title=Thai Curry, isVegetarian=false)
r1 == r2? true 
r1 === r2? false
r1 == r3? false
title is Thai Curry and vegetarian is false


» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — Kotlin

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Теги:
Хабы:
+13
Комментарии6

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия