openjdk.ruOpenJDK на русском

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).