Классический switch — оператор с проваливанием: забыли break, и выполнение утекло в следующую ветку. С Java 14 switch может быть выражением: он возвращает значение, ветки записываются через стрелку, и проваливания больше нет.
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
static int workingHours(Day day) {
return switch (day) {
case SAT, SUN -> 0;
case FRI -> 7;
default -> 8;
};
}
Три вещи стоит запомнить. Несколько меток перечисляются через запятую. Справа от стрелки стоит выражение, блок в фигурных скобках или throw. Если ветке нужен блок, значение возвращают словом yield:
String label = switch (code) {
case 200 -> "OK";
case 404 -> "Не найдено";
default -> {
var category = code / 100;
yield "Код класса " + category + "xx";
}
};
Выражение обязано покрыть все случаи. Для перечисления это значит: либо перечислены все константы, либо есть default. Компилятор проверяет полноту, и это одно из главных преимуществ по сравнению с цепочкой if.
Старый код проверял тип и тут же приводил объект, дублируя имя типа. С Java 16 переменную можно объявить прямо в проверке:
// Было
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Стало
if (value instanceof String s) {
System.out.println(s.length());
}
Переменная s видна там, где компилятор уверен, что проверка прошла. Это работает и в отрицательной форме: после if (!(value instanceof String s)) return; переменная s доступна ниже по тексту метода.
Java 21 сделала стандартом сопоставление с образцом в switch: ветки могут проверять тип и сразу связывать переменную. Это заменяет лестницы из instanceof:
static String describe(Object value) {
return switch (value) {
case null -> "ничего";
case Integer i when i < 0 -> "отрицательное число " + i;
case Integer i -> "число " + i;
case String s when s.isBlank() -> "пустая строка";
case String s -> "строка длиной " + s.length();
case int[] array -> "массив из " + array.length + " чисел";
default -> "что-то другое: " + value.getClass().getSimpleName();
};
}
Разберём новшества по порядку:
case null обрабатывает null явно. Без него switch по объекту бросит NullPointerException, как и раньше.when добавляет к образцу условие. Такие ветки называют охраняемыми.Integer — и получите ошибку компиляции «this case label is dominated».Образец может разбирать запись на компоненты. Это называется record pattern и тоже входит в стандарт Java 21:
record Point(int x, int y) {}
record Line(Point from, Point to) {}
static String where(Object shape) {
return switch (shape) {
case Point(int x, int y) when x == 0 && y == 0 -> "начало координат";
case Point(int x, int y) -> "точка (" + x + ", " + y + ")";
case Line(Point(var x1, var y1), Point(var x2, var y2)) ->
"отрезок из (" + x1 + ", " + y1 + ") в (" + x2 + ", " + y2 + ")";
default -> "неизвестная фигура";
};
}
Образцы вкладываются друг в друга, а типы компонентов можно заменить на var, если они очевидны. Деконструкция работает и в instanceof: if (shape instanceof Point(var x, var y)).
Если тип закрыт — перечисление или sealed-иерархия из следующего урока — компилятор сам знает все варианты, и default не нужен. Более того, его лучше не писать: тогда при появлении нового варианта компилятор укажет на каждый switch, который перестал быть полным. Это превращает добавление варианта из поиска по всему проекту в список ошибок компиляции.
Частая ошибка
Смешивать стрелочные и двоеточные ветки в одном switch нельзя. Выберите форму со стрелками: она не проваливается и читается однозначно. Старую форму с case X: и break оставьте только для поддержки унаследованного кода.
Соберём мини-интерпретатор, который получает строку и выполняет команду. Здесь сочетаются switch по строкам, охраняемые ветки и деконструкция:
record Command(String name, String argument) {}
static Command parse(String line) {
var parts = line.strip().split("\\s+", 2);
return new Command(parts[0].toLowerCase(), parts.length > 1 ? parts[1] : "");
}
static String run(Command command) {
return switch (command) {
case Command(var name, var arg) when name.equals("echo") -> arg;
case Command(var name, var arg) when name.equals("upper") -> arg.toUpperCase();
case Command(var name, var arg) when name.equals("len") -> String.valueOf(arg.length());
case Command c -> "неизвестная команда: " + c.name();
};
}
System.out.println(run(parse("upper фортуна"))); // ФОРТУНА
System.out.println(run(parse("len три слова тут"))); // 13
workingHours так, чтобы у него не было default, а все семь дней были перечислены. Добавьте в перечисление новый элемент и посмотрите, что скажет компилятор.String classify(Object o), который различает null, чётные и нечётные Integer, пустые и непустые String, а для остальных объектов возвращает имя класса.Circle(double radius) и Rect(double w, double h) и посчитайте площадь через switch с деконструкцией. Подумайте, что произойдёт, если передать объект другого типа.