JEP 405: Record Patterns (Preview)
Record Patterns (шаблоны записей), версия Preview (предварительная версия)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 19 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Связан с | JEP 427: Pattern Matching for switch (Third Preview) |
| JEP 432: Record Patterns (Second Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2021/01/21 16:44 |
| Обновлён | 2023/05/12 15:34 |
| Задача | 8260244 |
Аннотация
Расширить язык программирования Java с помощью Record Patterns для деконструкции значений record-классов. Record Patterns и шаблоны типов можно вкладывать друг в друга, и так получается мощная, декларативная и компонуемая форма навигации по данным и их обработки. Это возможность языка в статусе Preview.
Цели
-
Расширить Pattern Matching (сопоставление с образцом), чтобы на нём можно было выражать более сложные и компонуемые запросы к данным.
-
Не менять синтаксис и семантику шаблонов типов.
Мотивация
В JDK 16 JEP 394 расширил оператор instanceof: теперь он принимает шаблон типа и выполняет Pattern Matching. Это скромное расширение позволяет упростить привычную идиому «instanceof и приведение типа»:
// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}
// New code
if (o instanceof String s) {
... use s ...
}
В новом коде o соответствует шаблону типа String s, если во время выполнения значение o является экземпляром String. Если шаблон совпадает, выражение instanceof равно true, а переменная шаблона s инициализируется значением o, приведённым к String. Затем её можно использовать во вложенном блоке.
В JDK 17 и JDK 18 мы распространили шаблоны типов и на метки case в switch с помощью JEP 406 и JEP 420.
Шаблоны типов разом убирают множество приведений типа. Однако это лишь первый шаг к более декларативному стилю программирования, ориентированному на данные. Java поддерживает новые, более выразительные способы моделирования данных, и Pattern Matching может упростить работу с такими данными: разработчики смогут выражать смысловое намерение своих моделей.
Pattern Matching и record-классы
Record-классы (JEP 395) — прозрачные носители данных. Код, получающий экземпляр record-класса, как правило, извлекает из него данные, называемые компонентами. Например, с помощью шаблона типа можно проверить, является ли значение экземпляром record-класса Point, и если да, извлечь из значения компоненты x и y:
record Point(int x, int y) {}
static void printSum(Object o) {
if (o instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
Переменная шаблона p здесь нужна только для вызова методов доступа x() и y(), которые возвращают значения компонентов x и y. (В каждом record-классе методы доступа и компоненты взаимно однозначно соответствуют друг другу.) Было бы лучше, если бы шаблон не только проверял, является ли значение экземпляром Point, но и сразу извлекал из значения компоненты x и y, вызывая методы доступа за нас. Иными словами:
record Point(int x, int y) {}
void printSum(Object o) {
if (o instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
Point(int x, int y) — это record-шаблон. Он переносит объявление локальных переменных для извлекаемых компонентов в сам шаблон и инициализирует эти переменные, вызывая методы доступа, когда значение сопоставляется с шаблоном. По сути, record-шаблон разбирает экземпляр record-класса на компоненты.
Настоящая сила Pattern Matching в том, что он изящно масштабируется для сопоставления с более сложными графами объектов. Рассмотрим, например, следующие объявления:
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
Мы уже видели, что компоненты объекта можно извлечь с помощью record-шаблона. Если нужно извлечь цвет левой верхней точки, можно написать:
static void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}
Но наш ColoredPoint сам является record-классом, и его может понадобиться разобрать дальше. Поэтому Record Patterns поддерживают вложенность: компонент record-класса можно дальше сопоставлять с вложенным шаблоном и разбирать с его помощью. Можно вложить другой шаблон в record-шаблон и разобрать сразу и внешний, и внутренний record-классы:
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
Кроме того, вложенные шаблоны позволяют разбирать составной объект кодом, который так же ясен и лаконичен, как код, который его собирает. Например, создавая прямоугольник, мы, скорее всего, вложили бы конструкторы в одно выражение:
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
new ColoredPoint(new Point(x2, y2), c2));
С вложенными шаблонами такой прямоугольник можно деконструировать кодом, повторяющим структуру вложенных конструкторов:
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
var lr)) {
System.out.println("Upper-left corner: " + x);
}
}
Итак, вложенные шаблоны убирают случайную сложность навигации по объектам, и мы можем сосредоточиться на данных, которые эти объекты выражают.
Описание
Мы расширяем язык программирования Java вкладываемыми Record Patterns.
Грамматика шаблонов станет такой:
Pattern:
TypePattern
ParenthesizedPattern
RecordPattern
TypePattern:
LocalVariableDeclaration
ParenthesizedPattern:
( Pattern )
RecordPattern:
ReferenceType RecordStructurePattern [ Identifier ]
RecordStructurePattern:
( [ RecordComponentPatternList ] )
RecordComponentPatternList :
Pattern { , Pattern }
Record Patterns
Record-шаблон состоит из типа, списка шаблонов компонентов (возможно, пустого), который используется для сопоставления с соответствующими компонентами record-класса, и необязательного идентификатора. Record-шаблон с идентификатором называется именованным record-шаблоном, а его переменная — переменной record-шаблона.
Например, при объявлении
record Point(int i, int j) {}
значение v соответствует record-шаблону Point(int i, int j) p, если оно является экземпляром record-типа Point. В этом случае переменная шаблона i инициализируется результатом вызова на этом значении метода доступа, соответствующего i, а переменная шаблона j — результатом вызова на этом значении метода доступа, соответствующего j. (Имена переменных шаблона не обязаны совпадать с именами компонентов record-класса, то есть record-шаблон Point(int x, int y) действует точно так же, только инициализируются переменные шаблона x и y.) Переменная record-шаблона p инициализируется значением v, приведённым к Point.
Значение null не соответствует ни одному record-шаблону.
Record-шаблон может использовать var для сопоставления с компонентом record-класса, не указывая тип компонента. В этом случае компилятор выводит тип переменной шаблона, вводимой шаблоном var. Например, шаблон Point(var a, var b) — сокращённая запись шаблона Point(int a, int b).
Множество переменных шаблона, объявленных record-шаблоном, включает все переменные шаблона, объявленные в списке шаблонов компонентов, а если record-шаблон именованный, то и переменную record-шаблона.
Выражение совместимо с record-шаблоном, если его можно привести к record-типу из шаблона без непроверяемого преобразования (unchecked conversion).
Если record-класс обобщённый, то любой record-шаблон, в котором назван этот record-класс, должен использовать обобщённый тип. Например, при объявлении:
record Box<T>(T t) {}
Следующие методы корректны:
static void test1(Box<Object> bo) {
if (bo instanceof Box<Object>(String s)) {
System.out.println("String " + s);
}
}
static void test2(Box<Object> bo) {
if (bo instanceof Box<String>(var s)) {
System.out.println("String " + s);
}
}
а оба следующих приводят к ошибкам компиляции:
static void erroneousTest1(Box<Object> bo) {
if (bo instanceof Box(var s)) { // Error
System.out.println("I'm a box");
}
}
static void erroneousTest2(Box b) {
if (b instanceof Box(var t)) { // Error
System.out.println("I'm a box");
}
}
В будущем мы можем расширить вывод типов, чтобы выводить аргументы типа обобщённых record-шаблонов.
Record Patterns и исчерпывающий switch
JEP 420 расширил и выражения switch, и операторы switch: они поддерживают метки, содержащие шаблоны, в том числе record-шаблоны. И выражения switch, и операторы switch с шаблонами должны быть исчерпывающими: в блоке switch должны быть ветви, обрабатывающие все возможные значения выражения-селектора. Для меток с шаблонами это определяется анализом типов шаблонов. Например, метка case case Bar b соответствует значениям типа Bar и всех возможных подтипов Bar.
Для меток с record-шаблонами анализ сложнее, поскольку нужно учитывать типы шаблонов компонентов и иерархии sealed. Рассмотрим, например, объявления:
class A {}
class B extends A {}
sealed interface I permits C, D {}
final class C implements I {}
final class D implements I {}
record Pair<T>(T x, T y) {}
Pair<A> p1;
Pair<I> p2;
Следующий switch не является исчерпывающим, поскольку нет соответствия для пары, содержащей два значения типа A:
switch (p1) { // Error!
case Pair<A>(A a, B b) -> ...
case Pair<A>(B b, A a) -> ...
}
Эти два switch являются исчерпывающими, поскольку интерфейс I объявлен как sealed, и поэтому типы C и D покрывают все возможные экземпляры:
switch (p2) {
case Pair<I>(I i, C c) -> ...
case Pair<I>(I i, D d) -> ...
}
switch (p2) {
case Pair<I>(C c, I i) -> ...
case Pair<I>(D d, C c) -> ...
case Pair<I>(D d1, D d2) -> ...
}
Напротив, этот switch не является исчерпывающим, поскольку нет соответствия для пары, содержащей два значения типа D:
switch (p2) { // Error!
case Pair<I>(C fst, D snd) -> ...
case Pair<I>(D fst, C snd) -> ...
case Pair<I>(I fst, C snd) -> ...
}
Дальнейшая работа
Описанные здесь Record Patterns можно расширять во многих направлениях:
- шаблоны массивов, подшаблоны которых сопоставляются с отдельными элементами массива;
- шаблоны varargs, когда record-класс имеет компонент varargs;
- вывод аргументов типа в обобщённых record-шаблонах, возможно, с использованием ромбовидной формы (
<>); - шаблоны «не важно», которые могут стоять элементом в списке шаблонов компонентов, но не объявляют переменную шаблона;
- шаблоны на основе произвольных классов, а не только record-классов.
Некоторые из этих направлений мы можем рассмотреть в будущих JEP.
Зависимости
Этот JEP основан на JEP 394 (Pattern Matching for instanceof), выпущенном в JDK 16.