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

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.