JEP 406: Pattern Matching for switch (Preview)
Pattern Matching (сопоставление с образцом) для switch, версия Preview (предварительная версия)
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 17 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Связан с | JEP 420: Pattern Matching for switch (Second Preview) |
| JEP 427: Pattern Matching for switch (Third Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2018/10/29 08:07 |
| Обновлён | 2022/06/01 21:06 |
| Задача | 8213076 |
Аннотация
Расширить язык программирования Java: добавить Pattern Matching для выражений и операторов switch, а также расширить язык шаблонов. Распространение Pattern Matching на switch позволяет проверять выражение на соответствие ряду шаблонов, у каждого из которых своё действие, так что сложные запросы, ориентированные на данные, можно выражать кратко и безопасно. Это Preview-возможность языка в JDK 17.
Цели
-
Повысить выразительность и расширить область применения выражений и операторов
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 можно только по значениям нескольких типов — числовых типов, типов перечислений и 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, соответствующую всем треугольникам, а затем довольно неудобно разместить проверку площади треугольника внутри соответствующей группы операторов. Затем приходится использовать проваливание (fall-through), чтобы получить правильное поведение, когда площадь треугольника меньше 100. (Обратите внимание на аккуратное размещение break; внутри блока if.)
Проблема в том, что использование одного шаблона для различения случаев не масштабируется дальше одного условия. Нужен какой-то способ выразить уточнение шаблона. Один из подходов — разрешить уточнять метки case; в других языках программирования такое уточнение называется охранным условием (guard). Например, можно ввести новое ключевое слово where, которое ставится в конце метки case и за которым следует логическое выражение, например case Triangle t where t.calculateArea() > 100.
Однако есть более выразительный подход. Вместо того чтобы расширять функциональность меток case, можно расширить сам язык шаблонов. Можно добавить новый вид шаблона — охраняемый шаблон, записываемый как 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помимо констант использовать шаблоны и -
Вводим два новых вида шаблонов: охраняемые шаблоны и шаблоны в скобках.
Шаблоны в метках 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, либо типом перечисления. Мы расширяем это правило и требуем, чтобы тип выражения-селектора был либо целочисленным примитивным типом, либо любым ссылочным типом.
Например, в следующем switch с шаблонами выражение-селектор o сопоставляется с шаблонами типа, в которых участвуют тип класса, тип перечисления, тип записи и тип массива (а также метка 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 " + Color.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, поскольку каждое значение, соответствующее охраняемому шаблону String s && s.length() > 0, соответствует и шаблону String s.
Компилятор проверяет все метки-шаблоны. Если над меткой-шаблоном в блоке switch доминирует более ранняя метка-шаблон в том же блоке switch, это ошибка компиляции.
Это требование доминирования гарантирует, что если блок switch содержит только метки case с шаблонами типа, они будут расположены в порядке подтипов.
Понятие доминирования аналогично условиям для блоков
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 с шаблонами:
static int coverage(Object o) {
return switch (o) { // Error - incomplete
case String s -> s.length();
};
}
В блоке switch есть только одна метка case — case String s. Ей соответствует любое значение выражения-селектора, тип которого является подтипом String. Поэтому мы говорим, что покрытие типов этого правила со стрелкой — все подтипы String. Это выражение switch с шаблонами неполно, поскольку покрытие типов его блока switch не включает тип выражения-селектора.
Рассмотрим следующий (по-прежнему ошибочный) пример:
static int coverage(Object o) {
return switch (o) { // Error - incomplete
case String s -> s.length();
case Integer i -> i;
};
}
Покрытие типов этого блока 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-класс (Sealed Classes (запечатанные классы), JEP 409), то при проверке покрытия типов можно учитывать раздел permits sealed-класса, чтобы определить, является ли блок switch полным. Рассмотрим следующий пример 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 testSealedCoverage(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 не нужна.
Для защиты от несовместимой раздельной компиляции компилятор автоматически добавляет метку default, код которой выбрасывает IncompatibleClassChangeError. До этой метки выполнение дойдёт, только если интерфейс sealed изменён, а код switch не перекомпилирован. По сути, компилятор сам делает ваш код надёжнее.
Требование полноты выражения
switchс шаблонами аналогично правилам для выраженияswitch, выражение-селектор которого имеет тип enum-класса: там меткаdefaultне требуется, если есть ветка для каждой константы enum-класса.
Польза от того, что компилятор проверяет полноту switch-выражений, чрезвычайно полезна. Вместо того чтобы оставлять эту проверку только для выражений switch, мы распространяем её и на операторы switch. Из соображений обратной совместимости все существующие операторы switch будут компилироваться без изменений. Но если оператор switch использует какую-либо из новых возможностей, описанных в этом JEP, компилятор проверит, что он полон.
Точнее, полнота требуется от операторов switch, которые используют метки с шаблонами или метки null либо выражение-селектор которых не относится к унаследованным типам (char, byte, short, int, Character, Byte, Short, Integer, String или enum-тип).
Это означает, что теперь и выражения 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 switchStatementComplete(S s) {
switch (s) { // Error - incomplete; missing clause for permitted class B!
case A a :
System.out.println("A");
break;
case C c :
System.out.println("B");
break;
};
}
Чтобы сделать полными большинство операторов switch, достаточно добавить простую ветку default в конец тела switch. Так код становится понятнее, и его проще проверить. Например, следующий оператор switch неполон и ошибочен:
Object o = ...
switch (o) { // Error - incomplete!
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 complete!
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, за которой не следуют другие метки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 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, но инициализирована была бы только одна из переменных 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, которой явно соответствует значение выражения-селектора, равное 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 в JDK 16 поддерживают два стиля: один основан на помеченных группах операторов (форма :), где возможен провал, а другой — на форме с единственным следствием (форма ->), где провал невозможен. В первом стиле несколько меток обычно записываются как 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 соответствует этой метке, если оно либо является нулевой ссылкой, либо является 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 соответствует этой метке, если оно либо является нулевой ссылкой, либо ему не соответствует ни одна другая метка.
Шаблоны с условием и шаблоны в скобках
После успешного сопоставления с шаблоном мы часто дополнительно проверяем его результат. Это может приводить к громоздкому коду, например:
static void test(Object o) {
switch (o) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
Нужная проверка (что o является String длины 1), к сожалению, разбита между меткой case и следующим за ней оператором if. Код стал бы читаемее, если бы pattern switch позволял объединять шаблон и логическое выражение в метке case.
Вместо того чтобы добавлять ещё одну особую метку case, мы расширяем язык шаблонов и добавляем шаблоны с условием, которые записываются как 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 )
Шаблон с условием имеет вид p && e, где p — шаблон, а e — логическое выражение. В шаблоне с условием любая локальная переменная, формальный параметр или параметр исключения, которые используются, но не объявлены в подвыражении, должны быть либо final, либо фактически финальными (effectively final).
Шаблон с условием p && e вводит объединение переменных шаблона, вводимых шаблоном p и выражением e. Область видимости любого объявления переменной шаблона в p включает выражение e. Так становятся возможны шаблоны вроде String s && (s.length() > 1): ему соответствует значение, которое можно привести к String, причём длина строки больше единицы.
Значение соответствует шаблону с условием p && e, если, во-первых, оно соответствует шаблону p и, во-вторых, выражение e вычисляется в true. Если значение не соответствует p, выражение e не вычисляется.
Шаблон в скобках имеет вид (p), где p — шаблон. Шаблон в скобках (p) вводит те же переменные шаблона, что и подшаблон p. Значение соответствует шаблону в скобках (p), если оно соответствует шаблону p.
Мы также меняем грамматику выражений instanceof на следующую:
InstanceofExpression:
RelationalExpression instanceof ReferenceType
RelationalExpression instanceof PrimaryPattern
Это изменение, а также нетерминал ConditionalAndExpression в правиле грамматики для шаблона с условием гарантируют, что, например, выражение e instanceof String s && s.length() > 1 по-прежнему однозначно разбирается как выражение (e instanceof String s) && (s.length() > 1). Если завершающий && должен быть частью шаблона с условием, весь шаблон нужно заключить в скобки, например e instanceof (String s && s.length() > 1).
Использование нетерминала
ConditionalAndExpressionв правиле грамматики для шаблона с условием устраняет и ещё одну возможную неоднозначность, связанную с меткойcaseс шаблоном с условием. Например:boolean b = true; switch (o) { case String s && b -> s -> s; }Если бы условием в шаблоне с условием могло быть произвольное выражение, возникла бы неоднозначность: является ли первое вхождение
->частью лямбда-выражения или частью правила switch, тело которого — лямбда-выражение. Поскольку лямбда-выражение никогда не может быть допустимым логическим выражением, грамматику выражения-условия можно без риска ограничить.
Дальнейшая работа
-
Сейчас pattern
switchне поддерживает примитивные типыboolean,floatиdouble. Польза от них, по-видимому, минимальна, но их поддержку можно добавить. -
Мы ожидаем, что в будущем обычные классы смогут объявлять деконструирующие шаблоны, которые задают, как с ними выполнять сопоставление. Такие деконструирующие шаблоны можно использовать с pattern
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 полиморфных вычислений приходится использовать громоздкий [паттерн «Посетитель»][visitor]. Pattern Matching обычно прозрачнее и проще.
-
Может также оказаться полезным добавить шаблоны AND и OR, чтобы сделать метки
caseс шаблонами выразительнее.
Альтернативы
-
Вместо поддержки pattern
switchможно было бы определитьswitchпо типу, который поддерживает только выбор по типу выражения-селектора. Такую возможность проще специфицировать и реализовать, но она значительно менее выразительна. -
Для шаблонов с условием есть много других вариантов синтаксиса, например
p where e,p when e,p if eили дажеp &&& e. -
Альтернатива шаблонам с условием — поддерживать условия непосредственно как особую форму метки
case:SwitchLabel: case Pattern [ when Expression ] ...Для поддержки условий в метках
caseнужно ввестиwhenкак новое контекстное ключевое слово, тогда как шаблонам с условием не нужны ни новые контекстные ключевые слова, ни новые операторы. Шаблоны с условием значительно гибче, поскольку шаблон с условием может стоять рядом с тем местом, к которому он относится, а не в конце метки switch.
Зависимости
Этот JEP основан на Pattern Matching для instanceof (JEP 394), а также на улучшениях, которые дали выражения switch (JEP 361). Мы намерены выпустить этот JEP одновременно с JEP 405, который определяет два новых вида шаблонов с поддержкой вложенности. В реализации, вероятно, будут использоваться динамические константы (JEP 309).