JEP 440: Record Patterns
Record Patterns (шаблоны записей)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 21 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Связан с | JEP 432: Record Patterns (Second Preview) |
| Рецензенты | Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2023/01/18 14:38 |
| Обновлён | 2023/08/28 16:51 |
| Задача | 8300541 |
Аннотация
Расширение языка программирования Java с помощью Record Patterns для деконструкции значений record-классов. Record Patterns и шаблоны типов (type patterns) можно вкладывать друг в друга. Так получается мощная, декларативная и компонуемая форма навигации по данным и их обработки.
История
Record Patterns были предложены в статусе Preview (предварительная версия) в JEP 405 и вошли в JDK 19, а затем прошли вторую версию Preview в JEP 432 и вошли в JDK 20. Эта возможность развивалась совместно с Pattern Matching (сопоставление с образцом) for switch (JEP 441) и во многом с ним взаимодействует. Этот JEP предлагает сделать возможность окончательной, с дальнейшими доработками на основе накопленного опыта и отзывов.
Помимо нескольких незначительных редакционных изменений, основное изменение со времени второй версии Preview — удаление поддержки record-шаблонов в заголовке расширенного оператора for. Эта возможность может быть предложена повторно в одном из будущих JEP.
Цели
-
Расширить Pattern Matching для деструктуризации экземпляров классов
record, что позволит выполнять более сложные запросы к данным. -
Добавить вложенные шаблоны, что позволит составлять более компонуемые запросы к данным.
Мотивация
В Java 16 JEP 394 расширил оператор instanceof: теперь он принимает шаблон типа и выполняет Pattern Matching. Это скромное расширение позволяет упростить привычную идиому «instanceof с последующим приведением», сделав её одновременно более краткой и менее подверженной ошибкам:
// Prior to Java 16
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}
// As of Java 16
if (obj instanceof String s) {
... use s ...
}
В новом коде obj совпадает с шаблоном типа String s, если во время выполнения значение obj является экземпляром String. Если шаблон совпадает, выражение instanceof равно true, а переменная шаблона s инициализируется значением obj, приведённым к String, и её затем можно использовать внутри блока.
Шаблоны типов разом убирают множество приведений типов. Однако это лишь первый шаг к более декларативному стилю программирования, ориентированному на данные. По мере того как Java поддерживает новые, более выразительные способы моделирования данных, Pattern Matching может упростить работу с такими данными, позволяя разработчикам выражать смысловое намерение своих моделей.
Pattern Matching и Records (записи)
Records (JEP 395) — это прозрачные носители данных. Код, получающий экземпляр record-класса, обычно извлекает данные, называемые компонентами, с помощью встроенных методов доступа к компонентам. Например, мы можем использовать шаблон типа, чтобы проверить, является ли значение экземпляром record-класса Point, и если да, извлечь из значения компоненты x и y:
// As of Java 16
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, вызвав методы доступа за нас. Иначе говоря:
// As of Java 21
static 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-класса на компоненты.
Вложенные record-шаблоны
Истинная сила Pattern Matching в том, что он изящно масштабируется для сопоставления более сложных графов объектов. Например, рассмотрим следующие объявления:
// As of Java 16
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
Мы уже видели, что компоненты объекта можно извлечь с помощью record-шаблона. Если мы хотим извлечь цвет левой верхней точки, можно написать:
// As of Java 21
static void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}
Но значение ColoredPoint ul само является значением record-класса, которое мы, возможно, захотим разобрать дальше. Поэтому record-шаблоны поддерживают вложенность: компонент записи можно дополнительно сопоставить с вложенным шаблоном и разобрать с его помощью. Мы можем вложить другой шаблон в record-шаблон и разобрать внешнюю и внутреннюю записи одновременно:
// As of Java 21
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
Кроме того, вложенные шаблоны позволяют разбирать составной объект кодом, столь же ясным и кратким, как код, который его собирает. Например, создавая прямоугольник, мы, скорее всего, вложили бы конструкторы в одно выражение:
// As of Java 16
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
new ColoredPoint(new Point(x2, y2), c2));
С помощью вложенных шаблонов такой прямоугольник можно разобрать кодом, который повторяет структуру вложенных конструкторов:
// As of Java 21
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);
}
}
Вложенные шаблоны, конечно, могут и не совпасть:
// As of Java 21
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.
Итак, вложенные шаблоны устраняют случайную сложность навигации по объектам, и мы можем сосредоточиться на данных, которые эти объекты выражают. Они также позволяют централизовать обработку ошибок, поскольку значение не совпадает с вложенным шаблоном P(Q), если не совпадает один из подшаблонов или оба. Нам не нужно проверять и обрабатывать каждую отдельную неудачу сопоставления подшаблона — либо весь шаблон совпадает, либо нет.
Описание
Мы расширяем язык программирования Java вкладываемыми record-шаблонами.
Грамматика шаблонов становится такой:
Pattern:
TypePattern
RecordPattern
TypePattern:
LocalVariableDeclaration
RecordPattern:
ReferenceType ( [ PatternList ] )
PatternList :
Pattern { , Pattern }
Record Patterns
Record-шаблон состоит из типа record-класса и (возможно, пустого) списка шаблонов, который используется для сопоставления с соответствующими значениями компонентов записи.
Например, для объявления
record Point(int i, int j) {}
значение v совпадает с record-шаблоном Point(int i, int j), если оно является экземпляром record-типа Point; в этом случае переменная шаблона i инициализируется результатом вызова на значении v метода доступа, соответствующего i, а переменная шаблона j — результатом вызова на значении v метода доступа, соответствующего j. (Имена переменных шаблона не обязаны совпадать с именами компонентов записи; то есть record-шаблон Point(int x, int y) действует точно так же, за исключением того, что инициализируются переменные шаблона x и y.)
Значение null не совпадает ни с одним record-шаблоном.
В record-шаблоне можно использовать var для сопоставления с компонентом записи без указания типа компонента. В этом случае компилятор выводит тип переменной шаблона, вводимой шаблоном var. Например, шаблон Point(var a, var b) — это сокращённая запись шаблона Point(int a, int b).
Множество переменных шаблона, объявленных record-шаблоном, включает все переменные шаблона, объявленные в списке шаблонов.
Выражение совместимо с record-шаблоном, если его можно привести к record-типу из шаблона без непроверяемого преобразования (unchecked conversion).
Если record-шаблон называет обобщённый record-класс, но не задаёт аргументы типа (то есть в record-шаблоне используется сырой тип), аргументы типа всегда выводятся. Например:
// As of Java 21
record MyPair<S,T>(S fst, T snd){};
static void recordInference(MyPair<String, Integer> pair){
switch (pair) {
case MyPair(var f, var s) ->
... // Inferred record pattern MyPair<String,Integer>(var f, var s)
...
}
}
Вывод аргументов типа для record-шаблонов поддерживается во всех конструкциях, которые поддерживают record-шаблоны, а именно в выражениях instanceof и в операторах и выражениях switch.
Вывод работает и с вложенными record-шаблонами, например:
// As of Java 21
record Box<T>(T t) {}
static void test1(Box<Box<String>> bbs) {
if (bbs instanceof Box<Box<String>>(Box(var s))) {
System.out.println("String " + s);
}
}
Здесь аргумент типа для вложенного шаблона Box(var s) выводится как String, поэтому сам шаблон выводится как Box<String>(var s).
На самом деле аргументы типа можно опустить и во внешнем record-шаблоне, что даёт краткий код:
// As of Java 21
static void test2(Box<Box<String>> bbs) {
if (bbs instanceof Box(Box(var s))) {
System.out.println("String " + s);
}
}
Здесь компилятор выведет, что весь шаблон instanceof — это Box<Box<String>>(Box<String>(var s)),
Для совместимости шаблоны типа не поддерживают неявный вывод аргументов типа; например, шаблон типа List l всегда рассматривается как шаблон сырого типа.
Record-шаблоны и исчерпывающий switch
JEP 441 расширяет и выражения switch, и операторы switch, добавляя поддержку меток-шаблонов. И выражения 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:
// As of Java 21
switch (p1) { // Error!
case Pair<A>(A a, B b) -> ...
case Pair<A>(B b, A a) -> ...
}
Эти два switch являются исчерпывающими, поскольку интерфейс I является sealed, и поэтому типы C и D охватывают все возможные экземпляры:
// As of Java 21
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:
// As of Java 21
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-шаблоны можно расширять во многих направлениях:
- шаблоны с переменным числом аргументов (varargs) для записей переменной арности;
- безымянные шаблоны, которые могут находиться в списках шаблонов record-шаблонов, совпадают с любым значением, но не объявляют переменных шаблона; и
- шаблоны, применимые к значениям произвольных классов, а не только record-классов.
Некоторые из них мы можем рассмотреть в будущих JEP.
Зависимости
Этот JEP основан на Pattern Matching for instanceof (JEP 394), вошедшем в JDK 16. Он развивался совместно с Pattern Matching for switch (JEP 441).