Урок 5 из 11 базовый около 35 мин
Switch-выражения и сопоставление с образцом
Switch как выражение, стрелочные ветки, проверка типов через instanceof-паттерны, деконструкция записей и охранные условия when.
Switch как выражение
Классический 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.
Проверка типа без приведения: instanceof-паттерн
Старый код проверял тип и тут же приводил объект, дублируя имя типа. С 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 доступна ниже по тексту метода.
Switch по типу
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)).
Полнота без default
Если тип закрыт — перечисление или 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с деконструкцией. Подумайте, что произойдёт, если передать объект другого типа.