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

JEP 433: Pattern Matching for switch (Fourth Preview)

Pattern Matching (сопоставление с образцом) для switch, четвёртая версия Preview (предварительная версия)

ОтветственныйGavin Bierman
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск20
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
Связан сJEP 427: Pattern Matching for switch (Third Preview)
JEP 432: Record Patterns (Second Preview)
JEP 441: Pattern Matching for switch
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2022/09/23 14:22
Обновлён2023/05/12 15:34
Задача8294285

Аннотация

Расширить язык программирования Java механизмом Pattern Matching для выражений и операторов switch. Распространение Pattern Matching на switch позволяет проверять выражение на соответствие нескольким шаблонам, для каждого из которых задано своё действие, так что сложные запросы, ориентированные на данные, можно записывать кратко и безопасно. Это Preview-возможность языка.

История

Pattern Matching для switch был предложен как Preview-возможность в JEP 406 и выпущен в JDK 17, предложен для второй версии Preview в JEP 420 и выпущен в JDK 18, а также предложен для третьей версии Preview в JEP 427 и выпущен в JDK 19. Этот JEP предлагает четвёртую версию Preview, чтобы продолжить совместное развитие с Preview-возможностью Record Patterns (шаблоны записей), описанной в JEP 432, и внести другие улучшения на основе накопленного опыта и отзывов.

Основные изменения по сравнению с третьей версией Preview:

  • Исчерпывающий switch (то есть выражение switch или оператор switch с шаблонами) по классу enum теперь выбрасывает MatchException, а не IncompatibleClassChangeError, если во время выполнения не подходит ни одна метка switch.

  • Грамматика меток switch упрощена.

  • Вывод аргументов типа для обобщённых шаблонов записей теперь поддерживается в выражениях и операторах switch, как и в других конструкциях, поддерживающих шаблоны.

Цели

  • Повысить выразительность и расширить область применения выражений и операторов switch, разрешив использовать шаблоны в метках case.

  • Позволить при необходимости ослабить исторически сложившуюся враждебность switch к null.

  • Повысить безопасность операторов switch, потребовав, чтобы операторы switch с шаблонами покрывали все возможные входные значения.

  • Гарантировать, что все существующие выражения и операторы switch по-прежнему компилируются без изменений и выполняются с той же семантикой.

Мотивация

В Java 16 JEP 394 расширил оператор instanceof: теперь он принимает шаблон типа и выполняет Pattern Matching. Это скромное расширение позволяет упростить привычную идиому «instanceof и приведение типа», делая её одновременно более краткой и менее подверженной ошибкам:

// Old code
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}

// New code
if (obj instanceof String s) {
    ... use s ...
}

Часто нужно сравнить переменную, например obj, с несколькими вариантами. Java поддерживает многовариантные сравнения с помощью операторов switch, а начиная с Java 14 — и выражений switch (JEP 361), но, к сожалению, возможности switch очень ограничены. Выполнять switch можно только по значениям нескольких типов — целочисленных примитивных типов (кроме long), соответствующих им обёрточных типов, типов enum и String, — и проверять можно только точное равенство константам. Нам могло бы понадобиться использовать шаблоны, чтобы проверить одну и ту же переменную на несколько вариантов и выполнить для каждого своё действие, но поскольку существующий switch этого не поддерживает, в итоге получается цепочка проверок if...else, например:

static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj 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 obj) {
    return switch (obj) {
        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        -> obj.toString();
    };
}

Семантика этого switch ясна: метка case с шаблоном срабатывает, если значение выражения-селектора obj соответствует шаблону. (Для краткости мы показали выражение switch, но могли бы показать и оператор switch; блок switch, включая метки case, остался бы тем же.)

Замысел этого кода понятнее, потому что мы используем правильную управляющую конструкцию: мы говорим: «параметр obj соответствует не более чем одному из следующих условий, определи, какому именно, и вычисли соответствующую ветвь». Дополнительный плюс в том, что такой код лучше поддаётся оптимизации: в этом случае мы с большей вероятностью сможем выполнить диспетчеризацию за время 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.)

Уточнение 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, соответствующую всем треугольникам, а затем довольно неудобно поместить проверку площади треугольника внутрь соответствующей группы операторов. Затем приходится использовать проваливание (fall-through), чтобы получить правильное поведение, когда площадь треугольника меньше 100. (Обратите внимание на аккуратное размещение оператора break внутри блока if.)

Проблема в том, что использование одного шаблона для различения вариантов не масштабируется дальше одного условия — нужен способ выразить уточнение шаблона. Поэтому мы разрешаем в блоках switch предложения when, которые задают охранные условия для меток case с шаблонами, например case Triangle t when t.calculateArea() > 100. Такую метку case мы называем охраняемой меткой case, а логическое выражение — охранным условием.

При таком подходе можно пересмотреть код 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 и затем охранное условие t.calculateArea() > 100 вычисляется в true. (В охранном условии можно использовать любые переменные шаблона, объявленные шаблоном в метке case.)

С switch метки case легко понимать и изменять, когда меняются требования к приложению. Например, может понадобиться вынести треугольники из ветви по умолчанию; для этого можно использовать два предложения case, одно с охранным условием, другое без него:

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 в блоке switch следующим образом (ср. JLS §14.11.1):

SwitchLabel:
  case CaseConstant { , CaseConstant }
  case null [, default]
  case Pattern
  default

Главное улучшение — новая метка case, case p, где p — шаблон. Суть switch не меняется: значение выражения-селектора сравнивается с метками switch, выбирается одна из меток, и выполняется или вычисляется связанный с ней код. Разница теперь в том, что для меток case с шаблонами выбранная метка определяется результатом Pattern Matching, а не проверкой на равенство. Например, в следующем коде значение obj соответствует шаблону Long l, и вычисляется выражение, связанное с меткой case Long l:

Object obj = 123L;
String formatted = switch (obj) {
    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        -> obj.toString();
};

После успешного сопоставления с шаблоном мы часто дополнительно проверяем его результат. Это может приводить к громоздкому коду, например:

static void test(Object obj) {
    switch (obj) {
        case String s:
            if (s.length() == 1) { ... }
            else { ... }
            break;
        ...
    }
}

Нужная проверка — что obj является String длины 1 — к сожалению, разделена между меткой case с шаблоном и следующим за ней оператором if.

Чтобы решить эту проблему, мы вводим охраняемые метки case с шаблонами, поддерживая необязательное охранное условие после метки с шаблоном. Это позволяет переписать приведённый выше код так, чтобы вся условная логика была перенесена в метку switch:

static void test(Object obj) {
    switch (obj) {
        case String s when s.length() == 1 -> ...
        case String s                      -> ...
        ...
    }
}

Первое предложение срабатывает, если obj является String и имеет длину 1. Второй case срабатывает, если obj является String любой длины.

Охранное условие может быть только у меток с шаблонами. Например, нельзя написать метку с константой case и охранным условием, например case "Hello" when RandomBooleanExpression().

Иногда шаблоны нужно заключать в скобки для удобочитаемости. Поэтому мы расширяем язык шаблонов, добавляя поддержку шаблонов в скобках, записываемых как (p), где p — шаблон. Шаблон в скобках (p) вводит те же переменные шаблона, что и подшаблон p. Значение соответствует шаблону в скобках (p), если оно соответствует шаблону p.

При поддержке шаблонов в switch нужно рассмотреть пять основных аспектов дизайна языка:

  1. Улучшенная проверка типов
  2. Исчерпываемость выражений и операторов switch
  3. Область видимости объявлений переменных шаблона
  4. Обработка null
  5. Ошибки

1. Улучшенная проверка типов

1a. Типизация выражения-селектора

Поддержка шаблонов в switch означает, что можно ослабить текущие ограничения на тип выражения-селектора. Сейчас тип выражения-селектора обычного switch должен быть либо целочисленным примитивным типом (кроме long), либо соответствующим обёрточным типом (то есть Character, Byte, Short или Integer), либо String, либо типом enum. Мы расширяем это и требуем, чтобы тип выражения-селектора был либо целочисленным примитивным типом (кроме long), либо любым ссылочным типом.

Например, в следующем switch с шаблонами выражение-селектор obj сопоставляется с шаблонами типов, в которых участвуют тип класса, тип enum, тип записи и тип массива, а также с меткой null case и с default:

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object obj) {
    switch (obj) {
        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

Поддержка меток case с шаблонами означает, что теперь для одного значения выражения-селектора могут подходить сразу несколько меток case (раньше могла подойти не более чем одна метка case). Например, если выражение-селектор вычисляется в String, то подойдут обе метки case: и case String s, и case CharSequence cs.

Сначала нужно решить, какая именно метка должна применяться в такой ситуации. Вместо сложного подхода с поиском наилучшего соответствия мы принимаем более простую семантику: выбирается первая метка case в блоке switch, которая подходит к значению.

static void first(Object obj) {
    switch (obj) {
        case String s ->
            System.out.println("A string: " + s);
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        default -> {
            break;
        }
    }
}

В этом примере, если значение obj имеет тип String, применяется первая метка case; если оно имеет тип CharSequence, но не тип String, применяется вторая метка с шаблоном.

Но что будет, если поменять эти две метки case местами?

static void error(Object obj) {
    switch (obj) {
        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;
        }
    }
}

Теперь, если значение obj имеет тип String, применяется метка CharSequence case, поскольку она стоит в блоке switch первой. Метка String case недостижима в том смысле, что нет такого значения выражения-селектора, при котором она была бы выбрана. По аналогии с недостижимым кодом это считается ошибкой программиста и приводит к ошибке компиляции.

Точнее говоря, первая метка case case CharSequence cs доминирует над второй меткой case case String s, потому что каждое значение, которое соответствует шаблону String s, соответствует и шаблону CharSequence cs, но не наоборот. Так происходит потому, что тип второго шаблона, String, является подтипом типа первого шаблона, CharSequence.

Метка case с шаблоном без охранного условия доминирует над меткой case с тем же шаблоном и охранным условием. Например, метка case с шаблоном (без охранного условия) case String s доминирует над меткой case с шаблоном и охранным условием case String s when s.length() > 0, поскольку каждое значение, соответствующее метке case case String s when s.length() > 0, обязано соответствовать метке case case String s.

Метка case с шаблоном и охранным условием доминирует над другой меткой case с шаблоном (с охранным условием или без него), только если её шаблон доминирует над шаблоном второй метки и её охранное условие — константное выражение со значением true. Например, метка case с шаблоном и охранным условием case String s when true доминирует над меткой case с шаблоном case String s. Мы не анализируем охранное выражение дальше, чтобы точнее определить, какие значения соответствуют метке с шаблоном (в общем случае эта задача неразрешима).

Метка case с шаблоном может доминировать над меткой case с константой. Например, метка case с шаблоном case Integer i доминирует над меткой case с константой case 42, а метка case с шаблоном case E e доминирует над меткой case с константой case A, если A является элементом типа класса enum E. Метка case с шаблоном и охранным условием доминирует над меткой case с константой, если над ней доминирует та же метка case с шаблоном без охранного условия. Иначе говоря, охранное условие мы не проверяем, поскольку в общем случае это неразрешимо. Например, метка case с шаблоном case String s when s.length() > 1, как и ожидается, доминирует над меткой case с константой case "hello"; но и case Integer i when i != 0 доминирует над меткой case case 0.

Всё это подсказывает простой, предсказуемый и удобочитаемый порядок меток case: метки case с константами должны стоять перед метками case с шаблонами и охранными условиями, а те — перед метками 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
}

Компилятор проверяет все метки case. Если над меткой case в блоке switch доминирует предшествующая метка case того же блока switch, это ошибка компиляции. Это требование доминирования гарантирует, что если блок switch содержит только метки case с шаблонами типов, они будут расположены в порядке подтипов.

(Понятие доминирования аналогично условиям для секций catch оператора try: ошибкой считается, если секции catch, перехватывающей класс исключения E, предшествует секция catch, которая может перехватить E или суперкласс E (JLS §11.2.3). Логически предшествующая секция catch доминирует над последующей секцией catch.)

Ошибкой компиляции также считается, если в блоке switch выражения switch или оператора switch больше одной метки switch, соответствующей всем значениям. Такие метки — это default и метки case с шаблоном, который безусловно соответствует выражению-селектору. Например, шаблон типа String s безусловно соответствует выражению-селектору типа String, а шаблон типа Object o безусловно соответствует выражению-селектору любого ссылочного типа.

1c. Вывод аргументов типов в Record Patterns

Если record-шаблон указывает обобщённый класс записи, но не задаёт аргументы типов (то есть в record-шаблоне используется сырой тип), аргументы типов всегда выводятся. Например:

record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
            ... // Inferred record Pattern MyPair<String,Integer>(var f, var s)
        ...
    }
}

Вывод аргументов типов для record-шаблонов поддерживается во всех конструкциях, которые поддерживают шаблоны: в операторах и выражениях switch, в выражениях instanceof и в расширенных операторах for.

2. Исчерпываемость выражений и операторов switch

Выражение switch требует, чтобы в блоке switch были обработаны все возможные значения выражения-селектора; иначе говоря, оно должно быть исчерпывающим. Так сохраняется свойство, что успешное вычисление выражения switch всегда даёт значение. Для обычных выражений switch это обеспечивается довольно простым набором дополнительных условий для блока switch.

Для выражений и операторов switch с шаблонами мы добиваемся этого, вводя понятие покрытия типов метками в блоке switch. Затем покрытия типов всех меток в блоке switch объединяются, чтобы определить, исчерпывает ли блок switch все возможные значения выражения-селектора.

Рассмотрим такое (ошибочное) выражение switch с шаблонами:

static int coverage(Object obj) {
    return switch (obj) {         // Error - not exhaustive
        case String s -> s.length();
    };
}

В блоке switch только одна метка, case String s. Ей соответствует любое значение obj, тип которого является подтипом String. Поэтому мы говорим, что покрытие типов этой метки — все подтипы String. Это выражение switch с шаблонами не является исчерпывающим, потому что покрытие типов его блока switch (все подтипы String) не включает тип выражения-селектора (Object).

Рассмотрим такой (всё ещё ошибочный) пример:

static int coverage(Object obj) {
    return switch (obj) {         // Error - still not exhaustive
        case String s  -> s.length();
        case Integer i -> i;
    };
}

Покрытие типов этого блока switch — объединение покрытий двух его меток. Иначе говоря, покрытие типов — это множество всех подтипов String и множество всех подтипов Integer. Но покрытие типов по-прежнему не включает тип выражения-селектора, поэтому это выражение switch с шаблонами тоже не является исчерпывающим и вызывает ошибку компиляции.

Покрытие типов метки default — все типы, поэтому этот пример (наконец-то!) корректен:

static int coverage(Object obj) {
    return switch (obj) {
        case String s  -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}

Если тип выражения-селектора — sealed-класс (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 с шаблонами, так и к операторам switch с шаблонами. Для обратной совместимости все существующие операторы switch будут компилироваться без изменений. Но если оператор switch использует какие-либо из описанных в этом JEP улучшений switch, компилятор проверит, что он исчерпывающий. (Будущие компиляторы языка Java могут выдавать предупреждения для старых операторов switch, которые не являются исчерпывающими.)

Точнее, исчерпываемость требуется от любого оператора switch, который использует метки с шаблонами или метки null либо выражение-селектор которого не относится к старым типам (char, byte, short, int, Character, Byte, Short, Integer, String или тип enum). Например:

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 obj = ...
switch (obj) {    // Error - not exhaustive!
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
}

Сделать его исчерпывающим несложно:

Object obj = ...
switch (obj) {
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
    default:    // Now exhaustive!
        break;
}

Понятие исчерпываемости усложняется из-за Record Patterns (JEP 432), поскольку такие шаблоны допускают вложение в них других шаблонов. Соответственно, понятие исчерпываемости должно учитывать эту потенциально рекурсивную структуру.

3. Область видимости объявлений переменных шаблона

Переменные шаблона (JEP 394) — это локальные переменные, объявляемые шаблонами. Объявления переменных шаблона необычны тем, что их область видимости зависит от потока управления. Напомним на примере, где шаблон типа String s объявляет переменную шаблона s:

static void test(Object obj) {
    if ((obj instanceof String s) && s.length() > 3) {
        System.out.println(s);
    } else {
        System.out.println("Not a string");
    }
}

Объявление s находится в области видимости в правом операнде выражения &&, а также в блоке «then». Однако в блоке «else» оно вне области видимости: чтобы управление перешло в блок «else», сопоставление с шаблоном должно завершиться неудачей, и в этом случае переменная шаблона не будет инициализирована.

Мы расширяем это зависящее от потока управления понятие области видимости объявлений переменных шаблона на объявления в шаблонах меток case с помощью трёх новых правил:

  1. Область видимости объявления переменной шаблона в метке switch включает секцию when этой метки.

  2. Область видимости объявления переменной шаблона в метке case правила switch включает выражение, блок или оператор throw справа от стрелки.

  3. Область видимости объявления переменной шаблона в метке case группы операторов с меткой switch включает операторы блока этой группы операторов. Проваливание (fall through) через метку case, которая объявляет переменную шаблона, запрещено.

Этот пример показывает действие первого правила:

static void test(Object obj) {
    switch (obj) {
        case Character c
        when c.charValue() == 7:
            System.out.println("Ding!");
            break;
        default:
            break;
        }
    }
}

Область видимости объявления переменной шаблона c включает выражение when метки switch.

Этот вариант показывает действие второго правила:

static void test(Object obj) {
    switch (obj) {
        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 obj) {
    switch (obj) {
        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, хотя выполнение первой группы операторов может провалиться через метку default оператора switch и выполнить эти операторы.

Мы запрещаем проваливание через метку case, которая объявляет переменную шаблона. Рассмотрим этот ошибочный пример:

static void test(Object obj) {
    switch (obj) {
        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;
    }
}

Если бы это было разрешено и значение obj было бы 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, но инициализирована была бы только одна из них, в зависимости от того, было ли значение obj типа Character или Integer.

С другой стороны, проваливание через метку, которая не объявляет переменную шаблона, безопасно, как показывает этот пример:

void test(Object obj) {
    switch (obj) {
        case String s:
            System.out.println("A string");
        default:
            System.out.println("Done");
    }
}

4. Обработка null

Традиционно switch выбрасывает NullPointerException, если выражение-селектор вычисляется в null. Это хорошо понятное поведение, и мы не предлагаем менять его для существующего кода с switch.

Однако, поскольку для Pattern Matching и значений null существует разумная семантика, не связанная с исключениями, у нас есть возможность сделать switch с шаблонами более удобным в работе с null, сохранив совместимость с существующей семантикой switch.

Во-первых, мы вводим новую метку null case. Затем мы снимаем общее правило, по которому switch сразу выбрасывает NullPointerException, если значение выражения-селектора равно null. Вместо этого мы проверяем метки case, чтобы определить поведение switch:

  • Если выражение-селектор вычисляется в null, то считается, что совпадает любая метка case null. Если в блоке switch такой метки нет, то switch выбрасывает NullPointerException, как и раньше.

  • Если выражение-селектор вычисляется в значение, отличное от null, то мы, как обычно, выбираем подходящую метку case. Если ни одна метка case не совпадает, то считается, что совпадает любая метка default.

Например, при приведённом ниже объявлении вычисление test(null) выведет null!, а не выбросит NullPointerException:

static void test(Object obj) {
    switch (obj) {
        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 obj) {
    switch (obj) {
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

эквивалентен следующему:

static void test(Object obj) {
    switch (obj) {
        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, как и раньше.

Также имеет смысл, и это нередко нужно, объединить случай null с default. Для этого мы разрешаем метке case null иметь необязательный default, например:

Object obj = ...
switch (obj) {
    ...
    case null, default ->
        System.out.println("The rest (including null)");
}

Значение obj совпадает с этой меткой, если это нулевая ссылка или если ни одна из остальных меток case не совпадает.

Если в блоке switch есть одновременно метка null case с default и метка default, это ошибка компиляции.

5. Ошибки

Pattern Matching может завершиться аварийно. Например, при сопоставлении значения с record-шаблоном метод доступа record-класса может завершиться аварийно. В этом случае определено, что Pattern Matching завершается аварийно с выбрасыванием MatchException. Если такой шаблон является меткой в switch, то switch также завершится аварийно с выбрасыванием MatchException.

Если шаблон снабжён охранным выражением when и вычисление выражения when завершается аварийно, то switch завершается аварийно по той же причине.

Если ни одна метка в switch с шаблонами не совпадает со значением выражения-селектора, то switch завершается аварийно с выбрасыванием MatchException, поскольку switch с шаблонами должны быть исчерпывающими.

Например:

record R(int i){
    public int i(){      // accessor method for i
        return i / 0;
    }
}

static void exampleAnR(R r) {
    switch(r) {
        case R(var i): System.out.println(i);
    }
}

Вызов exampleAnR(new R(42)) приводит к выбрасыванию MatchException.

Напротив:

static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

Вызов example(new R(42)) приводит к выбрасыванию ArithmeticException.

Для согласования с семантикой switch с шаблонами выражения switch над классом enum теперь выбрасывают MatchException, а не IncompatibleClassChangeError, когда во время выполнения не подходит ни одна метка switch. Это небольшое несовместимое изменение языка.

Дальнейшая работа

  • Сейчас 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 для записи подобных ситуативных полиморфных вычислений приходится использовать громоздкий паттерн «Посетитель» (visitor). Pattern Matching, как правило, прозрачнее и проще.

  • Также может быть полезно добавить шаблоны AND и OR, чтобы сделать метки case с шаблонами более выразительными.

Альтернативы

  • Вместо поддержки switch с шаблонами мы могли бы определить switch по типу, который поддерживает только переключение по типу выражения-селектора. Такую возможность проще специфицировать и реализовать, но она значительно менее выразительна.

  • Существует много других синтаксических вариантов для охраняемых меток шаблонов, например p where e, p if e или даже p &&& e.

  • Альтернатива охраняемым меткам шаблонов — поддерживать охраняемые шаблоны непосредственно как особую форму шаблона, например p && e. Мы экспериментировали с этим в предыдущих версиях Preview, и возникающая при этом неоднозначность с логическими выражениями привела нас к выбору конструкций when в switch с шаблонами.

Зависимости

Этот JEP основан на Pattern Matching для instanceof (JEP 394), а также на улучшениях, которые дают выражения switch (JEP 361). Когда Preview-возможность Record Patterns (JEP 432) станет окончательной, итоговая реализация, вероятно, будет использовать динамические константы (JEP 309).