Введение
Наверное, есть только малая часть приложений, код в которых выполняются строго последовательно. Классический Hello World! как раз из таких. В таких случаях говорят, что у выполняющейся программы есть только один поток выполнения — флоу. Однако, подавляющее число приложений меняют свой поток выполнения в зависимости от внешних условий (контекста выполнения, переменных среды, значений пропертей) или внутренних (переменные, значения полей и т.д.). Для таких случаев в Java еще с самой первой версии, как и во остальных языках программирования, есть оператор if-else и его модификации.
Давайте рассмотрим пример кода, в котором в зависимости от того, кем является член семьи, он делает какую-то обязанность по дому:
enum FamilyMember { FATHER, MOTHER, SON, DAUGHTER; } FamilyMember member = FamilyMember.SON; if (member == FamilyMember.SON) { destroyFlat(); } else if (member == FamilyMember.DAUGHTER) { playSilent(); } else if (member == FamilyMember.MOTHER) { cook(); } else if (member == FamilyMember.FATHER) { helpMotherCook(); } else { doNothing(); }
Согласитесть, что из-за всех этих открывающихся/закрывающихся скобок, ключевых слов if, else, member == код становится плохо читаемым?
Для повышаемости читаемости кода (и не только — об этом чуть ниже) в Java и существует оператор switch. Рассмотрим этот же пример, но с его использованием:
enum FamilyMember { FATHER, MOTHER, SON, DAUGHTER; } FamilyMember member = FamilyMember.SON; switch (member) { case SON: destroyFlat(); break; case DAUGHTER: playSilent(); break; case MOTHER: cook(); break; case FATHER: helpMotherCook(); break; default: doNothing(); break; // можно не ставить, но правило хорошего тона }
Более читаемо, не правда ли? А теперь давайте разбираться в особенностях реализации и ограничениях.
Почему нельзя просто взять и выбросить if-else?
К сожалению, оператор switch имеет ряд существенных органичений на case-варианты:
-
В секции switch можно использовать только примитивные типы char, byte, short, int, их обертки (Char, Byte, Short, Integer), enum-ы (с Java 1.5), String (c Java 8), Object (с Java 17 — об этом чуть позже)
-
В case можно писать только константные выражения: значения примитивов (описанных выше), enum-ы (с Java 1.5), String (c Java 8), Pattern Matching (с Java 17 — об этом чуть позже)
Например, вот так написать не получится:
int i = 0; switch (i) { case < 0: ... case == 0: ... case > 0: ... } boolean b = true; switch (b) { case true: ... case false: ... } String original = "C++"; String expected = "Java"; switch (expected) { case expected: ... default: ... } int val = 10; switch (val) { case someMethodReturningInt(): ... }
Поэтому все такие и остальные случаи использования можно покрыть только с помошью if-else выражений, т.к. для них необходимо только одно условие, чтобы в if было любое выражение, возвращающее true/false.
Оператор switch до Java 1.5
Как было написано выше, до версии Java 1.5 оператор switch поддерживал только значения некоторых примитивов и их обертки: char, byte, short, int.
Поддержки boolean нет, но и смысла использованияв switch для всего двух вариантов true/false тоже никакого нет. К тому же switch c числом вариантов меньше трех выглядит уже менее читаемым по сравнению с тем же if-else или тернарным оператором. Давайте сравним (если бы была поддержка boolean):
// код ниже не скомпилируется boolean b; switch (b) { case true: doSomerthingIfTrue(); break; case false: doSomerthingIfFalse(); break; }
И без использования switch:
boolean b; if (b) { doSomerthingIfTrue(); } else { doSomerthingIfFalse(); } boolean b2; if (b2) doSomerthingIfTrue(); else doSomerthingIfFalse(); boolean b3; int result = b3 ? returnSomerthingIfTrue() : returnSomerthingIfFalse();
Еще одно преимущество оператора switch перед if-else в том, что он быстрее, т.к. для него есть специальная инструкция в bytecode JVM, которая загружает таблицу значенией case и соответствующих им действий:
|
case 1: |
doIf1() |
|
case 10: |
doIf10() |
|
case 100: |
doIf100() |
JVM загружает эту таблицу, и последовательно сравнивает значение в switch с каждым из значений в первой колонке. Это получается быстрее, чем сравнение проверяемого значения с выражением в первом if, далее переход на второй if, сравнение с выпражением в нем и.т.д.
Кроме того, разрадность левой колонки таблицы 32 бита — поэтому switch не поддерживает значения типа long, которые, как известно, 64 бита.
Поддержки float/double нет по той причине, что они в Java хранятся согласно стандарту IEEE 754, описывающем представление в формате с плавающей точкой. Увы, но точное сравнение значений в таком формате не возможно.
Синтаксис оператора switch
Общий шаблон использования оператора switch представлен ниже:
switch (expression) { case 0: actionA(); actionB(); break; case 1: actionC(); break; case 2: actionD(); actionE(); case 3: actionF(); break; case 4: actionG(); return; case 5: case 6: actionH(); break; default: actionI(); actionK(); break; }
-
Сначала проверяется expression, который может быть одним из описанных выше приметивов — это может быть как константа, значение переменной, поле, метод, возвращащий примитив и т.д.
-
Далее происходит сравнение значения с константными значениями в case — литералами, которые должны соответствовать типу expression
-
В случае expression == 0 выполнятся два действия actionA() и actionB(), далее идет break, что значит, что произойдет выход из switch и переход к инструкции после строки 22
-
В случае expression == 1 выполнится одно действие actionC() и также выход из switch
-
В случае expression == 2 выполнятся два действия actionD() и actionE(), далее т.к. не break, то флоу выполнения перейдет на строку 13, и выполнится actionF(), а потом снова выход из switch
-
В случае expression == 3 выполнится одно действие actionF() и также выход из switch, т.к. стоит break
-
В случае expression == 4 выполнится одно действие actionG(), а далее сразу выход из метода, внутри которого расположен switch (в зависимости от типа возвращаемого знаяения return может ничего не возвращать — void, либо возвращать какое-то значение)
-
В случае expression == 5 или expression == 6 выполнится одно действие actionH() и также выход из switch, т.к. стоит break
-
В случае любого другого значения expression произойдет переход в секцию default, выполнятся два действия actionI() и actionK(), и потом выход из switch. Ключевое слово break тут указывать не обязательно, но правилом хорошего тона все-таки считается указывать. Секция default не является обязательной — если нет никакого логики, связанной с ней, то ее и не нужно прописывать
Из приведенного выше шаблона следует сделать следующие выводы:
-
Нельзя забывать ставить break (наиболее частая ошибка), когда это необходимо, т.к. иначе будут выполнены все действия следующие ниже до первого break или выхода из switch
-
Внутри блоков case можно выпоплнять блоки кода, состоящие из нескольких действий
-
Из блоков case можно выходить целиком из метода, если указать return
-
Версия switch до Java 14 не поддерживает возвращение значений из блоков case (об этом ниже)
Оператор switch до Java 8
В Java 1.5 добавили enum-ы. С этой версии Java оператор switch стал их поддерживать, как это описано во Введении к этой статье. При этом не обязательно, чтобы в case были прописаны все имеющиеся значения enum. Это приводит к еще одной наиболее частой ошибке, когда добавили новое значение enum, а case для этого значения прописать забыли (если есть default, то выполнится он, что тоже не всегда ожидаемо).
Оператор switch до Java 14
С Java 8 в операторе switch добавили поддержку строк String. Теперь в switch (string) стало возможно использовать строки (а так же методы, которые их возвращают), а в case — строковые литералы. Т.к. в Java строки — это объекты, то сравнения со значениями из case происходят не по ссылке ==, а через метод Object.equals(o) — это еще одно важное изменение.
Взглянем на блок кода:
String name = "Vova"; switch (name) { case "Vova": hiVova(); break; case "Vika": hiVika(); break; default: hiStranger(); break; }
В зависимости от name, выполняется то или иное приветствие.
Оператор switch до Java 21
Все способы применения оператора switch, что я описывал ранее, называются switch-statements. Однако, при разработке приложений часто возникают такие ситуации, что в зависимости от какого-то условия нужно вернуть значение. Давайте посмотрим, как это можно было сделать до Java 21 (на самом деле до Java 17, но фича не была финальной) при помощи тернарного оператора:
public String showLight(boolean on) { return on ? "It's on" : "It's off"; }
А что, если условние не да/нет? Воспользуемся if-else:
public String getPaymentState(String orderState) String paymentState; if ("ordered".equals(orderState)) { paymentState = "pending"; } else if ("paid".equals(orderState)) { paymentState = "completed"; } else if ("cancelled".equals(orderState)) { paymentState = "cancelled"; } else { paymentState = "unknown"; } return paymentState; }
Мы видимим, что нам потребовалась дополнительная переменная paymentState, ну и вообще код не очень читаемый. Или то же самое, но с использованием switch-statements:
String paymentState; switch (orderState) { case "ordered": paymentState = "pending"; break; case "paid": paymentState = "completed"; break; case "cancelled": paymentState = "cancelled"; break; default: paymentState = "unknown"; break; } System.out.println(paymentState);
Именно для таких случаев (когда нужно возвращать значение из switch) в Java 14 ввели новый switch (старый способ использования никуда не делся), который назвается switch-expressions.
Пример выше с использование switch-expressions можно переписать вот так:
String paymentState = switch (orderState) { case "ordered" -> "pending"; case "paid" -> "completed"; case "cancelled" -> "cancelled"; default -> "unknown"; } System.out.println(paymentState);
Согласитесь, куда более приятно?
Как мы видим, двоеточие : после case заменили на ->. Результат выполнения switch можно присваивать в переменную, поле, возвращать из метода, передавать как параметр метода.
Общий шаблон использования switch-expressions приведен ниже:
String value = switch (expression) { case 0 -> "abc"; case 1 -> { String s = "def"; yield s; } case 2, 3 -> "ghi"; default -> "klm"; }
Как мы видим, шаблон использования несколько сократился по сравнению с switch-statements:
-
Если expression == 0, то value примет значение «abc»
-
Если expression == 1, то в локальную переменную s присвоится значение «abc», а далее оно будет присвоено в value
-
Если expression == 2 или expression == 3, то value примет значение «ghi». В новом синтаксисе нескольок case не пишутся друг под другом, а перечисляются через запятую
-
По умолчанию, выполнится блок default (тоже не обязательный), в результате которого value примет значение «klm»
Какие можно сделать выводы из данного синтаксиса switch-expressions:
-
Не требуется ставить break, т.к. всегда выполнится только соответствующая ветка case, а ее результат сразу вернется в переменную
-
Внутри case можно, как и раньше, присать блоки кода, но, в конце они должны содержать строку с возвратом значения — ключевое слово yield (аналог return из методов)
-
Нельзя делать выход наружу из case с помощью ключевого слова return
-
Если в качестве expression передано значение enum, то компилятором будет проверно, что в case проверены все значения этого enum. Таким образом при добавлении нового значения мы не забудем прописать его в case
Кроме того добавлена поддержка нового синтаксиса для switch-statements (aka switch-expressions):
switch (expression) { case 0 -> System.out.println("abc"); case 1 -> { String s = "def"; System.out.println(s); } case 2, 3 -> System.out.println("ghi"); default -> System.out.println("klm"); }
Основные улучешения по сравнению с классическим switch-statements:
-
Не нужно писать break, т.к. всегда выполнится только действие для сматчившегося case
-
Более компактный синтаксис
Оператор switch с Java 21
В Java 21 финально появился Pattern Matching. Что же это такое? Давайте взглянем на код — я более чем уверен, что вам миллионы раз приходилось писать что-то такое:
interface Figure { int x(); int y(); }; record Rectangle(int x, int y, int width, int height) implements Figure { } record Circle(int x, int y, int radius) implements Figure { } Figure figure = new Rectangle(0, 0, 10, 20); if (figure instanceof Rectangle) { Rectangle rectangle = (Rectangle) figure; drawRectangle(rectangle.x, rectangle.y, rectangle.width, rectangle.height); } else if (figure instanceof Circle) { Circle circle = (Circle) figure; drawCircle(circle.x, circle.y, circle.radius); }
То есть, в зависимости от того, какой пришел объект, мы вызовем соответствующий метод его отрисовки: если прямоугольник, то drawRectangle(...), если если круг, то drawCircle(...). Основная проблема здесь в том, что у нас появилось два приведения типа, которые ухудшают читаемость кода: Rectangle rectangle = (Rectangle) figure; и Circle circle = (Circle) figure;.
Паттерн матчинг как раз и предназначен для решения этой проблемы. Вот как это выглядит для классических if-else выражений:
// те же самые классы Figure figure = new Rectangle(0, 0, 10, 20); if (figure instanceof Rectangle rectangle) { drawRectangle(rectangle.x, rectangle.y, rectangle.width, rectangle.height); } else if (figure instanceof Circle cicle) { drawCircle(circle.x, circle.y, circle.radius); }
Мы добавили название переменной после имени класса в instanceof, и у нас автоматически появилась переменная требуемого типа, поэтому приводить тип теперь не нужно, и можно пользоваться этой переменной.
Теперь давайте усложним, пример. Допустим, что нам нужно отрисовывать прямоугольник только, если его верхний угол находится в левой верхней части экрана (x == 0 и y == 0):
// те же самые классы Figure figure = new Rectangle(0, 0, 10, 20); if (figure instanceof Rectangle rectangle && rectangle.x == 0 && rectangle.y == 0) { drawRectangle(rectangle.x, rectangle.y, rectangle.width, rectangle.height); } else if (figure instanceof Circle cicle) { drawCircle(circle.x, circle.y, circle.radius); }
Как мы видим, такие с такими условиями в if-else Pattern Matching тоже умеет работать.
Все то же самое валидно и для switch-statements (в новом синтаксисе — пример ниже), и для switch-expressions — для них стало возможно передавать в switch объект Object:
// те же самые классы Figure figure = new Rectangle(0, 0, 10, 20); switch (figure) { case Rectangle r && (r.x == 0 || r.y == 0) -> drawRectangle(r.x, r.y, r.width, r.height); case Circle c -> drawCircle(c.x, c.y, c.radius); default -> drawNothing(); }
Как мы видим, данный, код получился более читаемым. Заметим, что если мы хотим добавить в case проверку на какой-то родительский класс для Rectangle и Circle или же на сам интерфейс Figure, то она должна идти после перед default — иначе не скомпилируется. Догадайтесь, почему?
Шаблон switch-expressions с Pattern Matching такой:
String value = switch (obj) { case ClassA a -> "It's A"; case ClassB b && (i = 0 true && b.canHandle()) -> { b.doSomething(); yield "It's B"; } default -> "It's UNKNOWN"; }
-
Если obj instanceof ClassA, то вернется строка «It’s A»
-
Если obj instanceof ClassB и выполняются условия справа (тут могут быть любые условия, которые возвращают в итоге true/false), то выполняется действие
b.doSomething();и возвращается строка «It’s B» -
Иначе возвращается строка «It’s UNKNOWN»
Остальные все правила валидны как для обычных switch-expressions. Более того Pattern Matching доступен так же и в классическом первоначальном switch-statements (через двоеточие :) с тем лишь ограничением, что в каждом блоке case должен обязательно присутствовать break.
Заключение
Как мы видим, история развития switch оператора очень богатая — они появились в самой первой версии Java. Все началось со стандартных switch-statements, которые до появления enum в Java могли только обрабатывать примитивные типы char, byte, short и int, а также их обертки.
С появлением enum — их тоже добавили в возможность использования в switch.
В Java 8 была добавлена поддержка сравнения строк, что также очень сильно упростило разработку и сделало код более читаемым.
По-тихоньку назревала необходимость возвращать значения из switch. Это было сделано в Java 14, что потребовало изменить синтаксис switch — такие выражения получили название switch expressions.
Ну и квинтессенцией всего стала Java 21, в которой наконец-таки избаваили разработчиков писать бесконечные instanceof в if-else с последующим приведением типов, а заменить это все на более лаконичный Pattern Matching, который также добавили и в switch-expression.
Что нас ждет дальше?
Задания для самопроверки
-
Какие способы ветвления кода вы знаете до появления оператора switch?
-
Какие типы данных можно передавать в выражение switch? Какие нельзя и почему?
-
Для чего нужны ключевые слова break, yield и default?
-
Что такое switch-expressions, и в чем его отличие от switch-statements? Зачем вообще появилисись switch-expressions?
-
Можно ли скомпилировать код с switch-expression, в котором не обработаны все значения enum?
-
Какие типы выражений можно использовать в блоках case до появления Pattern Matching?
-
Какие типы выражений стало возсожным писать в блоках case после после появления Pattern Matching?
-
Каким будет switch завтра? В новых версиях Java? 🙂
-
Напишите идентичный код с помощью if-ellse, switch-statements (классический через :), switch-statements (новый через ->), switch-expressions, который использует Pattern Matching, и в зависимости от класса возвращает соответствующую строку
Ответы на вопросы (кратко, не развернуто)
-
if-else, тернарный оператор
-
char, byte, short, int, их обертки, enum, String, Object. Потому что изначально switch задумывался как табличка с 32-битной колонкой для значений case со сравнением по ссылке. hashCode() объектов тоже int — 32 бита. Сравнивать числа с плавающей точкой на == нельзя
-
break — для выхода из switch внутри блока case, yield для возвращения значения из блока case в switch-expressions, default — для дефолтной ветки кода, если ни один case не подошел
-
switch-expressions позволяют возвращать значения из блоков case в переменную, поле, из метода, в параметр метода. Появились для того, чтобы уменьшить объем бойлерпэйт-кода для этих целей, свазанного с созданием новой переменной
-
Нет, компилятор выдаст ошибку
-
Только константные выражения — литералы char, byte, short, int, String и значения enum
-
Условные выражения, которые возвращают true/false, но только через оператор &&
-
Поживем — увидим 🙂
Object obj = "I'm object"; String str = switch (obj) { case String s -> "string"; case Object o -> "object"; }; switch (obj) { case String s: str = "string"; break; case Object o: str = "object"; break; } switch (obj) { case String s -> str = "string"; case Object o -> str = "object"; } if (obj instanceof String) { str = "string"; } else if (obj instanceof Object) { str = "object"; } System.out.println(str);
ссылка на оригинал статьи https://habr.com/ru/articles/838890/
Добавить комментарий