JEP 394: Pattern Matching for instanceof
Pattern Matching (сопоставление с образцом) для instanceof
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 16 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Связан с | JEP 305: Pattern Matching for instanceof (Preview) |
| JEP 375: Pattern Matching for instanceof (Second Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz, Maurizio Cimadamore |
| Одобрен | Brian Goetz |
| Создан | 2020/07/27 13:05 |
| Обновлён | 2022/06/10 16:12 |
| Задача | 8250623 |
Аннотация
Расширить язык программирования Java возможностью Pattern Matching для оператора instanceof. Pattern Matching позволяет выражать распространённую в программах логику, а именно условное извлечение компонентов из объектов, более кратко и безопасно.
История
Pattern Matching для instanceof был предложен в JEP 305 и включён в JDK 14 в статусе Preview (предварительная версия). Он был повторно предложен в JEP 375 и включён в JDK 15 для второго раунда Preview.
Этот JEP предлагает сделать эту возможность окончательной в JDK 16 со следующими доработками:
-
Снять ограничение, согласно которому переменные шаблона неявно являются final, чтобы уменьшить асимметрию между локальными переменными и переменными шаблона.
-
Сделать ошибкой компиляции ситуацию, когда выражение
instanceofс шаблоном сравнивает выражение типа S с шаблоном типа T, где S — подтип T. (Такое выражениеinstanceofвсегда будет успешным и поэтому бессмысленно. Противоположный случай, когда проверка на соответствие шаблону всегда будет неуспешной, уже является ошибкой компиляции.)
По результатам дальнейших отзывов могут быть внесены и другие доработки.
Мотивация
Почти любая программа содержит логику, которая сочетает проверку того, имеет ли выражение определённый тип или структуру, с последующим условным извлечением компонентов его состояния для дальнейшей обработки. Например, всем Java-программистам знакома идиома «instanceof и приведение типа»:
if (obj instanceof String) {
String s = (String) obj; // grr...
...
}
Здесь происходят три вещи: проверка (является ли obj значением типа String?), преобразование (приведение obj к String) и объявление новой локальной переменной (s), чтобы мы могли использовать строковое значение. Этот приём прост и понятен всем Java-программистам, но неоптимален по нескольким причинам. Он утомителен: выполнять и проверку типа, и приведение не должно быть нужно (что ещё делать после проверки instanceof?). Этот шаблонный код — в частности, три упоминания типа String — затемняет более важную логику, которая следует дальше. Но главное, повторение даёт ошибкам возможность незаметно проникнуть в программы.
Вместо того чтобы искать частные решения, мы считаем, что Java пора принять Pattern Matching. Pattern Matching позволяет кратко выразить желаемую «форму» объекта (шаблон), а различным операторам и выражениям — проверять эту «форму» на своих входных данных (сопоставление). Многие языки, от Haskell до C#, приняли Pattern Matching за его краткость и безопасность.
Описание
Шаблон — это сочетание (1) предиката, или проверки, который можно применить к цели, и (2) набора локальных переменных, называемых переменными шаблона, которые извлекаются из цели, только если предикат успешно к ней применяется.
Шаблон типа состоит из предиката, задающего тип, и одной переменной шаблона.
Оператор instanceof (JLS 15.20.2) расширен: теперь он принимает шаблон типа, а не только тип.
Это позволяет нам переписать утомительный код выше так:
if (obj instanceof String s) {
// Let pattern matching do the work!
...
}
(В этом коде шаблоном типа является фраза String s.) Смысл интуитивно понятен. Оператор instanceof сопоставляет цель obj с шаблоном типа следующим образом: если obj — экземпляр String, то он приводится к String, и значение присваивается переменной s.
Условность Pattern Matching — если значение не соответствует шаблону, переменной шаблона значение не присваивается — означает, что нужно тщательно продумать область видимости переменной шаблона. Можно было бы поступить просто и сказать, что область видимости переменной шаблона — это содержащий её оператор и все последующие операторы в объемлющем блоке. Но это приводит к неприятным последствиям — отравлению, например:
if (a instanceof Point p) {
...
}
if (b instanceof Point p) { // ERROR - p is in scope
...
}
Другими словами, ко второму оператору переменная шаблона p оказалась бы в отравленном состоянии: она находится в области видимости, но не должна быть доступна, поскольку ей может быть не присвоено значение. Но хотя обращаться к ней нельзя, раз она находится в области видимости, мы не можем просто объявить её заново. Это значит, что переменная шаблона может стать отравленной после объявления, и программистам пришлось бы придумывать множество разных имён для своих переменных шаблона.
Вместо грубого приближения области видимости переменные шаблона используют концепцию flow scoping (область видимости, определяемая потоком управления). Переменная шаблона находится в области видимости только там, где компилятор может сделать вывод, что шаблон точно совпал и переменной было присвоено значение. Этот анализ чувствителен к потоку управления и работает аналогично существующим видам анализа потока, например анализу определённого присваивания. Вернёмся к нашему примеру:
if (a instanceof Point p) {
// p is in scope
...
}
// p not in scope here
if (b instanceof Point p) { // Sure!
...
}
Девиз такой: «Переменная шаблона находится в области видимости там, где она точно совпала». Это позволяет безопасно повторно использовать переменные шаблона, и такой подход одновременно интуитивен и привычен, поскольку Java-разработчики уже привыкли к анализу, чувствительному к потоку управления.
Когда условное выражение оператора if становится сложнее одного instanceof, область видимости переменной шаблона соответственно расширяется. Например, в этом коде:
if (obj instanceof String s && s.length() > 5) {
flag = s.contains("jdk");
}
переменная шаблона s находится в области видимости в правой части оператора &&, а также в блоке для истинного условия. (Правая часть оператора && вычисляется, только если проверка на соответствие шаблону прошла успешно и присвоила значение s.) С другой стороны, следующий код не компилируется:
if (obj instanceof String s || s.length() > 5) { // Error!
...
}
Из-за семантики оператора || переменной шаблона s могло не быть присвоено значение, поэтому анализ потока управления определяет, что переменная s не находится в области видимости в правой части оператора ||.
Использование Pattern Matching в instanceof должно значительно сократить общее число явных приведений типов в программах на Java. Шаблоны проверки типа особенно полезны при написании методов сравнения на равенство. Рассмотрим следующий метод сравнения на равенство из пункта 10 книги Effective Java:
public final boolean equals(Object o) {
return (o instanceof CaseInsensitiveString) &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
С шаблоном типа его можно переписать понятнее:
public final boolean equals(Object o) {
return (o instanceof CaseInsensitiveString cis) &&
cis.s.equalsIgnoreCase(s);
}
Другие методы equals улучшаются ещё заметнее. Рассмотрим класс Point из примера выше, для которого мы могли бы написать метод equals так:
public final boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point other = (Point) o;
return x == other.x
&& y == other.y;
}
Если же использовать Pattern Matching, эти несколько операторов можно объединить в одно выражение, убрав повторы и упростив поток управления:
public final boolean equals(Object o) {
return (o instanceof Point other)
&& x == other.x
&& y == other.y;
}
Анализ flow scoping для переменных шаблона учитывает, может ли оператор завершиться нормально. Например, рассмотрим следующий метод:
public void onlyForStrings(Object o) throws MyException {
if (!(o instanceof String s))
throw new MyException();
// s is in scope
System.out.println(s);
...
}
Этот метод проверяет, является ли его параметр o значением типа String, и если нет, выбрасывает исключение. До оператора println можно дойти, только если условный оператор завершился нормально. Поскольку вложенный оператор условного оператора никогда не может завершиться нормально, это возможно, только если условное выражение вычислилось в значение false, что, в свою очередь, означает, что Pattern Matching прошёл успешно. Соответственно, область видимости переменной шаблона s безопасно включает операторы, следующие за условным оператором в блоке метода.
Переменные шаблона — всего лишь частный случай локальных переменных, и, если не считать определения их области видимости, во всех остальных отношениях переменные шаблона рассматриваются как локальные переменные. В частности, это означает, что (1) им можно присваивать значения и (2) они могут затенять объявление поля. Например:
class Example1 {
String s;
void test1(Object o) {
if (o instanceof String s) {
System.out.println(s); // Field s is shadowed
s = s + "\n"; // Assignment to pattern variable
...
}
System.out.println(s); // Refers to field s
...
}
}
Однако из-за того, что переменные шаблона подчиняются flow scoping, нужно внимательно определять, ссылается ли имя на объявление переменной шаблона, затеняющее объявление поля, или на само объявление поля.
class Example2 {
Point p;
void test2(Object o) {
if (o instanceof Point p) {
// p refers to the pattern variable
...
} else {
// p refers to the field
...
}
}
}
Грамматика instanceof соответственно расширена:
RelationalExpression:
...
RelationalExpression instanceof ReferenceType
RelationalExpression instanceof Pattern
Pattern:
ReferenceType Identifier
Дальнейшая работа
В будущих JEP язык программирования Java будет расширен более богатыми формами шаблонов, такими как шаблоны деконструкции для классов Records (записи), а также Pattern Matching для других конструкций языка, таких как выражения и операторы switch.
Альтернативы
Преимущества шаблонов типа можно было бы получить с помощью flow typing (уточнение типа по потоку управления) в операторах if или с помощью конструкции type switch. Pattern Matching обобщает обе эти конструкции.