Урок 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

Попробуйте сами

  1. Перепишите метод workingHours так, чтобы у него не было default, а все семь дней были перечислены. Добавьте в перечисление новый элемент и посмотрите, что скажет компилятор.
  2. Напишите метод String classify(Object o), который различает null, чётные и нечётные Integer, пустые и непустые String, а для остальных объектов возвращает имя класса.
  3. Объявите записи Circle(double radius) и Rect(double w, double h) и посчитайте площадь через switch с деконструкцией. Подумайте, что произойдёт, если передать объект другого типа.