Сопоставление с образцом в Java
Сопоставление с образцом в Java: instanceof, switch, образцы типов, образцы записей и деструктуризация.
Долгие годы код на Java, работающий со значениями неизвестного типа, следовал утомительному ритуалу: проверить тип с помощью instanceof, затем привести к этому типу, затем использовать. Сопоставление с образцом сворачивает этот ритуал в одно выражение. Образец описывает форму данных; если значение совпадает, Java привязывает его части к переменным, которые можно использовать сразу — без ручного приведения типов.
Сопоставление с образцом появлялось поэтапно: сначала образцы для instanceof, затем образцы в switch, затем образцы записей, деструктурирующие записи на составные части. Вместе они позволяют писать декларативный, типобезопасный код, который читается как данные, с которыми он работает.
В этой главе рассматриваются образец для instanceof, образцы типов в switch, охраняемые образцы и обработка null, а также образцы записей — и всё это объединяется в работающей программе. Глава опирается на три функции, которые вы, возможно, захотите изучить сначала: оператор instanceof, записи и выражения switch.
Сопоставление с образцом для instanceof
Классический шаблон «проверка и приведение» требовал трёх ссылок на один и тот же тип. Образец instanceof привязывает переменную одновременно с проверкой, и привязка действует везде, где проверка заведомо истинна.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Поскольку переменная привязки участвует в булевом выражении, можно продолжать сужение типа в том же if. Компилятор доказывает, что s безопасно использовать:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Образцы в switch
switch может сопоставляться с образцами типов, выбирая ветку по типу значения в момент выполнения. Каждый case привязывает совпавшее значение, поэтому тело работает с типизированной переменной напрямую. Это превращает длинные цепочки if/else instanceof в компактную читаемую таблицу.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}switch с образцами типов должен быть исчерпывающим — он обязан покрывать каждый возможный входной вариант. Для селекторов произвольного типа Object это означает ветку default; для sealed-иерархий компилятор знает полный набор подтипов и может проверить исчерпываемость без default.
Охраняемые образцы и null
Клауза when добавляет булево условие к case, позволяя двум значениям одного типа попасть в разные ветки. Это называется охраняемым образцом, и порядок важен: более конкретные охраняемые случаи идут перед неохраняемым запасным.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Традиционно switch бросал NullPointerException на null-селекторе. Образцовый switch может явно обработать null с помощью case null, оставляя проверку на null внутри той же конструкции, а не в отдельном условии перед ней.
| Функция | Синтаксис | Назначение |
|---|---|---|
| Образец типа | case String s | Сопоставление по типу и привязка |
| Охраняемый образец | case String s when s.isEmpty() | Добавление условия к case |
| Метка null | case null | Сопоставление с null-селектором |
| Образец записи | case Point(int x, int y) | Деструктуризация записи |
Образцы записей
Образец записи сопоставляется с записью и привязывает её компоненты за один шаг, избавляя от вызовов методов-аксессоров. Поскольку записи раскрывают свои компоненты, компилятор знает точную форму и позволяет именовать каждую часть прямо на месте. Образцы записей вкладываются друг в друга, позволяя деструктурировать запись из записей.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}Сопоставление с образцом особенно выразительно в сочетании с запечатанными типами: когда интерфейс перечисляет допустимые реализации, switch по ним является исчерпывающим без default, а добавление нового подтипа превращает пропущенный случай в ошибку компиляции, а не в молчаливую ошибку.
Полный работающий пример
Программа ниже объединяет все части. Она использует образец instanceof с охраной, запечатанную иерархию Shape из записей, образцы записей, деструктурирующие каждую фигуру в switch, охраняемый образец для обнаружения квадрата и case null — всё это без единого явного приведения типов.
Что следует вынести из этого примера:
describe(42)выводитpositive int 42, потому что охранаinstanceof Integer i && i > 0проверяет тип и значение вместе перед привязкойi.describe(-5)проваливается вunknown— тот же образецIntegerсовпадает по типу, но охранаi > 0не проходит, показывая, как охрана уточняет образец типа.- Ветка
areaне нуждается вdefault:Shapeзапечатан, поэтому перечислениеCircle,RectangleиTriangleявляется исчерпывающим, и компилятор удовлетворён. - Прямоугольник
5.0 x 5.0выводится какsquare side=5.0, потому что его охраняемый случайwhen w == hрасположен перед общим случаемRectangle rи выигрывает. - Последняя строка выводит
no shape: веткаcase nullобрабатываетnull-селектор внутри switch вместо того, чтобы бросатьNullPointerException.