Новый синтаксис оператора switch в Java 12
Java 12 привносит новый вариант использования оператора switch . В случаях, если нам нужно сделать присвоение какой-либо переменной в зависимости от нужного условия в case , мы можем использовать новый синтаксис.
Как мы писали на Java конструкции с оператором switch? Примерно так:
public class SwitchOperatorOld < public static void main(String[] args) < // Так было до Java 12 Weekdays weekday = FRIDAY; String typeOfDay; switch (weekday) < case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "будний день"; break; case SATURDAY: case SUNDAY: typeOfDay = "выходной день"; break; default: throw new IllegalStateException("Неизвестный день недели!" + weekday); >System.out.println(weekday.getName() + " это " + typeOfDay); > >
public enum Weekdays < MONDAY("Понедельник"), TUESDAY("Вторник"), WEDNESDAY("Среда"), THURSDAY("Четверг"), FRIDAY("Пятница"), SATURDAY("Субботу"), SUNDAY("Воскресенье"); private final String name; Weekdays(String name) < this.name = name; >public String getName() < return name; >>
Как видите, множество значений в строках case существенно увеличивают общую высоту блока switch , снижая читаемость и повышая вероятность допустить ошибку при поддержке такого кода.
С новым синтаксическим сахаром в виде упрощённого оператора switch мы теперь можем писать куда более простые и удобочитаемые конструкции:
public class SwitchOperatorNew < public static void main(String[] args) < // Нововведение в Java 12 Weekdays weekday = FRIDAY; String typeOfDay = switch (weekday) < case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY ->"будний день"; case SATURDAY, SUNDAY -> "выходной день"; default -> throw new IllegalStateException("Неизвестный день недели!" + weekday); >; System.out.println(weekday.getName() + " это " + typeOfDay); > >
Заметьте, что теперь мы маппим одно или несколько сравниваемых значений сразу на возвращаемое значение, упраздняя использование оператора break . А это, в свою очередь, снижает вероятность допустить ошибку.
Также мы можем использовать сразу несколько сравниваемых значений в блоке case , что повышает читаемость кода. Ведь мы может использовать несколько значение в одну строку и не раздувать тем самым код по высоте.
Новый синтаксис оператоа switch в Java 12 позволяет писать более краткий код. При этом читаемость нисколько не снижается, а наоборот — улучшается.
Java switch case lambda
Старый добрый switch был в Java с первого дня. Мы все используем его и привыкли к нему — особенно к его причудам (кого-нибудь еще раздражает break?). Но начиная с Java 12, ситуация начала меняться: switch вместо оператора стал выражением:
boolean result = switch(ternaryBool) < case TRUE ->true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default -> throw new IllegalArgumentException("Seriously?!"); >;
Результат работы switch-выражения теперь можно сохранять в переменную; ушла необходимость использовать break в каждой ветке case благодаря лямбда-синтаксису и многое другое.
- оператор или выражение (с Java 14)
- двоеточия или стрелки (с Java 14)
- метки или шаблоны (3-й превью в Java 19)
В этом руководстве я расскажу обо всем, что необходимо знать о switch-выражениях, и как их лучше всего использовать в современной Java.
Прежде, чем мы перейдем к обзору нововведений, давайте рассмотрим один пример кода. Допустим, мы столкнулись с «ужасным» тернарным boolean и хотим преобразовать его в обычный boolean. Вот один из способов сделать это:
boolean result; switch(ternaryBool) < case TRUE: result = true; break; case FALSE: result = false; break; case FILE_NOT_FOUND: // объявление переменной для демонстрации проблемы в default var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // А вот и проблема: мы не можем объявить еще одну переменную с именем ex var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; >
Реализация данного кода хромает: наличие break в каждой ветке, которые легко забыть; можно не учесть все возможные значения ternaryBool (забыть реализовать какой-то case); с переменной result не все гладко — область видимости не соответствует ее использованию; нельзя объявить в разных ветках переменные с одинаковым именем. Согласитесь, что данное решение выглядит крайне громоздко и неудобно — тут явно есть, что улучшить.
private static boolean toBoolean(Bool ternaryBool) < switch(ternaryBool) < case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); // без default метод не скомпилируется default: throw new IllegalArgumentException("Seriously?!"); >>
Так намного лучше: отсутствует фиктивная переменная result, нет break, загромождающих код и сообщений компилятора об отсутствии default (даже если в этом нет необходимости, как в данном случае).
Но если подумать, то мы не обязаны создавать методы только для того, чтобы обойти неуклюжую особенность языка. И это даже без учёта, что такой рефакторинг не всегда возможен. Нет, нам нужно решение получше!
boolean result = switch(ternaryBool) < case TRUE ->true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); // в ветке `default` уже нет необходимости default -> throw new IllegalArgumentException("Seriously?!"); >;
Я думаю, что это довольно очевидно: если ternartBool равен TRUE, то result будет присвоено true, а FALSE становится false.
Возможно, вы удивлены, что switch теперь является выражением. А чем же он был до этого? До Java 12 switch был оператором — императивной конструкцией, управляющей исполняющим потоком.
Думайте о различиях старой и новой версии switch, как о разнице между if и тернарным оператором. Они оба проверяют логическое условие и выполняют ту или иную ветку в зависимости от его результата.
Разница состоит в том, что if просто выполняет соответствующий блок, тогда как тернарный оператор возвращает какой-то результат:
if(condition) < result = doThis(); >else < result = doThat(); >result = condition ? doThis() : doThat();
То же самое и у switch: до Java 12, если вы хотели вычислить значение и сохранить результат, то должны были либо присвоить его переменной, либо вернуть из метода, созданного специально для оператора switch.
Еще одно отличие заключается в том, что поскольку выражение является частью оператора, то оно должно заканчиваться точкой с запятой, в отличие от классического оператора switch.
В самом начале статьи использовался пример с новым синтаксисом в лямбда-стиле со стрелкой между меткой и выполняющейся частью. Эквивалентный ему код без лямбда-стиля можно записать так:
boolean result = switch (ternaryBool) < case TRUE: yield true; case FALSE: yield false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!"); >;
Обратите внимание, что вам нужно использовать новое ключевое слово yield, чтобы вернуть значение из ветки case (этот синтаксис появился в Java 13. В Java 12 вместо yield применялся break, т. е. break true вместо yield true, что выглядело странно).
Исторически сложилось, что метки с двоеточием определяют точку входа в блок операторов. С этого места начинается выполнение всего кода ниже, даже когда встречается другая метка (при отсутствии break). Механизм такой работы известен, как сквозной переход к следующему case. Для его прерывания нужен break или return.
Отсутствие break в case часто используется для применения одинакового поведение к веткам с разными метками. При этом программа будет переходить к следующему case, пока не наткнется на break. Из этого можно сделать вывод, что оператор switch в каждом case поддерживает наличие только одной метки:
String result = switch(ternaryBool) < case TRUE, FALSE ->"sane"; default -> "insane"; >;
Поведение этого кода очевидно: TRUE и FALSE приводят к одному и тому же результату — вычисляется выражение «sane».
switch (number) < case 1 ->callMethod("one"); case 2 -> callMethod("two"); default -> callMethod("many"); >
«…Текущий дизайн оператора switch в Java тесно связан с такими языками, как C и C++ и по умолчанию поддерживает сквозную семантику. Хотя этот традиционный способ управления часто полезен для написания низкоуровневого кода (такого как парсеры для двоичного кодирования), поскольку switch используется в коде более высокого уровня, ошибки такого подхода начинают перевешивать его гибкость.»
switch(ternaryBool) < case TRUE, FALSE ->System.out.println("Bool was sane"); default -> System.out.println("Bool was insane"); >;
Стрелка позволяет вывести «Bool was sane» в единственном экземпляре, в то время, как с двоеточием это же сообщение отобразилось бы дважды.
Как и в случае с лямбдами, стрелка может указывать либо на один оператор (как выше), либо на блок, выделенный фигурными скобками:
boolean result = switch (Bool.random()) < case TRUE -> < System.out.println("Bool true"); yield true; >case FALSE -> < System.out.println("Bool false"); yield false; >case FILE_NOT_FOUND -> < var ex = new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); throw ex; >default -> < var ex = new IllegalArgumentException( "Seriously?!"); throw ex; >>;
Блоки необходимы для использования более одной строки кода в case. При этом они имеют дополнительное преимущество — позволяют создавать одинаковые имена переменных в разных ветках за счет локальной области видимости для каждой ветки.
Если вам показался необычным способ выхода из блоков с помощью yield, а не return, то это необходимо, чтобы избежать путаницы: return может быть неправильно истолкован, как выход из метода. Мы лишь завершаем работу switch, оставаясь в том же методе.
- множественные выражения
- ранний возврат
- охват всех значений (исчерпываемость)
Switch-выражения являются множественными выражениями. Это означает, что они не имеют своего собственного типа, но могут быть одним из нескольких типов. Наиболее часто в качестве таких выражений используются лямбда-выражения: s -> s + » «, могут быть и Function , и Function или UnaryOperator .
Тип switch-выражения определяется исходя из типов его веток, а также из места его использования. Если результат работы switch-выражения присваивается типизированной переменной, передается в качестве аргумента или используется в контексте, где известен точный тип (целевой тип), то все его ветки должны соответствовать этому типу. Вот, что мы делали до сих пор:
String result = switch (ternaryBool) < case TRUE, FALSE ->"sane"; default -> "insane"; >;
Как итог — switch присваивается переменной String result. Следовательно, String является целевым типом, и все ветки должны возвращать результат этого типа.
Serializable serializableMessage = switch (bool) < case TRUE, FALSE ->"sane"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); >;
// compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) < case TRUE, FALSE ->"sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); >;
Если целевой тип неизвестен из-за использования var, то он вычисляется путем нахождения наиболее конкретного супертипа из типов, создаваемых ветками.
Следствием различия между выражением и оператором switch является то, что вы можете использовать return для выхода из оператора switch:
public String sanity(Bool ternaryBool) < switch (ternaryBool) < // `return` is only possible from block case TRUE, FALSE -> < return "sane"; >default -> < return "This is ridiculous!"; >>; >
public String sanity(Bool ternaryBool) < String result = switch (ternaryBool) < // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> < return "sane"; >default -> < return "This is ridiculous!"; >>; >
Если вы используете switch в качестве оператора, тогда не имеет значения, охвачены все варианты или нет. Конечно, вы можете случайно пропустить case, и код будет работать неправильно, но компилятору все равно — вы, ваша IDE и ваши инструменты анализа кода останетесь с этим наедине.
Switch-выражения усугубляют эту проблему. Куда следует перейти switch, если нужная метка отсутствует? Единственный ответ, который может дать Java — это возвращать null для ссылочных типов и значение по умолчанию для примитивов. Это породило бы множество ошибок в основном коде.
Чтобы предотвратить такой исход, компилятор может помочь вам. Для switch-выражений компилятор будет настаивать, чтобы все возможные варианты были охвачены. Для каждого возможного значения переменной switch должна быть ветвь — это называется исчерпываемостью. Давайте посмотрим на пример, который может привести к ошибке компиляции:
// compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) < case TRUE ->true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); >;
Интересным является следующее решение: добавление ветки default, конечно, исправит ошибку, но это не является единственным решением — еще можно добавить case для FALSE.
// compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) < case TRUE ->true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); >;
Да, компилятор наконец-то сможет определить, охватываются ли все значения enum, что позволяет не использовать бесполезные значения в default!
Что касается исчерпываемости, я стараюсь избегать ветвей по умолчанию, когда это возможно, предпочитая получать ошибки компиляции, когда что-то меняется.
Хотя, это все же вызывает один вопрос. Что делать, если кто-то возьмет и превратит сумасшедший Bool в кватернионный (с четырьмя значениями) Boolean, добавив четвертое значение? Если вы перекомпилируете switch- выражение для расширенного Bool, то получите ошибку компиляции (т. к. выражение больше не будет исчерпывающим). Чтобы отловить эту проблему, компилятор переходит в ветку default, которая ведет себя так же, как та, которую мы использовали до сих пор, вызывая исключение.
В настоящее время охват всех значений без ветки default работает только для enum, но когда switch в будущих версиях Java станет более мощным, он также сможет работать и с произвольными типами. Если метки case смогут не только проверять равенство, но и проводить сравнения (например _ < 5 ->…) — это позволит охватить все варианты для числовых типов.