Полное руководство по switch-выражениям в Java 12

Старый добрый switch был в Java с первого дня. Мы все используем его и привыкли к нему — особенно к его причудам. (Кого-нибудь еще раздражает break ?) Но теперь все начинает меняться: в Java 12 switch вместо оператора стал выражением:
В switch появилась возможность возвращать результат своей работы, который можно присвоить переменной; вы также можете использовать синтаксис в стиле «лямбда», который позволяет избавиться от сквозного прохода по всем case , в которых нет оператора break .
В этом руководстве я расскажу Вам обо всем, что необходимо знать о switch-выражениях в Java 12.
Предварительный обзор
Это означает, что данная управляющая конструкция может быть изменена в будущих версиях спецификации языка.
Что бы начать использовать новую версию switch необходимо применить опцию командной строки —enable-preview как во время компиляции, так и во время запуска программ (также необходимо использовать —release 12 при компиляции — примечание переводчика).
Так что имейте ввиду, что switch, как выражение, не имеет на данный момент окончательного варианта синтаксиса в Java 12.
Если у вас возникло желание поиграть со всем этим самим, то вы можете посетить мой демо-проект Java X на гитхабе.
Проблема с операторами в switch
Прежде, чем мы перейдем к обзору нововведений в switch, давайте быстро оценим одну ситуацию. Допустим, мы столкнулись с «ужасным» тернарным булеаном и хотим преобразовать его в обычный булеан. Вот один из способов сделать это:
Согласитесь, что это очень неудобно. Как и многие другие варианты switch, встречающиеся в «природе», представленный выше пример просто вычисляет значение переменной и присваивает его, но реализация обходная (объявляем идентификатор result и используем его позже), повторяющаяся (мои break ‘и всегда результат copy-pasta) и подвержена ошибкам (забыл еще одну ветку? Ой!). Тут явно есть, что улучшить.
Давайте попробуем решить эти проблемы, поместив switch в отдельный метод:
Так намного лучше: отсутствует фиктивная переменная, нет break ‘ов, загромождающих код и сообщений компилятора об отсутствии default (даже если в этом нет необходимости, как в данном случае).
Но, если подумать, то мы не обязаны создавать методы только для того, чтобы обойти неуклюжую особенность языка. И это даже без учёта, что такой рефакторинг не всегда возможен. Нет, нам нужно решение получше!
Представляем switch-выражения!
Как я показал в начале статьи, начиная с Java 12 и выше, вы можете решить вышеуказанную проблему следующим образом:
Я думаю, что это довольно очевидно: если ternartBool равен TRUE , то result ‘у будет присвоено true (иными словами TRUE превращается в true ). FALSE становится false .
Сразу возникают две мысли:
- switch может иметь результат;
- что там со стрелками?
Прежде чем углубляться в детали новых возможностей switch, в начале я расскажу об этих двух основных аспектах.
Выражение или оператор
Возможно, вы удивлены, что switch теперь является выражением. А чем же он был до этого?
До Java 12 switch был оператором — императивной конструкцией, регулирующей поток управления.
Думайте о различиях старой и новой версии switch, как о разнице между if и тернарным оператором. Они оба проверяют логическое условие и выполняют ветвление в зависимости от его результата.
Разница в том, что if просто выполняет соответствующий блок, тогда как тернарный оператор возвращает какой-то результат:
То же самое для switch: до Java 12, если вы хотели вычислить значение и сохранить результат, то должны были либо присвоить его переменной (а затем break ), либо вернуть из метода, созданного специально для оператора switch .
Теперь же всё выражение оператора switch оценивается (выбирается для выполнения соответствующая ветка), и результат вычислений может быть присвоен переменной.
Еще одним отличием между выражением и оператором является то, что выражение switch, поскольку оно является частью оператора, должно заканчиваться точкой с запятой, в отличие от классического оператора switch.
Стрелка или двоеточие
В вводном примере использовался новый синтаксис в лямбда-стиле со стрелкой между меткой и выполняющейся частью. Важно понимать, что для этого не обязательно использовать switch в качестве выражения. Фактически, пример ниже эквивалентен приведенному в начале статьи коду:
Обратите внимание, что теперь вы можете использовать break со значением! Это идеально согласуется с инструкциями switch старого стиля, которые используют break без какого-либо значения. Так в каком случае стрелка означает выражение вместо оператора, для чего она здесь? Просто хипстерский синтаксис?
Исторически сложилось, что метки с двоеточием просто отмечают точку входа в блок операторов. С этого места начинается выполнение всего кода ниже, даже когда встречается другая метка. В switch нам это известно, как сквозной переход к следующему case (fall-through): метка case определяет, куда перепрыгивает поток управления. Для его завершения нужен break или return .
В свою очередь, использование стрелки означает, что будет выполнен только блок справа от нее. И никакого «проваливания».
Подробнее об эволюции switch
Несколько меток на case
До сих пор каждый case содержал только одну метку. Но теперь все изменилось — один case может соответствовать нескольким меткам:
Поведение должно быть очевидным: TRUE и FALSE приводят к одному и тому же результату — вычисляется выражение «sane».
Это довольно приятное нововведение, которое пришло на смену множественному использованию case , когда требовалось реализовать сквозной переход к следующему case .
Типы за пределами Enum
Все примеры со switch в этой статье используют enum . А как насчет других типов? Выражения и операторы switch также могут работать с String , int , (проверьте документацию) short , byte , char и их обертками. Здесь пока что ничего не изменилось, хотя идеи об использовании таких типов данных, как float и long по прежнему остаются в силе (со второго по последний абзац).
Подробнее о стрелке
Давайте рассмотрим два свойства, характерных для стрелочной формы записи разделителя:
- отсутствие сквозного перехода к следующему case ;
- блоки операторов.
Отсутствие сквозного перехода к следующему case
Вот, что говорится в JEP 325 об этом:
Я полностью согласен и приветствую возможность использовать switch без поведения по умолчанию:
Важно усвоить, что это не имеет никакого отношения к тому, используете ли вы switch в качестве выражения или оператора. Решающим фактором тут является стрелка против двоеточия.
Блоки операторов
Как и в случае с лямбдами, стрелка может указывать либо на один оператор (как выше), либо на блок, выделенный фигурными скобками:
Блоки, которые приходится создавать для многострочных операторов имеют дополнительное преимущество (что не требуется при применении двоеточия), которое заключается в том, что для использования одинаковых имен переменных в разных ветках, switch не требует специальной обработки.
В случае, если вам показался необычным способ выхода из блоков с помощью break , а не через return , то не переживайте — меня это тоже озадачило и показалось странным. Но потом я задумался и пришел к выводу, что это имеет смысл, поскольку сохраняет старый стиль конструкции switch , которая использует break без значений.
Подробнее о выражениях switch
И последнее, но не менее важное — особенности использования switch в качестве выражения:
- множественные выражения;
- ранний возврат (досрочный return );
- охват всех значений.
Обратите внимание, что при этом не имеет значения, какая форма используется!
Множественные выражения
Switch-выражения являются множественными выражениями. Это означает, что они не имеют своего собственного типа, но могут быть одним из нескольких типов. Наиболее часто в качестве таких выражений используются лямбда-выражения: s -> s + » » , могут быть Function<String, String> , но также могут быть Function<Serializable, Object> или UnaryOperator<String> .
С помощью switch-выражений тип определяется по взаимодействию между тем, где используется switch и типами его веток. Если switch-выражение назначается типизированной переменной, передается в качестве аргумента или иным образом используется в контексте, где известен точный тип (это называется целевым типом), то все его ветки должны соответствовать этому типу. Вот что мы делали до сих пор:
Как итог — switch присваивается переменной result типа String . Следовательно, String является целевым типом, и все ветки должны возвращать результат типа String .
То же самое происходит и здесь:
А что произойдет сейчас?
(Про использование типа var читайте в нашей прошлой статье 26 рекомендаций по использованию типа var в Java — примечание переводчика)
Если целевой тип неизвестен, из-за того, что мы используем var, тип вычисляется путем нахождения наиболее конкретного супертипа из типов, создаваемых ветками.
Ранний возврат
Следствием различия между выражением и оператором switch является то, что вы можете использовать return для выхода из оператора switch :
… вы не можете использовать return внутри выражения …
Это имеет смысл независимо от того, используете ли вы стрелку или двоеточие.
Покрытие всех вариантов
Если вы используете switch в качестве оператора, тогда не имеет значения, охвачены все варианты или нет. Конечно, вы можете случайно пропустить case , и код будет работать неправильно, но компилятору все равно — вы, ваша IDE и ваши инструменты анализа кода останетесь с этим наедине.
Switch-выражения усугубляют эту проблему. Куда следует перейти switch , если нужная метка отсутствует? Единственный ответ, который может дать Java — это возвращать null для ссылочных типов и значение по умолчанию для примитивов. Это породило бы множество ошибок в основном коде.
Чтобы предотвратить такой исход, компилятор может помочь вам. Для switch-выражений компилятор будет настаивать, чтобы все возможные варианты были охвачены. Давайте посмотрим на пример, который может привести к ошибке компиляции:
Интересным является следующее решение: добавление ветки default , конечно, исправит ошибку, но это не является единственным решением — еще можно добавить case для FALSE .
Да, компилятор наконец-то сможет определить, охватываются ли все значения enum (исчерпывают ли все варианты), и не установить бесполезные значения по умолчанию! Давайте посидим минуту в безмолвной благодарности.
Хотя, это все же вызывает один вопрос. Что делать, если кто-то возьмет и превратит сумасшедший Bool в кватернионный Boolean, добавив четвертое значение? Если вы перекомпилируете switch-выражение для расширенного Bool, то получите ошибку компиляции (выражение больше не является исчерпывающим). Без перекомпиляции это превратится в проблему во время выполнения. Чтобы отловить эту проблему, компилятор переходит в ветку default , которая ведет себя так же, как та, которую мы использовали до сих пор, вызывая исключение.
В Java 12 охват всех значений без ветки default работает только для enum , но когда switch в будущих версиях Java станет более мощным, он также сможет работать с произвольными типами. Если метки case ‘ов смогут не только проверять равенство, но и проводить сравнения (например _ < 5 -> …) — это позволит охватить все варианты для числовых типов.
Размышление
Из статьи мы узнали, что Java 12 превращает switch в выражение, наделяя его новыми возможностями:
- теперь один case может соответствовать нескольким меткам;
- новая стрелочная форма case … -> … следует синтаксису лямбда-выражений:
- допускаются однострочные операторы или блоки;
- предотвращается сквозной переход к следующему case ;
Куда это нас приведет? Во-первых, поскольку это не окончательная версия switch , у вас все еще есть время, чтобы оставить отзыв в списке рассылки Amber, если вы с чем-то не согласны.
Затем, предполагая, что switch остается таким, каким он является в данный момент, я думаю, что стрелочная форма станет новым вариантом по умолчанию. Без сквозного перехода к следующему case и с лаконичными лямбда-выражениями (это очень естественно иметь case и один оператор в одной строке) switch выглядит намного компактнее и не ухудшает читаемость кода. Я уверен, что буду использовать только двоеточие, если у меня возникнет необходимость в сквозном проходе.
Оператор switch
Вторым оператором выбора в C# является оператор switch , который обеспечивает многонаправленное ветвление программы. Следовательно, этот оператор позволяет сделать выбор среди нескольких альтернативных вариантов дальнейшего выполнения программы. Несмотря на то, что многонаправленная проверка может быть организована с помощью последовательного ряда вложенных операторов if , во многих случаях более эффективным оказывается применение оператора switch . Этот оператор действует следующим образом. Значение выражения последовательно сравнивается с константами выбора из заданного списка. Как только будет обнаружено совпадение с одним из условий выбора, выполняется последовательность операторов, связанных с этим условием. Ниже приведена общая форма оператора switch .
Заданное выражение в операторе switch должно быть целочисленного типа ( char , byte , short или int ), перечислимого или же строкового. (О перечислениях и символьных строках типа string речь пойдет далее в этом курсе.) А выражения других типов, например с плавающей точкой, в операторе switch не допускаются.
Зачастую выражение, управляющее оператором switch , просто сводится к одной переменной. Кроме того, константы выбора должны иметь тип, совместимый с типом выражения. В одном операторе switch не допускается наличие двух одинаковых по значению констант выбора.
Последовательность операторов из ветви default выполняется в том случае, если ни одна из констант выбора не совпадает с заданным выражением. Ветвь default не является обязательной. Если же она отсутствует и выражение не совпадает ни с одним из условий выбора, то никаких действий вообще не выполняется. Если же происходит совпадение с одним из условий выбора, то выполняются операторы, связанные с этим условием, вплоть до оператора break .
Ниже приведен пример программы, в котором демонстрируется применение оператора switch .
Результат выполнения этой программы выглядит следующим образом.
Как видите, на каждом шаге цикла выполняются операторы, связанные с совпадающей константой выбора, в обход всех остальных операторов. Когда же значение переменной i становится равным или больше пяти, то оно не совпадает ни с одной из констант выбора, а следовательно, выполняются операторы из ветви default .
В приведенном выше примере оператором switch управляла переменная i типа int . Как пояснялось ранее, для управления оператором switch может быть использовано выражение любого целочисленного типа, включая и char . Ниже приведен пример применения выражения и констант выбора типа char в операторе switch .
Вот какой результат дает выполнение этой программы.
Обратите в данном примере внимание на отсутствие ветви default в операторе switch . Напомним, что ветвь default не является обязательной. Когда она не нужна, ее можно просто опустить.
Переход последовательности операторов, связанных с одной ветвью case , в следующую ветвь case считается ошибкой, поскольку в C# должно непременно соблюдаться правило недопущения «провалов» в передаче управления ходом выполнения программы. Именно поэтому последовательность операторов в каждой ветви case оператора switch оканчивается оператором break . (Избежать подобных «провалов», можно также с помощью оператора безусловного перехода goto , рассматриваемого далее в этой главе, но для данной цели чаще применяется оператор break .)
Когда в последовательности операторов отдельной ветви case встречается оператор break , происходит выход не только из этой ветви, но из всего оператора switch , а выполнение программы возобновляется со следующего оператора, находящегося за пределами оператора switch . Последовательность операторов в ветви default также должна быть лишена “провалов”, поэтому она завершается, как правило, оператором break .
Правило недопущения «провалов» относится к тем особенностям языка C#, которыми он отличается от C, C++ и Java. В этих языках программирования одна ветвь case может переходить (т. е. «проваливаться») в другую. Данное правило установлено в C# для ветвей case по двум причинам.
Во-первых, оно дает компилятору возможность свободно изменять порядок следования последовательностей операторов из ветвей case для целей оптимизации. Такая реорганизация была бы невозможной, если бы одна ветвь case могла переходить в другую.
И во-вторых, требование завершать каждую ветвь case явным образом исключает непроизвольные ошибки программирования, допускающие переход одной ветви case в другую.
Несмотря на то, что правило недопущения «провалов» не допускает переход одной ветви case в другую, в двух или более ветвях case все же разрешается ссылаться с помощью меток на одну и ту же кодовую последовательность, как показано в следующем примере программы.
Ниже приведен результат выполнения этой программы.
Если значение переменной i в данном примере равно 1, 2 или 3, то выполняется первый оператор, содержащий вызов метода WriteLine() . Такое расположение нескольких меток ветвей case подряд не нарушает правило недопущения «провалов»; поскольку во всех этих ветвях используется одна и та же последовательность операторов.
Расположение нескольких меток ветвей case подряд зачастую применяется в том случае, если у нескольких ветвей имеется общий код. Благодаря этому исключается излишнее дублирование кодовых последовательностей.
switch
Инструкция switch сравнивает выражение со случаями, перечисленными внутри неё , а затем выполняет соответствующие инструкции.
Синтаксис
Описание
Если выражение соответствует какому-то случаю, то выполняются инструкции этого случая. Если несколько случаев соответствуют значению, только первый случай будет использован.
Сначала программа пытается найти подходящий случай, значение которого равно значению искомого выражения (используется строгое сравнение, ===) и затем выполняет инструкции, соответствующие случаю. Если подходящего случая нет, ищется случай по умолчанию ( default ), который не является обязательным. Если случая по умолчанию нет, выполнение продолжается на инструкции, следующей сразу после switch . По соглашению, случай default описывается последним, но это не является строгим правилом.
Опциональная инструкция break выполняет выход из блока switch . Она может располагаться в каждом из случаев, но не является обязательной. Если её нет, то выполняется следующая инструкция из блока switch .
Примеры
Пример: Использование switch
В этом примере, если expr равно «Bananas», программа находит случай «Bananas» и выполняет соответствующие инструкции. При выполнении инструкции break , выполнение продолжится за пределами switch . Если бы break не было, то выполнились бы инструкции случая «Cherries».
Пример: Что случится, если не использовать break?
Если вы не использовали инструкцию break , то будут выполнены инструкции следующего случая. И проверка на соответствие выражению не будет выполняться.
Цепочки case
Одна операция
Этот метод использует тот факт, что после case нет прерывания и продолжится выполнение следующего case независимо от того, соответствует ли case предоставленному условию. Подробнее в примере «Что случится, если не использовать break?.
Это пример case с одной операцией, где четыре разных значения отрабатывают одинаково.
Цепочка операций
Это пример множественных операций внутри case , где в зависимости от предоставленного числа можно увидеть разный вывод. Здесь показывается, что операции отрабатывают в том порядке, в котором расположены case . При этом числовая последовательность может не соблюдаться. Также возможно примешать в case строки.