JEP 432: Record Patterns (Second Preview)
Record Patterns (шаблоны записей), вторая версия Preview (предварительная версия)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 20 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Связан с | JEP 405: Record Patterns (Preview) |
| JEP 433: Pattern Matching for switch (Fourth Preview) | |
| JEP 440: Record Patterns | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2022/09/20 22:08 |
| Обновлён | 2023/05/12 15:34 |
| Задача | 8294078 |
Аннотация
Дополнить язык программирования Java возможностью Record Patterns для деконструкции значений record-классов. Record-шаблоны и шаблоны типов можно вкладывать друг в друга, что даёт мощный, декларативный и компонуемый способ навигации по данным и их обработки. Это возможность языка в статусе Preview.
История
Record Patterns были предложены как Preview-возможность в JEP 405 и вошли в JDK 19. Этот JEP предлагает вторую версию Preview с дальнейшими доработками на основе накопленного опыта и отзывов.
Основные изменения по сравнению с первой версией Preview:
- добавить поддержку вывода аргументов типа для обобщённых record-шаблонов,
- добавить поддержку record-шаблонов в заголовке расширенного оператора
forи - убрать поддержку именованных record-шаблонов.
Цели
-
Расширить Pattern Matching (сопоставление с образцом), чтобы выражать более сложные и компонуемые запросы к данным.
-
Не менять синтаксис и семантику шаблонов типов.
Мотивация
В JDK 16 JEP 394 расширил оператор instanceof: теперь он принимает шаблон типа и выполняет Pattern Matching. Это скромное расширение позволяет упростить привычную идиому «instanceof и приведение типа»:
// Old code
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}
// New code
if (obj instanceof String s) {
... use s ...
}
В новом коде obj соответствует шаблону типа String s, если во время выполнения значение obj является экземпляром String. Если шаблон соответствует, выражение instanceof равно true, а переменная шаблона s инициализируется значением obj, приведённым к String, и её можно использовать во вложенном блоке.
В JDK 17, JDK 18 и JDK 19 мы распространили шаблоны типов также на метки case оператора switch — в JEP 406, JEP 420 и JEP 427.
Шаблоны типов разом убирают множество приведений типов. Однако это лишь первый шаг к более декларативному стилю программирования, ориентированному на данные. По мере того как Java поддерживает новые, более выразительные способы моделирования данных, Pattern Matching может упростить работу с такими данными, позволяя разработчикам выражать семантический замысел своих моделей.
Pattern Matching и record-классы
Record-классы (JEP 395) — прозрачные носители данных. Код, получающий экземпляр record-класса, как правило, извлекает из него данные, называемые компонентами. Например, можно с помощью шаблона типа проверить, является ли значение экземпляром record-класса Point, и если да, извлечь из значения компоненты x и y:
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj 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 obj) {
if (obj 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-шаблоны поддерживают вложенность: компонент 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);
}
}
Разумеется, вложенные шаблоны могут и не совпасть:
record Pair(Object x, Object y) {}
Pair p = new Pair(42, 42);
if (p instanceof Pair(String s, String t)) {
System.out.println(s + ", " + t);
} else {
System.out.println("Not a pair of strings");
}
Здесь record-шаблон Pair(String s, String t) содержит два вложенных шаблона: String s и String t. Значение соответствует шаблону Pair(String s, String t), если оно является Pair и, рекурсивно, значения его компонентов соответствуют шаблонам String s и String t. В приведённом выше примере эти рекурсивные сопоставления не удаются, поскольку ни одно из значений компонентов не является строкой, и поэтому выполняется блок else.
Итак, вложенные шаблоны убирают привнесённую сложность навигации по объектам, и мы можем сосредоточиться на данных, которые эти объекты выражают.
Выражение instanceof и switch — не единственные места, где удобно поведение record-шаблона, разбирающего объект на части. Если разрешить record-шаблон в расширенном операторе for, можно будет легко перебирать коллекцию значений record-классов и быстро извлекать компоненты каждого из них. Например:
record Point(int x, int y) {}
static void dump(Point[] pointArray) {
for (Point(var x, var y) : pointArray) { // Record Pattern in header!
System.out.println("(" + x + ", " + y + ")");
}
}
Смысл интуитивно понятен: на каждой итерации цикла очередной элемент массива или Iterable сопоставляется с record-шаблоном в заголовке.
Record-шаблон в расширенном операторе for может содержать вложенные шаблоны, например:
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printUpperLeftColors(Rectangle[] r) {
for (Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr): r) {
System.out.println(c);
}
}
Описание
Мы дополняем язык программирования Java record-шаблонами, допускающими вложение.
Грамматика шаблонов станет такой:
Pattern:
TypePattern
ParenthesizedPattern
RecordPattern
TypePattern:
LocalVariableDeclaration
ParenthesizedPattern:
( Pattern )
RecordPattern:
ReferenceType RecordStructurePattern
RecordStructurePattern:
( [ RecordComponentPatternList ] )
RecordComponentPatternList :
Pattern { , Pattern }
Record-шаблоны
Record-шаблон состоит из типа и (возможно, пустого) списка шаблонов компонентов, который используется для сопоставления с соответствующими компонентами record-класса.
Например, для объявления
record Point(int i, int j) {}
значение v соответствует record-шаблону Point(int i, int j), если оно является экземпляром record-типа Point; в этом случае переменная шаблона i инициализируется результатом вызова метода доступа, соответствующего i, для значения v, а переменная шаблона j инициализируется результатом вызова метода доступа, соответствующего j, для значения v. (Имена переменных шаблона не обязаны совпадать с именами компонентов record-класса: например, record-шаблон Point(int x, int y) работает так же, только инициализируются переменные шаблона x и y.)
Значение null не соответствует ни одному record-шаблону.
Record-шаблон может использовать var для сопоставления с компонентом record-класса без указания типа компонента. В этом случае компилятор выводит тип переменной шаблона, вводимой шаблоном var. Например, шаблон Point(var a, var b) — это сокращённая запись шаблона Point(int a, int b).
Множество переменных шаблона, объявленных record-шаблоном, включает все переменные шаблона, объявленные в списке шаблонов компонентов.
Выражение совместимо с record-шаблоном, если его можно привести к record-типу из шаблона без непроверяемого преобразования.
Если record-класс обобщённый, его можно использовать в record-шаблоне либо как параметризованный тип, либо как сырой тип. Например:
record Box<T>(T t) {}
static void test1(Box<String> bo) {
if (bo instanceof Box<String>(var s)) {
System.out.println("String " + s);
}
}
Здесь тип record-класса в record-шаблоне — параметризованный тип. Эквивалентно можно записать так, и в этом случае аргумент типа выводится:
static void test2(Box<String> bo) {
if (bo instanceof Box(var s)) { // Inferred to be Box<String>(var s)
System.out.println("String " + s);
}
}
Вывод типов применяется и к вложенным record-шаблонам. Например:
static void test3(Box<Box<String>> bo) {
if (bo instanceof Box<Box<String>>(Box(var s))) {
System.out.println("String " + s);
}
}
Здесь аргумент типа для вложенного шаблона Box(var s) выводится. Ещё короче будет убрать аргументы типа и во внешнем record-шаблоне:
static void test4(Box<Box<String>> bo) {
if (bo instanceof Box(Box(var s))) {
System.out.println("String " + s);
}
}
Шаблоны типов не поддерживают неявный вывод аргументов типа: например, шаблон типа List l всегда рассматривается как шаблон сырого типа.
Record-шаблоны и исчерпывающий 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-шаблоны и расширенные операторы for
Если R — record-шаблон, то расширенный оператор for вида
for (R : e) S
эквивалентен следующему расширенному оператору for, в заголовке которого нет record-шаблона:
for (var tmp : e) {
switch(tmp) {
case null -> throw new MatchException(new NullPointerException());
case R -> S;
}
}
Из такой трансляции следует:
-
Record-шаблон
Rдолжен быть применим к типу элементов массива илиIterable. -
Record-шаблон
Rдолжен быть исчерпывающим для типа элементов массива илиIterable. -
Если какой-либо элемент
eравенnull, выполнение расширенного оператораforприводит к выбросуMatchException.
Например:
record Pair(Object fst, Object snd){}
static void notApplicable(String[] arg) {
for (Pair(var fst, var snd): arg) { // Compile-time error, pattern not applicable
System.out.println("An element");
}
}
static void notExhaustive(Pair[] arg) {
for (Pair(String s, String t): arg) { // Compile-time error, pattern not exhaustive
System.out.println(s+", "+t);
}
}
static void exceptionTest() {
Pair[] ps = new Pair[]{
new Pair(1,2),
null,
new Pair("hello","world")
};
for (Pair(var f, var s): ps) { // Run-time MatchException
System.out.println(f);
}
}
Дальнейшая работа
Описанные здесь record-шаблоны можно расширять во многих направлениях:
- шаблоны массивов, подшаблоны которых сопоставляются с отдельными элементами массива;
- шаблоны varargs — когда record-класс объявлен с varargs;
- шаблоны «не важно» (do-not-care), которые могут стоять элементом списка шаблонов компонентов, но не объявляют переменную шаблона; и
- шаблоны на основе произвольных классов, а не только record-классов.
Некоторые из них мы, возможно, рассмотрим в будущих JEP.
Зависимости
Этот JEP основан на JEP 394 (Pattern Matching for instanceof), вошедшем в JDK 16.