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

JEP 325: Switch Expressions (Preview)

Выражения switch, версия Preview (предварительная версия)

АвторBrian Goetz
ОтветственныйJan Lahoda
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск12
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
Связан сJEP 361: Switch Expressions
JEP 354: Switch Expressions (Second Preview)
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2017/12/04 08:56
Обновлён2022/03/11 20:22
Задача8192963

Аннотация

Расширить оператор switch так, чтобы его можно было использовать и как оператор, и как выражение, и чтобы обе формы могли использовать либо «традиционное», либо «упрощённое» поведение областей видимости и потока управления. Эти изменения упростят повседневное написание кода, а также подготовят почву для использования Pattern Matching (сопоставление с образцом) (JEP 305) в switch. В JDK 12 это возможность языка в статусе Preview.

Обратите внимание: этот JEP заменён JEP 354, запланированным в JDK 13.

Мотивация

Пока мы готовимся расширить язык программирования Java поддержкой Pattern Matching (JEP 305), ряд особенностей существующего оператора switch, которые давно раздражают пользователей, становится препятствием. К ним относятся поведение потока управления в блоках switch по умолчанию (проваливание, fall through), область видимости блоков switch по умолчанию (весь блок считается одной областью видимости) и то, что switch работает только как оператор, хотя многовариантные условия обычно естественнее выражать выражениями.

Текущий дизайн оператора switch в Java во многом повторяет такие языки, как C и C++, и по умолчанию поддерживает семантику проваливания. Такой традиционный поток управления часто полезен при написании низкоуровневого кода (например, парсеров двоичных кодировок), но по мере того как switch используется в контекстах более высокого уровня, его склонность к ошибкам начинает перевешивать его гибкость.

Например, в следующем коде многочисленные операторы break делают его излишне многословным, а этот визуальный шум часто маскирует трудно отлаживаемые ошибки, когда из-за пропущенных операторов break происходит случайное проваливание.

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

Мы предлагаем ввести новую форму метки switch, записываемую как «case L ->», которая означает, что при совпадении метки выполняется только код справа от неё. Например, предыдущий код теперь можно записать так:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

(В этом примере также используются несколько меток case: мы предлагаем поддержать несколько меток через запятую в одной метке switch.)

Код справа от метки switch «case L ->» может быть только выражением, блоком или (для удобства) оператором throw. Приятное следствие этого в том, что если ветвь вводит локальную переменную, та должна находиться внутри блока и, следовательно, не находится в области видимости других ветвей блока switch. Так устраняется ещё одно неудобство «традиционных» блоков switch, в которых областью видимости локальной переменной является весь блок switch.

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...     // Why can't I call this temp?
        break;
    default:
        int temp3 = ...     // Why can't I call this temp?
}

Многие существующие операторы switch по сути имитируют выражения switch: каждая ветвь либо присваивает значение общей целевой переменной, либо возвращает значение:

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

Выражать это оператором — окольно, многословно и чревато ошибками. Автор кода хотел выразить, что мы должны вычислить значение numLetters для каждого дня. Это должно быть возможно сказать напрямую, с помощью выражения switch, что и понятнее, и безопаснее:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

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

Описание

В дополнение к «традиционным» блокам switch мы предлагаем добавить новую «упрощённую» форму с новыми метками switch «case L ->». Если метка совпала, выполняется только выражение или оператор справа от метки со стрелкой; проваливания нет. Например, для метода:

static void howMany(int k) {
    switch (k) {
        case 1 -> System.out.println("one");
        case 2 -> System.out.println("two");
        case 3 -> System.out.println("many");
    }
}

Следующий код:

howMany(1);
howMany(2);
howMany(3);

выводит следующее:

one
two
many

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

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

Выражение switch является поли-выражением (poly expression): если целевой тип известен, он передаётся вниз в каждую ветвь. Типом выражения switch является его целевой тип, если он известен; если нет, вычисляется самостоятельный тип путём объединения типов всех ветвей case.

В большинстве выражений switch справа от метки switch «case L ->» будет стоять одно выражение. На случай, если нужен полноценный блок, мы расширили оператор break так, чтобы он принимал аргумент, который становится значением объемлющего выражения switch.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        break result;
    }
};

Выражение switch, как и оператор switch, может также использовать «традиционный» блок switch с метками switch «case L:» (что подразумевает семантику проваливания). В этом случае значения выдаются с помощью оператора break со значением:

int result = switch (s) {
    case "Foo": 
        break 1;
    case "Bar":
        break 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 0;
};

Две формы break (со значением и без) аналогичны двум формам return в методах. Обе формы return немедленно завершают выполнение метода; в методе, возвращающем не void, дополнительно нужно указать значение, которое передаётся вызвавшему метод коду. (Неоднозначности между формой break с выражением-значением и формой break с меткой можно разрешить относительно легко.)

Ветви case выражения switch должны быть исчерпывающими: для любого возможного значения должна существовать подходящая метка switch. На практике это обычно просто означает, что требуется ветвь default; однако в случае выражения switch по enum, которое охватывает все известные варианты (а в перспективе и выражений switch по sealed-типам), компилятор может вставить ветвь default, указывающую, что определение enum изменилось между временем компиляции и временем выполнения. (Сегодня разработчики делают это вручную, но если ветвь вставляет компилятор, это и менее навязчиво, и, скорее всего, даёт более информативное сообщение об ошибке, чем написанные вручную.)

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

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a break with value
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        break 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a break with value
};

Ещё одно следствие состоит в том, что операторы управления break, return и continue не могут выполнять переход сквозь выражение switch, как в следующем примере:

z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                break 1;
            case 1:
                break 2;
            default: 
                continue z; 
                // ERROR! Illegal jump through a switch expression 
        };
    ...
    }

Попутно, если представится возможность, мы можем расширить switch поддержкой переключения по примитивным типам (и их обёрточным типам), которые ранее были запрещены, таким как float, double и long.

Зависимости

Pattern Matching (JEP 305) зависит от этого JEP.