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

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.