Классы данных

Нередко мы создаём классы, единственным назначением которых является хранение данных. Функционал таких классов зависит от самих данных, которые в них хранятся. В Kotlin класс может быть отмечен словом data:

data class User(val name: String, val age: Int)

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

  • пару функций equals()/hashCode(),
  • функцию toString() в форме "User(name=John, age=42)",
  • компонентные функции componentN(), которые соответствуют свойствам, в соответствии с порядком их объявления,
  • функцию copy() (см. ниже)

Если какая-либо из этих функций явно определена в теле класса (или унаследована от родительского класса), то генерироваться она не будет.

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

  • Основной конструктор должен иметь как минимум один параметр;
  • Все параметры основного конструктора должны быть отмечены, как val или var;
  • Классы данных не могут быть абстрактными, open, sealed или inner;
  • (до версии 1.1) Классы данных не могут наследоваться от других классов (но могут реализовывать интерфейсы).

Дополнительно, генерация членов классов данных при наследовании подчиняется следующим правилам:

  • Если существуют явные реализации equals(), hashCode() или toString() в теле класса данных или конечные (final) реализации в суперклассе, то эти функции не генерируются, а используются существующие реализации;
  • Если суперкласс включает функции componentN(), которые являются открытыми и возвращают совместимые типы, соответствующие компонентные функции создаются для класса данных и переопределяют функции суперкласса. Если функции суперкласса не могут быть переопределены из-за несовместимости сигнатур или являются конечными (final), выдаётся сообщение об ошибке;
  • Наследование класса данных от типа, который уже имеет функцию copy(...) с совпадающей сигнатурой не рекомендуется в Kotlin 1.2 и запрещена в Kotlin 1.3;
  • Предоставление явных реализаций для функций componentN() и copy() не допускается.

Начиная с версии 1.1, классы данных могут расширять другие классы (см. примеры в статье Изолированные классы)

Для того, чтобы у сгенерированного в JVM класса был конструктор без параметров, значения всех свойств должны быть заданы по умолчанию (см. Конструкторы)

 data class User(val name: String = "", val age: Int = 0)
 ```

Свойства, объявленные в теле класса

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

data class Person(val name: String) {
    var age: Int = 0
}

Только свойство name будет учитываться в реализациях функций toString(), equals(), hashCode() и copy(), и будет создана только одна компонентная функция component1(). Даже если два объекта класса Person будут иметь разные значения свойств age, они будут считаться равными.

    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
    println("${person1 == person2}") // выведет "true"

Копирование

Довольно часто нам приходится копировать объект с изменением только некоторых его свойств. Для этой задачи генерируется функция copy(). Для написанного выше класса User такая реализация будет выглядеть следующим образом:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

Это позволяет нам писать:

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

Классы данных и мульти-декларации

Сгенерированные для классов данных компонентные функции позволяют использовать их в мульти-декларациях:

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // выводит "Jane, 35 years of age"

Стандартные классы данных

Стандартная библиотека предоставляет классы Pair и Triple. Однако, в большинстве случаев, проименованные классы данных являются лучшим решением, потому что делают код более читаемым, избегая малосодержательные имена для свойств.

Статья на эту тему на Хабре