JEP 420: Pattern Matching for switch (Second Preview)
Pattern Matching (сопоставление с образцом) для switch, вторая версия Preview (предварительная версия)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 18 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Связан с | JEP 406: Pattern Matching for switch (Preview) |
| JEP 427: Pattern Matching for switch (Third Preview) | |
| Рецензенты | Brian Goetz, Maurizio Cimadamore |
| Создан | 2021/09/03 13:00 |
| Обновлён | 2023/05/12 15:34 |
| Задача | 8273326 |
Аннотация
Дополнить язык программирования Java механизмом Pattern Matching для выражений и операторов switch, а также расширить язык шаблонов. Распространение Pattern Matching на switch позволяет проверять выражение на соответствие нескольким шаблонам, каждому из которых соответствует своё действие, поэтому сложные запросы, ориентированные на данные, можно записывать кратко и безопасно. Это возможность языка в статусе Preview в JDK 18.
История
Pattern Matching для switch был предложен в JEP 406 и вошёл в JDK 17 как Preview-возможность. Этот JEP предлагает вторую версию Preview этой возможности в JDK 18 с небольшими доработками на основе опыта и отзывов.
Улучшения по сравнению с первой версией Preview:
-
Проверка доминирования теперь требует, чтобы для удобства чтения метка case с константой стояла перед guarded pattern того же типа (см. 1b ниже); и
-
Проверка полноты блоков switch теперь работает точнее с sealed-иерархиями, в которых разрешённый прямой подкласс лишь расширяет конкретизацию (обобщённого) суперкласса
sealed(см. 2 ниже).
Цели
-
Расширить выразительность и применимость выражений и операторов
switch, разрешив использовать шаблоны в меткахcase. -
Позволить при необходимости ослабить исторически сложившуюся нетерпимость
switchк null. -
Ввести два новых вида шаблонов: guarded patterns, чтобы логику Pattern Matching можно было уточнять произвольными булевыми выражениями, и parenthesized patterns, чтобы устранить некоторые неоднозначности синтаксического разбора.
-
Обеспечить, чтобы все существующие выражения и операторы
switchпродолжали компилироваться без изменений и выполнялись с идентичной семантикой. -
Не вводить новое выражение или оператор, похожий на
switch, с семантикой Pattern Matching, отдельный от традиционной конструкцииswitch. -
Не делать так, чтобы выражение или оператор
switchвели себя по-разному в зависимости от того, являются ли метки case шаблонами или традиционными константами.
Мотивация
В Java 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, с несколькими альтернативами. Java поддерживает многовариантные сравнения с помощью операторов switch и, начиная с Java 14, выражений switch (JEP 361), но, к сожалению, возможности switch очень ограничены. Выполнять switch можно только по значениям нескольких типов — числовых типов, типов enum и String, — и проверять можно только точное равенство константам. Хотелось бы с помощью шаблонов проверять одну и ту же переменную на соответствие нескольким вариантам и выполнять для каждого своё действие, но существующий switch этого не поддерживает, и в итоге получается цепочка проверок if...else вроде такой:
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
Этот код выигрывает от использования выражений instanceof с шаблонами, но он далёк от совершенства. Прежде всего, такой подход позволяет ошибкам в коде оставаться незамеченными, потому что мы использовали слишком общую управляющую конструкцию. Замысел в том, чтобы в каждой ветви цепочки if...else присваивать что-то переменной formatted, но ничто не позволяет компилятору распознать и проверить этот инвариант. Если какой-то блок — возможно, выполняющийся редко — не присваивает значение formatted, в коде ошибка. (Если объявить formatted как локальную переменную без инициализатора, к этому хотя бы подключился бы анализ определённого присваивания в компиляторе, но такие объявления пишут не всегда.) Кроме того, приведённый код не поддаётся оптимизации: без героических усилий компилятора его временная сложность будет O(n), хотя исходная задача часто имеет сложность O(1).
Но switch идеально подходит для Pattern Matching! Если расширить операторы и выражения switch так, чтобы они работали с любым типом, и разрешить метки case с шаблонами, а не только с константами, то приведённый код можно было бы переписать понятнее и надёжнее:
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
Семантика этого switch понятна: метка case с шаблоном соответствует значению выражения-селектора o, если это значение соответствует шаблону. (Для краткости мы показали выражение switch, но могли бы показать и оператор switch; блок switch, включая метки case, остался бы тем же.)
Замысел этого кода понятнее, потому что мы используем подходящую управляющую конструкцию. Мы говорим: «параметр o соответствует не более чем одному из следующих условий; определи, какому, и вычисли соответствующую ветвь». Вдобавок этот код поддаётся оптимизации: в данном случае мы, скорее всего, сможем выполнить диспетчеризацию за время O(1).
Pattern Matching и null
Традиционно операторы и выражения switch выбрасывают NullPointerException, если выражение-селектор вычисляется в null, поэтому проверку на null приходится выполнять вне switch:
static void testFooBar(String s) {
if (s == null) {
System.out.println("oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
Это было разумно, пока switch поддерживал лишь несколько ссылочных типов. Однако если switch допускает выражение-селектор любого типа, а метки case могут содержать шаблоны типов, то отдельная проверка на null выглядит произвольным разграничением, порождает лишний однотипный код и создаёт возможность для ошибок. Лучше было бы встроить проверку на null в switch:
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
Поведение switch, когда значение выражения-селектора равно null, всегда определяется его метками case. При наличии case null (или тотального шаблона типа; см. 4a ниже) switch выполняет код, связанный с этой меткой; без case null switch выбрасывает NullPointerException, как и раньше. (Чтобы сохранить обратную совместимость с текущей семантикой switch, метка default не соответствует селектору, равному null.)
Может понадобиться обрабатывать null так же, как другую метку case. Например, в следующем коде case null, String s будет соответствовать и значению null, и всем значениям String:
static void testStringOrNull(Object o) {
switch (o) {
case null, String s -> System.out.println("String: " + s);
}
}
Уточнение шаблонов в switch
Эксперименты с шаблонами в switch показывают, что шаблоны часто требуется уточнять. Рассмотрим следующий код, выполняющий switch по значению Shape:
class Shape {}
class Rectangle extends Shape {}
class Triangle extends Shape { int calculateArea() { ... } }
static void testTriangle(Shape s) {
switch (s) {
case null:
break;
case Triangle t:
if (t.calculateArea() > 100) {
System.out.println("Large triangle");
break;
}
default:
System.out.println("A shape, possibly a small triangle");
}
}
Замысел этого кода — выделить особый случай для больших треугольников (площадью больше 100) и случай по умолчанию для всего остального (включая маленькие треугольники). Однако выразить это напрямую одним шаблоном нельзя. Сначала приходится написать метку case, соответствующую всем треугольникам, а затем довольно неуклюже поместить проверку площади треугольника в соответствующую группу операторов. Затем приходится использовать проваливание, чтобы получить правильное поведение, когда площадь треугольника меньше 100. (Обратите внимание на аккуратное размещение break; внутри блока if.)
Проблема в том, что различение случаев с помощью одного шаблона не масштабируется за пределы одного условия. Нужен способ выразить уточнение шаблона. Один из подходов — разрешить уточнять метки case; в других языках программирования такое уточнение называется охранным условием. Например, можно ввести новое ключевое слово where, которое ставится в конце метки case и за которым следует булево выражение, например case Triangle t where t.calculateArea() > 100.
Однако есть и другой подход: вместо того чтобы расширять функциональность меток case, можно расширить сам язык шаблонов. Можно добавить новый вид шаблона — guarded pattern, записываемый как p && b, — который позволяет уточнить шаблон p произвольным булевым выражением b.
При таком подходе можно вернуться к коду с testTriangle и выразить особый случай для больших треугольников напрямую. Это избавляет от проваливания в операторе switch, а значит, можно пользоваться лаконичными правилами со стрелкой (->):
static void testTriangle(Shape s) {
switch (s) {
case Triangle t && (t.calculateArea() > 100) ->
System.out.println("Large triangle");
default ->
System.out.println("A shape, possibly a small triangle");
}
}
Значение s соответствует шаблону Triangle t && (t.calculateArea() > 100), если, во-первых, оно соответствует шаблону типа Triangle t и, если это так, выражение t.calculateArea() > 100 вычисляется в true.
Благодаря switch метки case легко понимать и изменять, когда меняются требования к приложению. Например, может понадобиться вывести треугольники из ветви по умолчанию; это можно сделать, используя и уточнённый, и неуточнённый шаблон:
static void testTriangle(Shape s) {
switch (s) {
case Triangle t && (t.calculateArea() > 100) ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
Описание
Мы улучшаем операторы и выражения switch двумя способами:
-
расширяем метки
case, чтобы помимо констант они могли содержать шаблоны, и -
вводим два новых вида шаблонов: guarded patterns и parenthesized patterns.
Шаблоны в метках switch
Суть предложения — ввести новую метку switch case p, где p — шаблон. Сущность switch не меняется: значение выражения-селектора сравнивается с метками switch, выбирается одна из меток и выполняется связанный с ней код. Разница в том, что теперь для меток case с шаблонами этот выбор определяется с помощью Pattern Matching, а не проверкой на равенство. Например, в следующем коде значение o соответствует шаблону Long l, и будет выполнен код, связанный с case Long l:
Object o = 123L;
String formatted = switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
Когда метки case могут содержать шаблоны, возникают четыре основных вопроса проектирования:
- Улучшенная проверка типов
- Полнота выражений и операторов
switch - Область видимости объявлений переменных шаблона
- Обработка
null
1. Улучшенная проверка типов
1a. Типизация выражения-селектора
Поддержка шаблонов в switch означает, что можно ослабить текущие ограничения на тип выражения-селектора. Сейчас тип выражения-селектора обычного switch должен быть либо целочисленным примитивным типом (char, byte, short или int), либо соответствующей упакованной формой (Character, Byte, Short или Integer), либо String, либо типом enum. Мы расширяем это правило и требуем, чтобы тип выражения-селектора был либо целочисленным примитивным типом, либо любым ссылочным типом.
Например, в следующем switch с шаблонами выражение-селектор o сопоставляется с шаблонами типов, в которых участвуют тип класса, тип enum, record-тип и тип массива (а также метка null case и default):
record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }
static void typeTester(Object o) {
switch (o) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color with " + c.values().length + " values");
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of ints of length" + ia.length);
default -> System.out.println("Something else");
}
}
Каждая метка case в блоке switch должна быть совместима с выражением-селектором. Для метки case с шаблоном, которая называется меткой шаблона, мы используем существующее понятие совместимости выражения с шаблоном (JLS §14.30.1).
1b. Доминирование меток шаблонов
Выражение-селектор может соответствовать нескольким меткам в блоке switch. Рассмотрим такой проблемный пример:
static void error(Object o) {
switch(o) {
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
case String s -> // Error - pattern is dominated by previous pattern
System.out.println("A string: " + s);
default -> {
break;
}
}
}
Первая метка шаблона case CharSequence cs доминирует над второй меткой шаблона case String s, потому что каждое значение, соответствующее шаблону String s, соответствует и шаблону CharSequence cs, но не наоборот. Это так, потому что тип второго шаблона, String, является подтипом типа первого шаблона, CharSequence.
Метка шаблона вида case p, где p — тотальный шаблон для типа выражения-селектора, доминирует над меткой case null. Это так, потому что тотальный шаблон соответствует всем значениям, включая null.
Метка шаблона вида case p доминирует над меткой шаблона вида case p && e, т. е. такой, где шаблон является вариантом исходного шаблона с охранным условием. Например, метка шаблона case String s доминирует над меткой шаблона case String s && s.length() > 0, поскольку каждое значение, соответствующее guarded pattern String s && s.length() > 0, соответствует и шаблону String s.
Метка шаблона может доминировать над меткой константы. Например, метка шаблона case Integer i доминирует над меткой константы case 42, а метка шаблона case E e доминирует над меткой константы case A, если A — константа перечисления типа класса-перечисления E. Метка шаблона с охранным условием доминирует над меткой константы, если над ней доминирует содержащаяся в ней метка; охранное выражение мы не проверяем, поскольку в общем случае это неразрешимая задача. Поэтому метка шаблона с охранным условием case String s && s.length()>1, как и ожидается, доминирует над меткой константы case "hello", но и case Integer i && i <> 0 тоже доминирует над меткой case 0. Отсюда следует простой и понятный порядок меток case: метки констант должны идти перед метками шаблонов с охранным условием, а те — перед метками шаблонов типа без охранного условия:
switch(o) {
case -1, 1 -> ... // Special cases
case Integer i && i > 0 -> ... // Positive integer cases
case Integer i -> ... // All the remaining integers
default ->
}
Компилятор проверяет все метки. Если над меткой в блоке switch доминирует более ранняя метка в том же блоке switch, это ошибка компиляции. Благодаря этому требованию доминирования метки case с шаблонами типа, если в блоке switch есть только они, будут идти в порядке подтипов.
(Понятие доминирования аналогично условиям для предложений catch оператора try: ошибкой считается, если предложению catch, которое перехватывает класс исключения E, предшествует предложение catch, способное перехватить E или суперкласс E (JLS §11.2.3). Логически предшествующее предложение catch доминирует над последующим предложением catch.)
Ошибкой компиляции также считается, если в блоке switch больше одной метки switch, соответствующей всем значениям. Две такие универсальные метки — это default и тотальные шаблоны типа (см. 4a ниже).
2. Исчерпываемость выражений и операторов switch
Выражение switch требует, чтобы блок switch обрабатывал все возможные значения выражения-селектора; другими словами, оно должно быть исчерпывающим. Так сохраняется свойство, что успешное вычисление выражения switch всегда даёт значение. Для обычных выражений switch это обеспечивается довольно простым набором дополнительных условий на блок switch. Для выражений switch с шаблонами мы добиваемся этого, вводя понятие покрытия типов для меток switch в блоке switch. Затем покрытия типов всех меток switch в блоке switch объединяются, чтобы определить, исчерпывает ли блок switch все возможности выражения-селектора.
Рассмотрим такое (ошибочное) выражение switch с шаблонами:
static int coverage(Object o) {
return switch (o) { // Error - not exhaustive
case String s -> s.length();
};
}
В блоке switch только одна метка case — case String s. Ей соответствует любое значение выражения-селектора, тип которого является подтипом String. Поэтому мы говорим, что покрытие типов этой метки switch — каждый подтип String. Это выражение switch с шаблонами не является исчерпывающим, потому что покрытие типов его блока switch (все подтипы String) не включает тип выражения-селектора (Object).
Рассмотрим такой (всё ещё ошибочный) пример:
static int coverage(Object o) {
return switch (o) { // Error - not exhaustive
case String s -> s.length();
case Integer i -> i;
};
}
Покрытие типов этого блока switch — объединение покрытий двух его меток switch. Другими словами, покрытие типов — это множество всех подтипов String и множество всех подтипов Integer. Но покрытие типов снова не включает тип выражения-селектора, поэтому это выражение switch с шаблонами тоже не является исчерпывающим и вызывает ошибку компиляции.
Покрытие типов метки default — все типы, поэтому этот пример (наконец-то!) допустим:
static int coverage(Object o) {
return switch (o) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
Если тип выражения-селектора относится к Sealed Classes (запечатанные классы; JEP 409), то при проверке покрытия типов можно учесть предложение permits sealed-класса, чтобы определить, является ли блок switch исчерпывающим. Иногда благодаря этому предложение default становится не нужно. Рассмотрим следующий пример интерфейса sealed S с тремя разрешёнными подклассами A, B и C:
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // Implicitly final
static int testSealedExhaustive(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}
Компилятор может определить, что покрытие типов блока switch — это типы A, B и C. Поскольку тип выражения-селектора, S, — sealed-интерфейс, разрешённые подклассы которого — ровно A, B и C, этот блок switch является исчерпывающим. Поэтому метка default не нужна.
Нужна дополнительная осторожность, когда разрешённый прямой подкласс реализует только конкретизацию (обобщённого) суперкласса sealed. Например:
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
static int testGenericSealedExhaustive(I<Integer> i) {
return switch (i) {
// Exhaustive as no A case possible!
case B<Integer> bi -> 42;
}
}
Единственные разрешённые подклассы I — это A и B, но компилятор может определить, что для исчерпываемости блоку switch достаточно покрыть класс B, поскольку выражение-селектор имеет тип I<Integer>.
Для защиты от несовместимой раздельной компиляции компилятор автоматически добавляет метку default, код которой выбрасывает IncompatibleClassChangeError. Управление дойдёт до этой метки, только если интерфейс sealed изменится, а код switch не будет перекомпилирован. По сути, компилятор укрепляет ваш код за вас.
(Требование исчерпываемости для выражения switch с шаблонами аналогично обработке выражения switch, выражение-селектор которого имеет тип класса-перечисления: там предложение default не требуется, если есть предложение для каждой константы класса-перечисления.)
Проверка компилятором того, что выражения switch являются исчерпывающими, крайне полезна. Вместо того чтобы оставлять эту проверку только для выражений switch, мы распространяем её и на операторы switch. Для обратной совместимости все существующие операторы switch будут компилироваться без изменений. Но если оператор switch использует любую из новых возможностей, описанных в этом JEP, компилятор проверит, что он является исчерпывающим.
Точнее, исчерпываемость требуется от любого оператора switch, который использует метки шаблонов или null либо выражение-селектор которого не относится к унаследованным типам (char, byte, short, int, Character, Byte, Short, Integer, String или тип перечисления).
Это значит, что теперь и выражения switch, и операторы switch получают преимущества более строгой проверки типов. Например:
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // Implicitly final
static void switchStatementExhaustive(S s) {
switch (s) { // Error - not exhaustive; missing clause for permitted class B!
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
Чтобы сделать большинство операторов switch исчерпывающими, достаточно добавить простое предложение default в конце блока switch. Так код становится понятнее, и его легче проверить. Например, следующий оператор switch не является исчерпывающим и ошибочен:
Object o = ...
switch (o) { // Error - not exhaustive!
case String s:
System.out.println(s);
break;
case Integer i:
System.out.println("Integer");
break;
}
Сделать его исчерпывающим тривиально:
Object o = ...
switch (o) {
case String s:
System.out.println(s);
break;
case Integer i:
System.out.println("Integer");
break;
default: // Now exhaustive!
break;
}
(Будущие компиляторы языка Java могут выдавать предупреждения для унаследованных операторов switch, которые не являются исчерпывающими.)
3. Область видимости объявлений переменных шаблона
Переменные шаблона (JEP 394) — это локальные переменные, объявляемые шаблонами. Объявления переменных шаблона необычны тем, что их область видимости зависит от потока управления. Напомним на следующем примере, где шаблон типа String s объявляет переменную шаблона s:
static void test(Object o) {
if ((o instanceof String s) && s.length() > 3) {
System.out.println(s);
} else {
System.out.println("Not a string");
}
}
Объявление s находится в области видимости в правом операнде выражения &&, а также в блоке «then». Однако в блоке «else» оно вне области видимости: чтобы управление перешло в блок «else», сопоставление с шаблоном должно завершиться неудачей, и в этом случае переменная шаблона не будет инициализирована.
Мы расширяем это зависящее от потока управления понятие области видимости для объявлений переменных шаблона на объявления шаблонов в метках case с помощью двух новых правил:
-
Область видимости объявления переменной шаблона, находящегося в метке
caseправилаswitch, включает выражение, блок или операторthrow, стоящие справа от стрелки. -
Область видимости объявления переменной шаблона, находящегося в метке
caseгруппы операторов с меткойswitch, включает операторы блока этой группы операторов. Проваливание через меткуcase, объявляющую переменную шаблона, должно быть невозможно.
Этот пример показывает первое правило в действии:
static void test(Object o) {
switch (o) {
case Character c -> {
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character");
}
case Integer i ->
throw new IllegalStateException("Invalid Integer argument of value " + i.intValue());
default -> {
break;
}
}
}
Область видимости объявления переменной шаблона c — блок справа от первой стрелки.
Область видимости объявления переменной шаблона i — оператор throw справа от второй стрелки.
Второе правило сложнее. Сначала рассмотрим пример, где у группы операторов с меткой switch только одна метка case:
static void test(Object o) {
switch (o) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
default:
System.out.println();
}
}
Область видимости объявления переменной шаблона c включает все операторы группы, а именно два оператора if и оператор println. Область видимости не включает операторы группы default, хотя выполнение первой группы операторов может провалиться через метку switch default и выполнить эти операторы.
Возможность проваливания через метку case, объявляющую переменную шаблона, должна исключаться как ошибка компиляции. Рассмотрим этот ошибочный пример:
static void test(Object o) {
switch (o) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
case Integer i: // Compile-time error
System.out.println("An integer " + i);
default:
break;
}
}
Если бы это было разрешено и значение выражения-селектора o было бы Character, то выполнение блока switch могло бы провалиться во вторую группу операторов (после case Integer i:), где переменная шаблона i не была бы инициализирована. Поэтому допущение проваливания выполнения через метку case, объявляющую переменную шаблона, является ошибкой компиляции.
Именно поэтому case Character c: case Integer i: ... не разрешено. Аналогичные рассуждения объясняют запрет нескольких шаблонов в метке case: не допускаются ни case Character c, Integer i: ..., ни case Character c, Integer i -> .... Если бы такие метки case были разрешены, то после двоеточия или стрелки в области видимости были бы и c, и i, но инициализирована была бы только одна из них — в зависимости от того, было ли значение o типа Character или Integer.
С другой стороны, проваливание через метку, которая не объявляет переменную шаблона, безопасно, как показывает этот пример:
void test(Object o) {
switch (o) {
case String s:
System.out.println("A string");
default:
System.out.println("Done");
}
}
4. Работа с null
4a. Сопоставление null
Традиционно switch выбрасывает NullPointerException, если выражение-селектор вычисляется в null. Это хорошо понятное поведение, и мы не предлагаем менять его для существующего кода с switch.
Однако, поскольку для Pattern Matching и значений null существует разумная семантика, не порождающая исключений, появляется возможность сделать switch с шаблонами более дружественным к null, сохранив совместимость с существующей семантикой switch.
Во-первых, мы вводим для case новую метку null, которая срабатывает, когда значение выражения-селектора равно null.
Во-вторых, мы замечаем, что если в метке case с шаблоном стоит шаблон, тотальный для типа выражения-селектора, то эта метка также сработает, когда значение выражения-селектора равно null. (Шаблон типа p с типом U является тотальным для типа T, если T — подтип U. Например, шаблон типа Object o тотален для типа String.)
Мы отменяем общее правило, по которому switch сразу выбрасывает NullPointerException, если значение выражения-селектора равно null. Вместо этого мы анализируем метки case, чтобы определить поведение switch:
-
Если выражение-селектор вычисляется в
null, то считается, что срабатывает любая метка casenullили метка case с тотальным шаблоном. Если в блоке switch такой метки нет, тоswitch, как и раньше, выбрасываетNullPointerException. -
Если выражение-селектор вычисляется в значение, отличное от
null, мы, как обычно, выбираем подходящую меткуcase. Если ни одна меткаcaseне подходит, считается, что срабатывает любая универсальная метка.
Например, при объявлении ниже вычисление test(null) выведет null!, а не выбросит NullPointerException:
static void test(Object o) {
switch (o) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}
Новое поведение в отношении null выглядит так, как будто компилятор автоматически дополняет блок switch меткой case null, тело которой выбрасывает NullPointerException. Иными словами, этот код:
static void test(Object o) {
switch (o) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
эквивалентен следующему:
static void test(Object o) {
switch (o) {
case null -> throw new NullPointerException();
case String s -> System.out.println("String: "+s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
В обоих примерах вычисление test(null) приведёт к выбрасыванию NullPointerException.
Мы сохраняем интуитивное представление, заложенное в существующей конструкции switch: выполнять switch по null — исключительная ситуация. Отличие шаблонного switch в том, что у вас есть механизм, позволяющий обработать этот случай прямо внутри switch, а не снаружи. Если вы решите не включать в блок switch метку case, срабатывающую на null, то switch по значению null, как и раньше, выбросит NullPointerException.
4b. Новые формы меток, возникающие из меток null
Блоки switch в Java 16 поддерживают два стиля: один основан на помеченных группах инструкций (форма :), где возможен проход в следующую ветку (fall-through), а другой — на форме с единственным следствием (форма ->), где такой проход невозможен. В первом стиле несколько меток обычно записываются как case l1: case l2:, а во втором — как case l1, l2 ->.
Поддержка меток null означает, что ряд особых случаев можно выразить в форме :. Например:
Object o = ...
switch(o) {
case null: case String s:
System.out.println("String, including null");
break;
...
}
Разработчики обоснованно ожидают, что формы : и -> одинаково выразительны и что если в первом стиле поддерживается case A: case B:, то во втором должно поддерживаться case A, B ->. Следовательно, предыдущий пример говорит о том, что нам следует поддержать метку case null, String s ->, как показано ниже:
Object o = ...
switch(o) {
case null, String s -> System.out.println("String, including null");
...
}
Значение o соответствует этой метке, если оно является либо null-ссылкой, либо String. В обоих случаях переменная шаблона s инициализируется значением o.
(Обратная форма, case String s, null, также допускается и ведёт себя точно так же.)
Также имеет смысл и встречается нередко объединение случая null с меткой default, т. е.
Object o = ...
switch(o) {
...
case null: default:
System.out.println("The rest (including null)");
}
Это тоже должно поддерживаться в форме ->. Для этого мы вводим новую метку case default:
Object o = ...
switch(o) {
...
case null, default ->
System.out.println("The rest (including null)");
}
Значение o соответствует этой метке, если оно является null-ссылкой или если не срабатывает ни одна другая метка.
Guarded-шаблоны и шаблоны в скобках
После успешного сопоставления с шаблоном мы часто дополнительно проверяем его результат. Это может приводить к громоздкому коду, например:
static void test(Object o) {
switch (o) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
Нужная проверка — что o является String длины 1 — к сожалению, разделена между меткой case и следующей за ней инструкцией if. Читаемость можно было бы улучшить, если бы шаблонный switch поддерживал сочетание шаблона и логического выражения в метке case.
Вместо того чтобы добавлять ещё одну специальную метку case, мы расширяем язык шаблонов, добавляя guarded-шаблоны (шаблоны с охранным условием), которые записываются как p && e. Так приведённый выше код можно переписать, перенеся всю условную логику в метку case:
static void test(Object o) {
switch (o) {
case String s && (s.length() == 1) -> ...
case String s -> ...
...
}
}
Первый case срабатывает, если o является String и имеет длину 1. Второй case срабатывает, если o является String какой-либо другой длины.
Иногда шаблоны нужно заключать в скобки, чтобы избежать неоднозначностей при разборе. Поэтому мы расширяем язык шаблонов поддержкой шаблонов в скобках, которые записываются как (p), где p — шаблон.
Точнее, мы меняем грамматику шаблонов. Если предположить, что Record Patterns (шаблоны записей) и шаблоны массивов из JEP 405 будут добавлены, грамматика шаблонов станет такой:
Pattern:
PrimaryPattern
GuardedPattern
GuardedPattern:
PrimaryPattern && ConditionalAndExpression
PrimaryPattern:
TypePattern
RecordPattern
ArrayPattern
( Pattern )
Guarded-шаблон имеет вид p && e, где p — шаблон, а e — логическое выражение. В guarded-шаблоне любая локальная переменная, формальный параметр или параметр исключения, которые используются, но не объявлены в подвыражении, должны быть либо final, либо фактически финальными (effectively final).
Guarded-шаблон p && e вводит объединение переменных шаблона, вводимых шаблоном p и выражением e. Область видимости любого объявления переменной шаблона в p включает выражение e. Это позволяет использовать такие шаблоны, как String s && (s.length() > 1), который соответствует значению, приводимому к String, причём длина этой строки больше единицы.
Значение соответствует guarded-шаблону p && e, если, во-первых, оно соответствует шаблону p и, во-вторых, выражение e вычисляется в true. Если значение не соответствует p, то попытка вычислить выражение e не предпринимается.
Шаблон в скобках имеет вид (p), где p — шаблон. Шаблон в скобках (p) вводит те переменные шаблона, которые вводит подшаблон p. Значение соответствует шаблону в скобках (p), если оно соответствует шаблону p.
Мы также меняем грамматику выражений instanceof на следующую:
InstanceofExpression:
RelationalExpression instanceof ReferenceType
RelationalExpression instanceof PrimaryPattern
Это изменение, а также нетерминал ConditionalAndExpression в правиле грамматики для guarded-шаблона гарантируют, что, например, выражение e instanceof String s && s.length() > 1 по-прежнему однозначно разбирается как выражение (e instanceof String s) && (s.length() > 1). Если завершающее && должно быть частью guarded-шаблона, весь шаблон следует заключить в скобки, например e instanceof (String s && s.length() > 1).
Использование нетерминала ConditionalAndExpression в правиле грамматики для guarded-шаблонов также устраняет ещё одну возможную неоднозначность, связанную с меткой case с guarded-шаблоном. Например:
boolean b = true;
switch (o) {
case String s && b -> s -> s;
}
Если бы охранное выражение guarded-шаблона могло быть произвольным выражением, возникала бы неоднозначность: является ли первое вхождение -> частью лямбда-выражения или частью правила switch, тело которого — лямбда-выражение. Поскольку лямбда-выражение никогда не может быть допустимым логическим выражением, ограничить грамматику охранного выражения безопасно.
Дальнейшая работа
-
Сейчас шаблонный
switchне поддерживает примитивные типыboolean,floatиdouble. Польза от них, по-видимому, минимальна, но их поддержку можно добавить. -
Мы ожидаем, что в будущем классы общего вида смогут объявлять шаблоны деконструкции, определяющие, как с ними выполнять сопоставление. Такие шаблоны деконструкции можно использовать с шаблонным
switch, чтобы получать очень лаконичный код. Например, если у нас есть иерархияExprс подтипами дляIntExpr(содержит одноint),AddExprиMulExpr(содержат по дваExpr) иNegExpr(содержит одноExpr), мы можем сопоставитьExprс шаблонами и обработать конкретные подтипы за один шаг:int eval(Expr n) { return switch(n) { case IntExpr(int i) -> i; case NegExpr(Expr n) -> -eval(n); case AddExpr(Expr left, Expr right) -> eval(left) + eval(right); case MulExpr(Expr left, Expr right) -> eval(left) * eval(right); default -> throw new IllegalStateException(); }; }Без такого Pattern Matching для записи подобных ad-hoc полиморфных вычислений приходится использовать громоздкий паттерн «Посетитель». Pattern Matching, как правило, нагляднее и проще.
-
Также может оказаться полезным добавить шаблоны AND и OR, чтобы сделать метки
caseс шаблонами более выразительными.
Альтернативы
-
Вместо поддержки шаблонного
switchможно было бы определитьswitchпо типу, который поддерживает только переключение по типу выражения-селектора. Эту возможность проще специфицировать и реализовать, но она значительно менее выразительна. -
Для guarded-шаблонов есть много других вариантов синтаксиса, например
p where e,p when e,p if eили дажеp &&& e. -
Альтернатива guarded-шаблонам — поддерживать guard-условия непосредственно как особую форму метки
case:SwitchLabel: case Pattern [ when Expression ] ...Поддержка guard-условий в метках
caseтребует введенияwhenкак нового контекстного ключевого слова, тогда как guarded-шаблонам не нужны новые контекстные ключевые слова или операторы. Guarded-шаблоны дают значительно большую гибкость, поскольку guarded-шаблон может находиться рядом с тем местом, к которому он относится, а не в конце метки switch.
Зависимости
Этот JEP основан на Pattern Matching для instanceof (JEP 394), а также на улучшениях, которые дают выражения switch (JEP 361). Когда появится JEP 405 (Record Patterns & Array Patterns), итоговая реализация, вероятно, будет использовать динамические константы (JEP 309).