JEP 427: Pattern Matching for switch (Third Preview)
Pattern Matching (сопоставление с образцом) для switch, третья версия Preview (предварительная версия)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 19 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Связан с | JEP 406: Pattern Matching for switch (Preview) |
| JEP 420: Pattern Matching for switch (Second Preview) | |
| JEP 405: Record Patterns (Preview) | |
| JEP 433: Pattern Matching for switch (Fourth Preview) | |
| Рецензенты | Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2022/02/22 18:01 |
| Обновлён | 2023/05/12 15:34 |
| Задача | 8282272 |
Аннотация
Расширить язык программирования Java механизмом Pattern Matching для выражений и операторов switch. Распространение Pattern Matching на switch позволяет проверять выражение на соответствие ряду шаблонов, для каждого из которых задано своё действие, и так кратко и безопасно выражать сложные запросы, ориентированные на данные. Это возможность языка в статусе Preview.
История
Pattern Matching для switch был предложен как Preview-возможность в JEP 406 и поставлен в JDK 17, а затем предложен во второй версии Preview в JEP 420 и поставлен в JDK 18. Этот JEP предлагает третью версию Preview с дальнейшими доработками на основе накопленного опыта и отзывов.
Основные изменения по сравнению со второй версией Preview:
-
Guarded patterns заменены на конструкции
whenв блоках switch. -
Семантика выполнения switch с шаблонами, когда значение выражения-селектора равно
null, ближе к семантике традиционного switch.
Цели
-
Расширить выразительность и применимость выражений и операторов
switch, разрешив использовать шаблоны в меткахcase. -
Позволить при необходимости ослабить исторически сложившуюся враждебность
switchк null. -
Повысить безопасность операторов
switch, потребовав, чтобы операторыswitchс шаблонами охватывали все возможные входные значения. -
Гарантировать, что все существующие выражения и операторы
switchпо-прежнему компилируются без изменений и выполняются с той же семантикой.
Мотивация
В 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 можно только по значениям нескольких типов — целочисленных примитивных типов (кроме long), соответствующих им упакованных типов, типов 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).
Switch с шаблонами и 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, разрешив новую метку case null:
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 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);
default -> System.out.println("Something else");
}
}
Уточнение case
Эксперименты с шаблонами в 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.)
Проблема в том, что использование одного шаблона для различения случаев не масштабируется за пределы одного условия: нужен какой-то способ выразить уточнение шаблона. Один из подходов — ввести guarded patterns, записываемые как p && b, которые позволяют уточнить шаблон p произвольным булевым выражением b.
Мы реализовали guarded patterns в предшественниках этого JEP. На основе опыта и отзывов мы предлагаем вместо этого разрешить конструкции when в блоках switch, задающие guard для меток с шаблонами, например case Triangle t when t.calculateArea() > 100. Такую метку с шаблоном мы называем guarded-меткой, а булево выражение — guard.
При таком подходе можно пересмотреть код testTriangle и напрямую выразить особый случай для больших треугольников. Это избавляет от проваливания в операторе switch, а значит, можно использовать краткие правила со стрелкой (->):
static void testTriangle(Shape s) {
switch (s) {
case null ->
{ break; }
case Triangle t
when t.calculateArea() > 100 ->
System.out.println("Large triangle");
default ->
System.out.println("A shape, possibly a small triangle");
}
}
Первая ветвь выбирается, если значение s соответствует шаблону Triangle t и затем guard t.calculateArea() > 100 вычисляется в true. (В guard можно использовать любые переменные шаблона, объявленные шаблоном в метке case.)
С switch легко понимать и изменять метки case при изменении требований к приложению. Например, мы можем захотеть вывести треугольники из ветви по умолчанию; это можно сделать с помощью двух ветвей case — одной с guard и одной без:
static void testTriangle(Shape s) {
switch (s) {
case null ->
{ break; }
case Triangle t
when t.calculateArea() > 100 ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
Описание
Мы расширяем операторы и выражения switch тремя способами:
-
Расширяем метки
case, чтобы помимо констант они могли содержать шаблоны иnull, -
Расширяем набор типов, допустимых для выражений-селекторов как операторов
switch, так и выраженийswitch, и -
Разрешаем необязательные конструкции
whenпосле метокcase.
Для удобства мы также вводим шаблоны в скобках.
Шаблоны в метках 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();
};
После успешного сопоставления с шаблоном мы часто дополнительно проверяем его результат. Это может приводить к громоздкому коду, например:
static void test(Object o) {
switch (o) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
Нужная проверка — что o является String длины 1 — к сожалению, разделена между меткой case с шаблоном и следующим за ней оператором if.
Чтобы решить эту проблему, мы вводим guarded-метки шаблонов, поддерживая необязательный guard после метки с шаблоном. Так приведённый код можно переписать, перенеся всю условную логику в метку switch:
static void test(Object o) {
switch (o) {
case String s when s.length() == 1 -> ...
case String s -> ...
...
}
}
Первая ветвь срабатывает, если o является String и имеет длину 1. Вторая ветвь срабатывает, если o является String любой длины.
Guard может быть только у меток с шаблонами. Например, недопустимо писать метку с константой case и guard, например case "Hello" when RandomBooleanExpression().
Иногда шаблоны нужно заключать в скобки для удобства чтения. Поэтому мы расширяем язык шаблонов поддержкой шаблонов в скобках, записываемых как (p), где p — шаблон. Шаблон в скобках (p) вводит те переменные шаблона, которые вводит подшаблон p. Значение соответствует шаблону в скобках (p), если оно соответствует шаблону p.
При поддержке шаблонов в switch нужно рассмотреть четыре основные области проектирования языка:
- Улучшенная проверка типов
- Исчерпываемость выражений и операторов
switch - Область видимости объявлений переменных шаблона
- Обработка
null
1. Улучшенная проверка типов
1a. Типизация выражения-селектора
Поддержка шаблонов в switch означает, что можно ослабить текущие ограничения на тип выражения-селектора. Сейчас тип выражения-селектора обычного switch должен быть целочисленным примитивным типом (кроме long), соответствующим упакованным типом (то есть Character, Byte, Short или Integer), String или типом enum. Мы расширяем это правило и требуем, чтобы тип выражения-селектора был либо целочисленным примитивным типом (кроме long), либо любым ссылочным типом.
Например, в следующем switch с шаблонами выражение-селектор o сопоставляется с шаблонами типов, включающими тип класса, тип enum, тип записи и тип массива, а также с меткой 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: " + c.toString());
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. Доминирование меток шаблонов
Поддержка меток шаблонов означает, что теперь к значению выражения-селектора могут быть применимы несколько меток (раньше все метки case не пересекались). Например, к значению типа String могут быть применимы обе метки: case String s и case CharSequence cs.
Сначала нужно решить, какая именно метка должна применяться в такой ситуации. Вместо сложного подхода с выбором наиболее подходящей метки мы принимаем более простую семантику: выбирается первая метка шаблона в блоке switch, которая применима к значению.
static void first(Object o) {
switch (o) {
case String s ->
System.out.println("A string: " + s);
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
default -> {
break;
}
}
}
В этом примере, если значение o имеет тип String, применяется первая метка шаблона; если оно имеет тип CharSequence, но не тип String, применяется вторая метка шаблона.
Но что произойдёт, если поменять эти две метки шаблонов местами?
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;
}
}
}
Теперь, если значение o имеет тип String, применяется метка шаблона CharSequence, поскольку она стоит в блоке switch первой. Метка шаблона String недостижима в том смысле, что не существует значения выражения-селектора, при котором она была бы выбрана. По аналогии с недостижимым кодом это считается ошибкой программиста и приводит к ошибке компиляции.
Точнее говоря, мы говорим, что первая метка шаблона case CharSequence cs доминирует над второй меткой шаблона case String s, потому что каждое значение, соответствующее шаблону String s, соответствует и шаблону CharSequence cs, но не наоборот. Так происходит потому, что тип второго шаблона, String, является подтипом типа первого шаблона, CharSequence.
Метка шаблона без охранного условия доминирует над меткой шаблона с охранным условием, у которой тот же шаблон. Например, метка шаблона case String s (без охранного условия) доминирует над меткой шаблона с охранным условием case String s when s.length() > 0, поскольку каждое значение, соответствующее метке шаблона case String s when s.length() > 0, обязательно соответствует метке шаблона case String s.
Метка шаблона с охранным условием доминирует над другой меткой шаблона (с охранным условием или без него), только если её шаблон доминирует над шаблоном другой метки и её охранное условие является константным выражением со значением true. Например, метка шаблона с охранным условием case String s when true доминирует над меткой шаблона case String s. Мы не анализируем охранное выражение глубже, чтобы точнее определить, какие значения соответствуют метке шаблона (в общем случае эта задача неразрешима).
Метка шаблона может доминировать над меткой-константой. Например, метка шаблона case Integer i доминирует над меткой-константой case 42, а метка шаблона case E e доминирует над меткой-константой case A, когда A — константа перечисления с типом перечисления E. Метка шаблона с охранным условием доминирует над меткой-константой, если над ней доминирует та же метка шаблона без ветви when. Иными словами, мы не проверяем охранное условие, поскольку в общем случае это неразрешимо. Например, метка шаблона case String s when s.length() > 1, как и ожидается, доминирует над меткой-константой case "hello"; но и case Integer i when i != 0 доминирует над меткой case 0.
Всё это подсказывает простой, предсказуемый и удобочитаемый порядок меток case, при котором метки-константы должны идти перед метками шаблонов с охранным условием, а те — перед метками шаблонов без охранного условия:
Integer i = ...
switch (i) {
case -1, 1 -> ... // Special cases
case Integer i when i > 0 -> ... // Positive integer cases
case Integer i -> ... // All the remaining integers
}
Компилятор проверяет все метки. Если над меткой в блоке switch доминирует более ранняя метка в том же блоке switch, это ошибка компиляции. Это требование доминирования гарантирует, что если блок switch содержит только метки case с шаблонами типов, они будут расположены в порядке подтипов.
(Понятие доминирования аналогично условиям на ветви catch оператора try, где ошибкой считается, если ветви catch, перехватывающей класс исключения E, предшествует ветвь catch, которая может перехватить E или суперкласс E (JLS §11.2.3). С точки зрения логики, предшествующая ветвь catch доминирует над последующей ветвью catch.)
Ошибкой компиляции также является наличие в блоке switch выражения switch или оператора switch более одной метки switch, охватывающей все значения. Метки, охватывающие все значения, — это default и метки шаблонов, шаблон которых безусловно соответствует выражению-селектору. Например, шаблон типа String s безусловно соответствует выражению-селектору типа String, а шаблон типа Object o безусловно соответствует выражению-селектору любого ссылочного типа.
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 есть только одна метка switch, case String s. Ей соответствует любое значение выражения-селектора, тип которого является подтипом String. Поэтому мы говорим, что покрытие типов этой меткой switch — все подтипы String. Это выражение switch с шаблонами не является исчерпывающим, потому что покрытие типов его блоком switch (все подтипы String) не включает тип выражения-селектора (Object).
Рассмотрим такой (по-прежнему ошибочный) пример:
static int coverage(Object o) {
return switch (o) { // Error - still 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>.
Для защиты от несовместимой раздельной компиляции в этом случае — switch по sealed-классу, где блок switch является исчерпывающим и нет case, охватывающего все значения, — компилятор автоматически добавляет метку default, код которой выбрасывает IncompatibleClassChangeError. Эта метка будет достигнута, только если интерфейс sealed изменится, а код switch не будет перекомпилирован. По сути, компилятор сам делает ваш код надёжнее.
Это условие полноты относится и к выражениям switch с шаблонами, и к операторам switch с шаблонами. Для обратной совместимости все существующие операторы switch будут компилироваться без изменений. Но если оператор switch использует какое-либо из улучшений switch, описанных в этом JEP, компилятор проверит, что он является исчерпывающим. (Будущие компиляторы языка Java могут выдавать предупреждения для старых операторов switch, которые не являются исчерпывающими.)
Точнее говоря, полнота требуется от любого оператора switch, который использует метки шаблонов или метки null либо выражение-селектор которого не относится к старым типам (char, byte, short, int, Character, Byte, Short, Integer, String или тип перечисления). Например:
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;
}
Понятие полноты усложняется из-за Record Patterns (шаблоны записей) (JEP 405), поскольку эти шаблоны допускают вложение в них других шаблонов. Соответственно, понятие полноты должно отражать эту потенциально рекурсивную структуру.
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, с помощью трёх новых правил:
-
Область видимости объявления переменной шаблона, встречающегося в метке switch, включает любую ветвь
whenэтой метки. -
Область видимости объявления переменной шаблона, встречающегося в метке
caseправилаswitch, включает выражение, блок или операторthrow, стоящие справа от стрелки. -
Область видимости объявления переменной шаблона, встречающегося в метке
caseгруппы операторов с меткойswitch, включает операторы блока этой группы операторов. Проваливание через меткуcase, объявляющую переменную шаблона, запрещено.
Этот пример показывает действие первого правила:
static void test(Object o) {
switch (o) {
case Character c
when c.charValue() == 7:
System.out.println("Ding!");
break;
default:
break;
}
}
}
Область видимости объявления переменной шаблона c включает выражение when метки switch.
Этот вариант показывает действие второго правила:
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: "
+ 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, объявляющую переменную шаблона, является ошибкой компиляции.
Именно поэтому метка switch, состоящая из нескольких меток шаблонов, например 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.
Во-первых, мы вводим новую метку null для case. Затем мы отменяем общее правило, согласно которому switch сразу выбрасывает NullPointerException, если значение выражения-селектора равно null. Вместо этого мы анализируем метки case, чтобы определить поведение switch:
-
Если выражение-селектор вычисляется в
null, то считается, что с ним совпадает любая метка casenull. Если в блоке switch такой метки нет, тоswitchвыбрасываетNullPointerException, как и раньше. -
Если выражение-селектор вычисляется в значение, отличное от
null, то мы, как обычно, выбираем совпадающую меткуcase. Если ни одна меткаcaseне совпадает, то совпадающей считается любая меткаdefault.
Например, при приведённом ниже объявлении вычисление 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 метку null, то эта метка совпадёт со значением null. Если вы не видите в блоке switch метки null, то switch по значению null выбросит NullPointerException, как и раньше.
4b. Новые формы меток, возникающие из меток null
Начиная с Java 16, блоки switch поддерживают два стиля: один основан на помеченных группах операторов (форма :), где возможно проваливание, а другой — на форме с единственным следствием (форма ->), где проваливание невозможно. В первом стиле несколько меток обычно записываются как 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, либо не совпадает ни с какой другой меткой.
Дальнейшая работа
-
В настоящий момент шаблонный
switchне поддерживает примитивные типыboolean,long,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 для подобных специализированных полиморфных вычислений приходится использовать громоздкий шаблон «Посетитель». Pattern Matching в целом прозрачнее и проще.
-
Также может быть полезно добавить шаблоны AND и OR, чтобы сделать метки
caseс шаблонами более выразительными.
Альтернативы
-
Вместо поддержки шаблонного
switchмы могли бы определить типовыйswitch, который поддерживает только switch по типу выражения-селектора. Эту возможность проще специфицировать и реализовать, но она значительно менее выразительна. -
Для меток шаблонов с охранным условием есть много других синтаксических вариантов, например
p where e,p if eили дажеp &&& e. -
Альтернатива меткам шаблонов с охранным условием — поддерживать шаблоны с охранным условием напрямую как особую форму шаблона, например
p && e. Мы экспериментировали с этим в предыдущих версиях Preview, и возникающая при этом неоднозначность с булевыми выражениями привела нас к тому, что мы предпочли конструкцииwhenв шаблонных switch.
Зависимости
Этот JEP основан на Pattern Matching для instanceof (JEP 394), а также на улучшениях, которые дают выражения switch (JEP 361). Когда появится JEP 405 (Record Patterns), в итоговой реализации, вероятно, будут использоваться динамические константы (JEP 309).