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

В 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.

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

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

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

Для того чтобы у сгенерированного в 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(). Свойство age объявлено в теле класса и исключено. Поэтому два объекта Person с одинаковым значением name, но разными значениями age считаются равными, так как equals() проверяет только свойства из основного конструктора.

data class Person(val name: String) {
    var age: Int = 0
}
fun main() {
//sampleStart
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20

    println("person1 == person2: ${person1 == person2}")
    // person1 == person2: true

    println("person1 with age ${person1.age}: ${person1}")
    // person1 with age 10: Person(name=John)

    println("person2 with age ${person2.age}: ${person2}")
    // person2 with age 20: Person(name=John)
//sampleEnd
}

Копирование

Используйте функцию 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)

Функция copy() создаёт поверхностную копию экземпляра. Другими словами, она не копирует компоненты рекурсивно. В результате ссылки на другие объекты остаются общими.

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

data class Employee(val name: String, val roles: MutableList<String>)

fun main() {
    val original = Employee("Jamie", mutableListOf("developer"))
    val duplicate = original.copy()

    duplicate.roles.add("team lead")

    println(original)
    // Employee(name=Jamie, roles=[developer, team lead])
    println(duplicate)
    // Employee(name=Jamie, roles=[developer, team lead])
}

Как видите, изменение свойства duplicate.roles также меняет свойство original.roles, потому что оба свойства ссылаются на один и тот же список.

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

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

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age

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

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