JEP 359: Records (Preview)
Records (записи), версия Preview (предварительная версия)
| Автор | Brian Goetz |
| Ответственный | Vicente Arturo Romero Zaldivar |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 14 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 384: Records (Second Preview) |
| JEP 395: Records | |
| Рецензенты | Alex Buckley |
| Создан | 2019/04/19 19:30 |
| Обновлён | 2021/08/28 00:18 |
| Задача | 8222777 |
Аннотация
Расширить язык программирования Java возможностью Records. Records дают компактный синтаксис для объявления классов, которые служат прозрачными носителями неглубоко неизменяемых данных. В JDK 14 это возможность языка в статусе Preview.
Мотивация и цели
Часто жалуются, что «Java слишком многословна» или что в ней слишком много «церемоний». Одни из худших нарушителей — классы, которые представляют собой не более чем простые «носители данных» и служат простыми агрегатами. Чтобы правильно написать класс — носитель данных, приходится писать много малоценного, повторяющегося кода, в котором легко ошибиться: конструкторы, методы доступа, equals(), hashCode(), toString() и т. д. У разработчиков иногда возникает соблазн срезать углы, например опустить эти важные методы (что приводит к неожиданному поведению или затрудняет отладку) или приспособить для этой цели другой, не вполне подходящий класс (потому что у него «правильная форма» и не хочется объявлять ещё один класс).
IDE помогают написать большую часть кода класса — носителя данных, но никак не помогают читателю извлечь из десятков строк шаблонного кода замысел «я носитель данных для x, y и z». Код на Java, моделирующий простые агрегаты, должно быть проще писать, читать и проверять на корректность.
Хотя на первый взгляд заманчиво считать, что Records прежде всего нужны для сокращения шаблонного кода, мы выбираем более смысловую цель: моделировать данные как данные. (Если семантика верна, шаблонный код позаботится о себе сам.) Объявлять неглубоко неизменяемые, корректно ведущие себя номинальные агрегаты данных должно быть просто, понятно и лаконично.
Что не является целью
Объявить «войну шаблонному коду» не является целью; в частности, не является целью решать проблемы изменяемых классов, использующих соглашения об именовании JavaBean. Не является целью и добавление таких возможностей, как свойства, метапрограммирование и генерация кода на основе аннотаций, хотя их часто предлагают как «решения» этой проблемы.
Описание
Records — новый вид объявления типа в языке Java. Как и enum, record — это ограниченная форма класса. Он объявляет своё представление и обязуется предоставлять API, соответствующий этому представлению. Records отказываются от свободы, которой обычно пользуются классы: возможности отделить API от представления. Взамен Records получают значительную лаконичность.
У record-класса есть имя и описание состояния. Описание состояния объявляет компоненты record-класса. Кроме того, у record-класса может быть тело (необязательно). Например:
record Point(int x, int y) { }
Поскольку record-классы семантически заявлены как простые прозрачные носители своих данных, record-класс автоматически получает многие стандартные члены:
- поле private final для каждого компонента описания состояния;
- публичный метод доступа для чтения для каждого компонента описания состояния, с тем же именем и типом, что у компонента;
- публичный конструктор с сигнатурой, совпадающей с описанием состояния, который инициализирует каждое поле из соответствующего аргумента;
- реализации
equalsиhashCode, по которым два экземпляра record-класса равны, если они одного типа и содержат одинаковое состояние; и - реализация
toString, которая включает строковое представление всех компонентов record-класса вместе с их именами.
Иначе говоря, представление record-класса механически и полностью выводится из описания состояния, как и протоколы создания, деконструкции (поначалу методы доступа, а когда у нас появится Pattern Matching (сопоставление с образцом), шаблоны деконструкции), равенства и отображения.
Ограничения record-классов
Record-классы не могут расширять никакой другой класс и не могут объявлять поля экземпляра, кроме полей private final, соответствующих компонентам описания состояния. Все остальные объявленные поля должны быть статическими. Эти ограничения гарантируют, что представление определяется только описанием состояния.
Record-классы неявно являются final и не могут быть abstract. Эти ограничения подчёркивают, что API record-класса определяется исключительно его описанием состояния и не может быть позднее расширен другим классом или record-классом.
Компоненты record-класса неявно являются final. Это ограничение воплощает политику неизменяемости по умолчанию, которая широко применима к агрегатам данных.
Помимо перечисленных ограничений, record-классы ведут себя как обычные классы: их можно объявлять на верхнем уровне или как вложенные, они могут быть обобщёнными, могут реализовывать интерфейсы, а их экземпляры создаются с помощью ключевого слова new. В теле record-класса можно объявлять статические методы, статические поля, статические инициализаторы, конструкторы, методы экземпляра и вложенные типы. Сам record-класс и отдельные компоненты в описании состояния можно аннотировать. Если record-класс вложенный, он неявно статический; так исключается непосредственно объемлющий экземпляр, который незаметно добавил бы record-классу состояние.
Явное объявление членов record-класса
Любой из членов, автоматически выводимых из описания состояния, можно объявить и явно. Однако небрежная реализация методов доступа или equals/hashCode грозит нарушить семантические инварианты record-классов.
Особые условия предусмотрены для явного объявления канонического конструктора (того, чья сигнатура совпадает с описанием состояния record-класса). Конструктор можно объявить без списка формальных параметров (в этом случае считается, что он совпадает с описанием состояния), и все поля record-класса, которые заведомо не присвоены при нормальном завершении тела конструктора, неявно инициализируются на выходе из соответствующих формальных параметров (this.x = x). Так явный канонический конструктор может выполнять только проверку и нормализацию своих параметров и опускать очевидную инициализацию полей. Например:
record Range(int lo, int hi) {
public Range {
if (lo > hi) /* referring here to the implicit constructor parameters */
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
Грамматика
RecordDeclaration:
{ClassModifier} record TypeIdentifier [TypeParameters]
(RecordComponents) [SuperInterfaces] [RecordBody]
RecordComponents:
{RecordComponent {, RecordComponent}}
RecordComponent:
{Annotation} UnannType Identifier
RecordBody:
{ {RecordBodyDeclaration} }
RecordBodyDeclaration:
ClassBodyDeclaration
RecordConstructorDeclaration
RecordConstructorDeclaration:
{Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
[Throws] ConstructorBody
Аннотации на компонентах record-классов
Аннотации объявлений разрешены на компонентах record-класса, если они применимы к компонентам record-классов, параметрам, полям или методам. Аннотации объявлений, применимые к любой из этих целей, распространяются на неявные объявления всех обязательных членов.
Аннотации типов, изменяющие типы компонентов record-класса, распространяются на типы в неявных объявлениях обязательных членов (например, на параметры конструктора, объявления полей и объявления методов). Явные объявления обязательных членов должны в точности совпадать по типу с соответствующим компонентом record-класса, не считая аннотаций типов.
API рефлексии
В java.lang.Class будут добавлены следующие публичные методы:
RecordComponent[] getRecordComponents()boolean isRecord()
Метод getRecordComponents() возвращает массив объектов java.lang.reflect.RecordComponent, где java.lang.reflect.RecordComponent — новый класс. Элементы этого массива соответствуют компонентам record-класса в том же порядке, в каком они указаны в объявлении record-класса. Из каждого RecordComponent в массиве можно получить дополнительную информацию, включая его имя, тип, обобщённый тип, аннотации и метод доступа.
Метод isRecord() возвращает true, если данный класс объявлен как record-класс. (Сравните с isEnum().)
Альтернативы
Records можно считать номинальной формой кортежей. Вместо Records мы могли бы реализовать структурные кортежи. Однако, хотя кортежи, возможно, дали бы более легковесный способ выражать некоторые агрегаты, в результате часто получаются худшие агрегаты:
-
Центральный принцип философии Java состоит в том, что имена важны. У классов и их членов есть осмысленные имена, а у кортежей и компонентов кортежей их нет. То есть класс
Personсо свойствамиfirstNameиlastNameпонятнее и безопаснее, чем анонимный кортеж изStringиString. -
Классы поддерживают проверку состояния в своих конструкторах; кортежи — нет. У некоторых агрегатов данных (например, числовых диапазонов) есть инварианты, на которые, если их обеспечивает конструктор, можно в дальнейшем полагаться; кортежи такой возможности не дают.
-
У классов может быть поведение, основанное на их состоянии; когда состояние и поведение находятся рядом, поведение легче обнаружить и проще к нему обратиться. Кортежи, будучи сырыми данными, такой возможности не дают.
Зависимости
Records хорошо сочетаются с sealed-типами (JEP 360); вместе record-классы и sealed-типы образуют конструкцию, которую часто называют алгебраическими типами данных. Кроме того, record-классы естественным образом подходят для Pattern Matching. Поскольку record-классы связывают свой API с описанием состояния, со временем мы сможем выводить для record-классов и шаблоны деконструкции, а также использовать информацию о sealed-типах, чтобы определять исчерпывающую полноту в выражениях switch с шаблонами типов или шаблонами деконструкции.