JEP 384: Records (Second Preview)
Records (записи), вторая версия Preview (предварительная версия)
| Автор | Brian Goetz |
| Ответственный | Vicente Arturo Romero Zaldivar |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 15 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 359: Records (Preview) |
| JEP 395: Records | |
| Рецензенты | Alex Buckley |
| Создан | 2020/04/07 20:05 |
| Обновлён | 2022/03/11 20:15 |
| Задача | 8242303 |
Аннотация
Расширить язык программирования Java возможностью Records — классами, которые служат прозрачными носителями неизменяемых данных. Records можно рассматривать как номинальные кортежи. Это Preview-возможность языка в JDK 15.
История
Records были предложены в JEP 359 в середине 2019 года и вошли в JDK 14 в начале 2020 года как Preview-возможность. Этот JEP предлагает повторно выпустить эту возможность в статусе Preview в JDK 15, чтобы учесть доработки по итогам отзывов и поддержать дополнительные формы локальных классов и интерфейсов в языке Java.
Цели
- Создать объектно-ориентированную конструкцию, которая выражает простую совокупность значений.
- Помочь программистам сосредоточиться на моделировании неизменяемых данных, а не расширяемого поведения.
- Автоматически реализовывать методы, определяемые данными, такие как
equalsи методы доступа. - Сохранить давние принципы Java, такие как номинальная типизация и совместимость при миграции.
Что не является целью
-
Цель не в том, чтобы объявить «войну шаблонному коду». В частности, цель не в том, чтобы решить проблемы изменяемых классов, которые следуют соглашениям об именовании JavaBeans.
-
Цель не в том, чтобы добавить такие возможности, как свойства или генерация кода на основе аннотаций, которые часто предлагают для упрощения объявления классов «Plain Old Java Objects» (простых старых Java-объектов).
Мотивация
Часто жалуются, что «Java слишком многословна» или в ней «слишком много церемоний». Одни из худших примеров — классы, которые представляют собой не более чем неизменяемые носители данных для нескольких значений. Чтобы правильно написать класс-носитель данных, нужно много малоценного, повторяющегося кода, в котором легко ошибиться: конструкторы, методы доступа, equals, hashCode, toString и т. д. Например, класс для хранения координат x и y неизбежно выглядит так:
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int x() { return x; }
int y() { return y; }
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return other.x == x && other.y = y;
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
Разработчики иногда поддаются соблазну срезать углы: пропускают такие методы, как equals, что приводит к неожиданному поведению или затрудняет отладку, или приспосабливают для этой задачи другой, не вполне подходящий класс, потому что у него «подходящая форма», а объявлять ещё один класс не хочется.
IDE помогают написать большую часть кода класса-носителя данных, но никак не помогают читателю извлечь из десятков строк шаблонного кода замысел «я носитель данных для x, y и z». Код на Java, который моделирует несколько значений, должно быть проще писать, читать и проверять на корректность.
Хотя на первый взгляд соблазнительно считать, что Records в первую очередь нужны для сокращения шаблонного кода, мы выбираем более семантическую цель: моделировать данные как данные. (Если семантика правильная, шаблонный код исчезнет сам собой.) Объявлять классы-носители данных, которые по умолчанию делают свои данные неизменяемыми и предоставляют идиоматичные реализации методов, создающих и использующих эти данные, должно быть просто и лаконично.
Описание
Records — новый вид классов в языке Java. Назначение record-класса — объявить, что небольшая группа переменных должна рассматриваться как новый вид сущности. Record-класс объявляет своё состояние — группу переменных — и обязуется предоставлять API, соответствующий этому состоянию. Это значит, что record-классы отказываются от свободы, которой обычно обладают классы, — возможности отделить API класса от его внутреннего представления, — но взамен становятся значительно лаконичнее.
Объявление record-класса задаёт имя, заголовок и тело. В заголовке перечислены компоненты record-класса — переменные, из которых состоит его состояние. (Список компонентов иногда называют описанием состояния.) Например:
record Point(int x, int y) { }
Поскольку record-классы семантически заявляют, что они прозрачные носители своих данных, record-класс автоматически получает многие стандартные члены:
-
Для каждого компонента в заголовке — два члена:
publicметод доступа с тем же именем и типом возвращаемого значения, что и у компонента, иprivatefinalполе того же типа, что и компонент; -
Канонический конструктор, сигнатура которого совпадает с заголовком и который присваивает каждому полю
privateсоответствующий аргумент из выраженияnew, создающего экземпляр record-класса; -
Методы
equalsиhashCode, согласно которым два record-объекта равны, если они одного типа и содержат равные значения компонентов; и -
Метод
toString, который возвращает строковое представление всех компонентов record-класса вместе с их именами.
Иными словами, заголовок record-класса описывает его состояние (типы и имена его компонентов), а API механически и полностью выводится из этого описания состояния. API включает протоколы создания, доступа к членам, проверки равенства и отображения. (Мы ожидаем, что в будущей версии появится поддержка шаблонов деконструкции, которые сделают возможным мощный Pattern Matching (сопоставление с образцом).)
Правила для Records
Любой из членов, автоматически выводимых из заголовка, кроме полей private, выводимых из компонентов record-класса, можно объявить явно. Любая явная реализация методов доступа или equals/hashCode должна аккуратно сохранять семантические инварианты record-классов.
Правила для конструкторов в record-классе отличаются от правил для обычного класса. Обычный класс без объявлений конструкторов автоматически получает конструктор по умолчанию. Record-класс без объявлений конструкторов, напротив, автоматически получает канонический конструктор, который присваивает всем полям private соответствующие аргументы выражения new, создавшего экземпляр record-класса. Например, объявленный ранее record-класс — record Point(int x, int y) { } — компилируется так, как если бы он был таким:
record Point(int x, int y) {
// Implicitly declared fields
private final int x;
private final int y;
// Other implicit declarations elided ...
// Implicitly declared canonical constructor
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Канонический конструктор можно объявить явно со списком формальных параметров, совпадающим с заголовком record-класса, как показано выше, или в более компактной форме, которая помогает разработчику сосредоточиться на проверке и нормализации параметров без утомительного присваивания параметров полям. Компактный канонический конструктор опускает список формальных параметров: они объявляются неявно, а полям private, соответствующим компонентам record-класса, нельзя присваивать значения в теле, но им автоматически присваиваются соответствующие формальные параметры (this.x = x;) в конце конструктора. Например, вот компактный канонический конструктор, который проверяет свои (неявные) формальные параметры:
record Range(int lo, int hi) {
Range {
if (lo > hi) // referring here to the implicit constructor parameters
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
На объявление record-класса наложено множество ограничений:
-
У record-класса нет предложения
extends. Суперкласс record-класса — всегдаjava.lang.Record, подобно тому как суперкласс перечисления — всегдаjava.lang.Enum. Хотя обычный класс может явно расширять свой неявный суперклассObject, record-класс не может явно расширять никакой класс, даже свой неявный суперклассRecord. -
Record-класс неявно является
finalи не может бытьabstract. Эти ограничения подчёркивают, что API record-класса определяется только описанием его состояния и не может быть позднее расширен другим классом или record-классом. -
Record-класс не может явно объявлять поля экземпляра и не может содержать инициализаторы экземпляра. Эти ограничения гарантируют, что состояние значения record-класса определяется только заголовком record-класса.
-
Неявно объявленные поля, соответствующие компонентам record-класса, являются
finalи, более того, не могут быть изменены через рефлексию (такая попытка приведёт к исключениюIllegalAccessException). Эти ограничения воплощают политику неизменяемости по умолчанию, которая широко применима к классам-носителям данных. -
Любое явное объявление члена, который иначе был бы выведен автоматически, должно точно совпадать по типу с автоматически выводимым членом, без учёта аннотаций типов в явном объявлении.
-
Record-класс не может объявлять методы
native. Если бы record-класс мог объявить методnative, то поведение record-класса по определению зависело бы от внешнего состояния, а не от явного состояния record-класса. Ни один класс с методамиnativeне является подходящим кандидатом для перевода в record-класс.
Помимо перечисленных выше ограничений, record-класс ведёт себя как обычный класс:
-
Экземпляр record-класса создаётся с помощью ключевого слова
new. -
Record-класс может быть объявлен на верхнем уровне или как вложенный, а также может быть обобщённым.
-
Record-класс может объявлять статические методы, статические поля и статические инициализаторы.
-
Record-класс может объявлять методы экземпляра. В частности, record-класс может явно объявлять методы доступа
public, соответствующие компонентам, а также может объявлять другие методы экземпляра. -
Record-класс может реализовывать интерфейсы. Хотя record-класс не может указать суперкласс (это означало бы унаследованное состояние помимо описанного в заголовке), он может свободно указывать суперинтерфейсы и объявлять методы экземпляра, помогающие их реализовать. Как и в случае классов, интерфейс может с пользой характеризовать поведение многих record-классов; это поведение может не зависеть от предметной области (например,
Comparable) или быть специфичным для неё — в этом случае record-классы могут быть частью sealed-иерархии, описывающей предметную область (см. ниже). -
Record-класс может объявлять вложенные типы, в том числе вложенные record-классы. Если record-класс сам является вложенным, то он неявно статический; так не возникает непосредственно объемлющего экземпляра, который незаметно добавил бы состояние record-классу.
-
Record-класс и компоненты в описании его состояния можно аннотировать. Аннотации переносятся на автоматически выводимые поля, методы и параметры конструктора. Аннотации типов на типах компонентов record-класса также переносятся на типы автоматически выводимых членов.
Records и sealed types
Records хорошо сочетаются с sealed types (JEP 360). Например, семейство record-классов может реализовывать один и тот же sealed-интерфейс:
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}
public record ConstantExpr(int i) implements Expr {...}
public record PlusExpr(Expr a, Expr b) implements Expr {...}
public record TimesExpr(Expr a, Expr b) implements Expr {...}
public record NegExpr(Expr e) implements Expr {...}
Сочетание Records и sealed types иногда называют алгебраическими типами данных. Records позволяют нам выражать типы-произведения, а sealed types — типы-суммы.
Локальные record-классы
Программа, которая создаёт и использует record-классы, скорее всего, будет иметь дело со множеством промежуточных значений, которые сами по себе являются простыми группами переменных. Часто будет удобно объявлять record-классы для моделирования этих промежуточных значений. Один вариант — объявить «вспомогательные» record-классы, которые являются static и вложенными, так же как многие программы сегодня объявляют вспомогательные классы. Более удобным вариантом было бы объявить record-класс внутри метода, рядом с кодом, который работает с этими переменными. Поэтому данный JEP предлагает локальные record-классы, аналогичные традиционной конструкции локальных классов.
В следующем примере совокупность продавца и его месячного объёма продаж моделируется локальным record-классом MerchantSales. Использование этого record-класса делает последующие операции над потоком данных более читаемыми:
List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
// Local record
record MerchantSales(Merchant merchant, double sales) {}
return merchants.stream()
.map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
.sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
.map(MerchantSales::merchant)
.collect(toList());
}
Локальные record-классы — частный случай вложенных record-классов. Как и все вложенные record-классы, локальные record-классы неявно статические. Это значит, что их собственные методы не могут обращаться к переменным объемлющего метода; в свою очередь, так не захватывается непосредственно объемлющий экземпляр, который незаметно добавил бы состояние record-классу. То, что локальные record-классы неявно статические, отличает их от локальных классов, которые неявно статическими не являются. Более того, локальные классы никогда не бывают статическими — ни неявно, ни явно — и всегда могут обращаться к переменным объемлющего метода.
Раз локальные record-классы полезны, было бы полезно иметь также локальные перечисления и локальные интерфейсы. Традиционно в Java они были запрещены из-за опасений относительно их семантики. В частности, вложенные перечисления и вложенные интерфейсы неявно статические, поэтому локальные перечисления и локальные интерфейсы тоже должны быть неявно статическими; однако локальные объявления в языке Java (локальные переменные, локальные классы) никогда не бывают статическими. Тем не менее появление локальных record-классов в JEP 359 сняло это семантическое затруднение: локальное объявление теперь может быть статическим, и это открыло путь локальным перечислениям и локальным интерфейсам.
Аннотации на record-классах
Компоненты record-класса играют в его объявлении несколько ролей. Компонент record-класса — самостоятельное понятие первого класса, но каждому компоненту также соответствуют поле с тем же именем и типом, метод доступа с тем же именем и возвращаемым типом и параметр конструктора с тем же именем и типом.
Отсюда возникает вопрос: что на самом деле аннотируется, когда аннотирован компонент? Ответ: «всё из перечисленного, к чему применима данная аннотация». Благодаря этому классы, которые используют аннотации на своих полях, параметрах конструкторов или методах доступа, можно перевести на record-классы без избыточного объявления этих членов. Например, класс вроде следующего
public final class Card {
private final @MyAnno Rank rank;
private final @MyAnno Suit suit;
@MyAnno Rank rank() { return this.rank; }
@MyAnno Suit suit() { return this.suit; }
...
}
можно перевести на эквивалентное и заметно более читаемое объявление record-класса:
public record Card(@MyAnno Rank rank, @MyAnno Suit suit) { ... }
Применимость аннотации объявляется с помощью мета-аннотации @Target. Рассмотрим следующее:
@Target(ElementType.FIELD)
public @interface I1 {...}
Здесь объявляется аннотация @I1 и то, что она применима к объявлению поля. Можно объявить, что аннотация применима к нескольким видам объявлений; например:
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface I2 {...}
Здесь объявляется аннотация @I2 и то, что она применима и к объявлению поля, и к объявлению метода.
Вернёмся к аннотациям на компоненте record-класса: эти аннотации появляются в тех соответствующих местах программы, где они применимы. Иными словами, распространением аннотаций управляет программист с помощью мета-аннотации @Target. Правила распространения систематичны и интуитивно понятны, и применяются все подходящие из них:
-
Если аннотация на компоненте record-класса применима к объявлению поля, то аннотация появляется на соответствующем поле
private. -
Если аннотация на компоненте record-класса применима к объявлению метода, то аннотация появляется на соответствующем методе доступа.
-
Если аннотация на компоненте record-класса применима к формальному параметру, то аннотация появляется на соответствующем формальном параметре канонического конструктора, если он не объявлен явно, или на соответствующем формальном параметре компактного конструктора, если он объявлен явно.
-
Если аннотация на компоненте record-класса применима к типу, то правила распространения те же, что и для аннотаций объявлений, за исключением того, что аннотация появляется на соответствующем использовании типа, а не на объявлении.
Если открытый метод доступа или (некомпактный) канонический конструктор объявлен явно, то у него есть только те аннотации, которые указаны на нём непосредственно; с соответствующего компонента record-класса на эти члены ничего не распространяется.
Также можно объявить, что аннотация пришла от аннотации, определённой на компоненте record-класса, с помощью нового объявления аннотации @Target(RECORD_COMPONENT). Такие аннотации можно получить через рефлексию, как подробно описано ниже в разделе Reflection API.
Грамматика Java
RecordDeclaration:
{ClassModifier} `record` TypeIdentifier [TypeParameters]
RecordHeader [SuperInterfaces] RecordBody
RecordHeader:
`(` [RecordComponentList] `)`
RecordComponentList:
RecordComponent { `,` RecordComponent}
RecordComponent:
{Annotation} UnannType Identifier
VariableArityRecordComponent
VariableArityRecordComponent:
{Annotation} UnannType {Annotation} `...` Identifier
RecordBody:
`{` {RecordBodyDeclaration} `}`
RecordBodyDeclaration:
ClassBodyDeclaration
CompactConstructorDeclaration
CompactConstructorDeclaration:
{Annotation} {ConstructorModifier} SimpleTypeName ConstructorBody
Представление в class-файле
Файл class record-класса использует атрибут Record для хранения информации о компонентах record-класса:
Record_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 components_count;
record_component_info components[components_count];
}
record_component_info {
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Если у компонента record-класса есть обобщённая сигнатура, отличающаяся от стёртого дескриптора, в структуре record_component_info должен быть атрибут Signature.
Reflection API
В java.lang.Class будут добавлены следующие открытые методы:
RecordComponent[] getRecordComponents()boolean isRecord()
Метод getRecordComponents() возвращает массив объектов java.lang.reflect.RecordComponent. Элементы этого массива соответствуют компонентам record-класса в том же порядке, в каком они указаны в объявлении record-класса. Из каждого элемента массива можно извлечь дополнительную информацию, включая его имя, аннотации и метод доступа.
Метод isRecord возвращает true, если данный класс был объявлен как record-класс. (Сравните с isEnum.)
Альтернативы
Record-классы можно считать номинальной формой кортежей. Вместо record-классов мы могли бы реализовать структурные кортежи. Однако, хотя кортежи, возможно, дали бы более легковесный способ выражать некоторые агрегаты, в результате часто получаются худшие агрегаты:
-
Одна из центральных идей философии проектирования Java состоит в том, что имена важны. У классов и их членов есть осмысленные имена, а у кортежей и компонентов кортежей их нет. То есть класс
Personсо свойствамиfirstNameиlastNameпонятнее и безопаснее, чем анонимный кортеж изStringиString. -
Классы поддерживают проверку состояния через свои конструкторы; кортежи — нет. У некоторых агрегатов данных (например, числовых диапазонов) есть инварианты, на которые, если их обеспечивает конструктор, можно в дальнейшем полагаться; кортежи такой возможности не дают.
-
У классов может быть поведение, основанное на их состоянии; размещение состояния и поведения вместе делает поведение более заметным и доступным. Кортежи, будучи просто данными, такой возможности не дают.
Зависимости
Помимо упомянутого выше сочетания record-классов и sealed-типов, record-классы естественным образом подходят для Pattern Matching. Поскольку record-классы связывают свой API с описанием своего состояния, со временем мы сможем выводить для record-классов и деконструирующие шаблоны, а также использовать информацию о sealed-типах, чтобы определять полноту в выражениях switch с шаблонами типов или деконструирующими шаблонами.