Pattern Matching и switch
Pattern matching — одна из самых революционных возможностей, появившихся в Java начиная с 16-й версии и окончательно оформившихся в 21-й. В этом уроке мы глубоко разберём, как pattern matching для instanceof и switch избавляет код от шаблонного样板式的 проверок типов, делает его безопаснее, короче и выразительнее. Мы рассмотрим guard-паттерны с when, sealed classes с проверкой полноты, обработку null, вложенные паттерны, записи (records) и завершим практическим примером — парсером математических выражений.
Зачем нужен pattern matching?
В классической Java проверка типа всегда шла по одной и той же схеме: сначала вы проверяете объект с помощью instanceof, а затем — явно приводите его к нужному типу через каст. Это приводило к двухстрочным конструкциям, которые повторяются десятки раз в каждом проекте. Pattern matching объединяет эти два шага в один: вы не просто проверяете, является ли объект экземпляром определённого типа, но и сразу получаете переменную, уже приведённую к этому типу. Это не синтаксический сахар — это принципиальное изменение парадигмы, которое:
- Устраняет возможность ошибки приведения типа (ClassCastException) — компилятор сам следит за безопасностью
- Делает код короче и понятнее — один паттерн вместо двух строк
- Позволяет компилятору проводить более глубокий анализ — он видит, какие типы вы уже обработали и какие остались
- Позволяет задавать дополнительные условия (guard) прямо в проверке через
when - Связывается с sealed classes для проверки полноты обработки (exhaustiveness)
instanceof pattern matching: старый и новый способ
Давайте начнём с самого базового — с instanceof. До появления pattern matching код проверки типа всегда выглядел одинаково. Рассмотрим простой пример: у нас есть интерфейс Shape и несколько его реализаций — Circle, Rectangle, Triangle. Мы хотим вычислить площадь фигуры.
Ключевой момент: instanceof в Java всегда возвращал boolean. Pattern matching не меняет эту семантику — он просто добавляет возможность сразу захватить результат приведения типа в переменную. Это безопасно, потому что если instanceof вернул true, объект гарантированно является экземпляром указанного типа.
Старый способ: instanceof + каст
Раньше, чтобы безопасно работать с объектом после проверки типа, нужно было:
- Проверить объект с помощью
instanceof - Явно привести его к нужному типу в отдельной переменной
- Использовать эту переменную
Это приводило к шаблонному коду, где одна и та же структура повторяется снова и снова. Хуже того, если вы забыли проверить instanceof перед приведением — получали ClassCastException во время выполнения. Компьютер не мог помочь вам увидеть эту ошибку заранее.
public class ShapeArea {
public static double calculate(Object shape) {
double area = 0;
if (shape instanceof Circle) {
Circle c = (Circle) shape;
area = Math.PI * c.radius() * c.radius();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
area = r.width() * r.height();
} else if (shape instanceof Triangle) {
Triangle t = (Triangle) shape;
area = 0.5 * t.base() * t.height();
} else {
throw new IllegalArgumentException("Неизвестная фигура: " + shape.getClass());
}
return area;
}
public static void main(String[] args) {
Shape circle = new Circle(5.0);
Shape rect = new Rectangle(4.0, 6.0);
Shape tri = new Triangle(3.0, 8.0);
System.out.println("Площадь круга: " + calculate(circle));
System.out.println("Площадь прямоугольника: " + calculate(rect));
System.out.println("Площадь треугольника: " + calculate(tri));
}
}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
interface Shape {}
Новый способ: instanceof pattern matching
Начиная с Java 16 (и окончательно стабилизированный в Java 21), вы можете указать имя переменной прямо в выражении instanceof. Компилятор автоматически создаст переменную нужного типа и присвоит ей значение. Приведение (каст) происходит неявно, и компилятор гарантирует, что переменная будет доступна только в той ветке кода, где проверка прошла успешно.
public class ShapeArea {
public static double calculate(Object shape) {
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (shape instanceof Rectangle r) {
return r.width() * r.height();
} else if (shape instanceof Triangle t) {
return 0.5 * t.base() * t.height();
}
throw new IllegalArgumentException("Неизвестная фигура: " + shape.getClass());
}
public static void main(String[] args) {
System.out.println("Круг: " + calculate(new Circle(5.0)));
System.out.println("Прямоугольник: " + calculate(new Rectangle(4.0, 6.0)));
System.out.println("Треугольник: " + calculate(new Triangle(3.0, 8.0)));
}
}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
interface Shape {}
Обратите внимание: код стал значительно короче и безопаснее. Вместо трёх строк (проверка + каст + использование) — одна строка. И更重要的是 — компилятор теперь отслеживает, какие типы вы уже проверили. Если вы попытаетесь использовать переменную c вне ветки if, где она объявлена, компилятор выдаст ошибку. Это полностью исключает ClassCastException.
Область видимости переменной паттерна
Одна из ключевых особенностей pattern matching — переменная, созданная в паттерне, видна только в той области (scope), где компилятор гарантированно знает её тип. Это называется flow scoping (область видимости потока выполнения). Например, переменная из instanceof-паттерна доступна:
- В блоке
if, если проверка успешна - После
if-проверки, если компилятор может доказать, что объект того же типа - В тернарном операторе и других выражениях с очевидным потоком
public class ScopeDemo {
public static void process(Object obj) {
if (obj instanceof String s) {
System.out.println("Длина строки: " + s.length());
}
if (obj instanceof Integer n && n > 0) {
System.out.println("Положительное число: " + n);
}
if (obj instanceof String s) {
System.out.println("Строка в lower case: " + s.toLowerCase());
}
}
public static void main(String[] args) {
process("Привет");
process(42);
process("Java 21");
process(3.14);
}
}
Pattern matching для switch (Java 21+)
Если pattern matching для instanceof — это первый шаг, то pattern matching для switch — это полная революция. Теперь оператор switch может принимать не только целочисленные значения, строки и перечисления, но и проверять объекты по типу. Это позволяет превратить длинные цепочки if-else if в компактную и безопасную конструкцию.
Ключевые преимущества switch с pattern matching:
- Проверка полноты: компилятор требует обработать все возможные типы (для sealed classes)
- Guard-паттерны: можно добавить условие
whenк любому case - Нет проваливания: каждый case должен завершаться break или return
- Null-safety: отдельный case для null
Почему switch с pattern matching безопаснее, чем цепочка if-else? Потому что компилятор выполняет exhaustiveness checking — проверку полноты обработки. Если вы работаете с sealed class (интерфейсом или классом, который имеет фиксированное множество подтипов), компилятор убедится, что вы обработали ВСЕ возможные типы. Если вы забудете один — код не скомпилируется. Это гарантия, что ваш код не пропустит ни одного типа.
public class ShapeDescriber {
public static String describe(Shape shape) {
return switch (shape) {
case Circle c -> "Круг с радиусом " + c.radius();
case Rectangle r -> "Прямоугольник " + r.width() + "x" + r.height();
case Triangle t -> "Треугольник с основанием " + t.base() + " и высотой " + t.height();
};
}
public static void main(String[] args) {
System.out.println(describe(new Circle(5.0)));
System.out.println(describe(new Rectangle(4.0, 6.0)));
System.out.println(describe(new Triangle(3.0, 8.0)));
}
}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
interface Shape {}
Switch expression vs switch statement
В Java 14+ появилось два варианта switch: как выражение (expression) и как утверждение (statement). Switch expression возвращает значение и может использоваться в присваиваниях, ternary операторах и возвращаемых значениях. Switch statement выполняет действия, но не возвращает значение. Вот关键ные отличия:
Switch expression (выражение):
- Использует стрелку
->(в большинстве случаев) илиcase:сyield - Возвращает значение — можно присвоить переменной или вернуть из метода
- Должен обработать все возможные значения (exhaustiveness)
- Не нужен
break— каждый case автоматически завершается
Switch statement (утверждение):
- Использует
case:сbreak(классический стиль) - Выполняет действия, не возвращает значение
- Не обязательно обрабатывать все значения (default не обязателен)
- Нужен
breakдля предотвращения проваливания (fall-through)
public class SwitchDemo {
static sealed interface Expr permits Num, Add, Mul {}
record Num(double value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
static double eval(Expr expr) {
return switch (expr) {
case Num n -> n.value();
case Add a -> eval(a.left()) + eval(a.right());
case Mul m -> eval(m.left()) * eval(m.right());
};
}
static String format(Expr expr) {
return switch (expr) {
case Num n -> String.valueOf(n.value());
case Add a -> "(" + format(a.left()) + " + " + format(a.right()) + ")";
case Mul m -> "(" + format(m.left()) + " * " + format(m.right()) + ")";
};
}
public static void main(String[] args) {
Expr expr = new Add(new Num(2), new Mul(new Num(3), new Num(4)));
System.out.println("Выражение: " + format(expr));
System.out.println("Результат: " + eval(expr));
}
}
Guard-паттерны с when
Guard-паттерны (паттерны с охраной) позволяют добавить дополнительное условие к любому case в switch. Это делается с помощью ключевого слова when. Guard-паттерны особенно полезны, когда нужно различать объекты одного типа, но с разными значениями.
Синтаксис: case Типимя переменная when условие -> действие. Условие после when — это обычное булево выражение. Если условие истинно, выполняется действие case. Если ложно — проверяется следующий case.
Важно: порядок case имеет значение! Guard-паттерны проверяются сверху вниз, и компилятор требует, чтобы более специфичные паттерны шли перед более общими. Если вы сначала напишете case Integer n, а потом case Integer n when n > 0 — компилятор выдаст ошибку, потому что второй case никогда не будет достигнут.
public class GradeClassifier {
public static String classify(int score) {
return switch (score) {
case int s when s >= 90 -> "Отлично (A)";
case int s when s >= 75 -> "Хорошо (B)";
case int s when s >= 60 -> "Удовлетворительно (C)";
case int s when s >= 45 -> "Неудовлетворительно (D)";
case int s when s >= 0 -> "Незачёт (F)";
default -> "Неверный балл";
};
}
public static String classifyWithGuard(int score) {
return switch (score) {
case int s when s < 0 -> "Ошибка: отрицательный балл";
case int s when s < 45 -> "Незачёт";
case int s when s < 60 -> "Неудовлетворительно";
case int s when s < 75 -> "Удовлетворительно";
case int s when s < 90 -> "Хорошо";
case int s -> "Отлично";
};
}
public static void main(String[] args) {
int[] scores = {95, 82, 68, 51, 30, -5, 100};
for (int score : scores) {
System.out.printf("Балл %d: %s%n", score, classify(score));
}
System.out.println();
for (int score : scores) {
System.out.printf("Балл %d: %s%n", score, classifyWithGuard(score));
}
}
}
Sealed classes и проверка полноты (exhaustiveness)
Sealed classes (запечатанные классы) — это классы, которые могут быть унаследованы только определёнными подклассами. В сочетании с pattern matching для switch это создаёт мощную систему проверки полноты обработки.
Когда вы используете switch с sealed class, компилятор знает точный список всех возможных подтипов. Если вы пропустите хотя бы один case — код не скомпилируется. Это гарантирует, что ваш код обработает все возможные варианты.
Ключевые правила:
- Sealed class должен явно перечислить все свои подтипы в конструкции
permits - Каждый подтип должен быть
final,sealedилиnon-sealed - Switch expression для sealed class может не иметь
defaultcase — компилятор проверит полноту - Проверка полноты работает даже для вложенных sealed hierarchies
public class PaymentSystem {
sealed interface Payment permits CreditCard, BankTransfer, Crypto, Cash {}
record CreditCard(String number, String holder, int expMonth, int expYear) implements Payment {}
record BankTransfer(String iban, String bankName, String swift) implements Payment {}
record Crypto(String walletAddress, String currency) implements Payment {}
record Cash(double amount) implements Payment {}
public static String processPayment(Payment payment) {
return switch (payment) {
case CreditCard cc -> "Оплата картой " + cc.number().substring(0, 4) + "****";
case BankTransfer bt -> "Банковский перевод на " + bt.iban();
case Crypto cr -> "Крипто-платёж на " + cr.walletAddress().substring(0, 10) + "...";
case Cash c -> "Наличные: " + c.amount() + " руб.";
};
}
public static double calculateFee(Payment payment) {
return switch (payment) {
case CreditCard cc -> 2.5;
case BankTransfer bt -> 0.5;
case Crypto cr -> 1.0;
case Cash c -> 0.0;
};
}
public static void main(String[] args) {
Payment[] payments = {
new CreditCard("4111111111111234", "Иванов И.И.", 12, 2025),
new BankTransfer("RU12345678901234567890", "Сбербанк", "SABRRUMM"),
new Crypto("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD38", "ETH"),
new Cash(5000.0)
};
for (Payment p : payments) {
System.out.println(processPayment(p) + " (комиссия: " + calculateFee(p) + "%)");
}
}
}
Доминирование и порядок паттернов
Порядок case в switch имеет критическое значение. Компилятор проверяет, что:
- Более специфичные паттерны идут перед более общими
- Guard-паттерны с
whenидут после базовых паттернов того же типа - Не существует «мёртвых» case (case, которые никогда не будут достигнуты)
Доминирование означает, что если паттерн A «покрывает» паттерн B (то есть каждый объект, совпадающий с B, также совпадает и с A), то B должен идти перед A. Иначе B никогда не будет достигнут.
public class DominanceDemo {
static sealed interface Shape permits Circle, Rectangle, Polygon {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Polygon(int sides, double area) implements Shape {}
static String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 10 -> "Большой круг";
case Circle c -> "Маленький круг";
case Rectangle r when r.w() == r.h() -> "Квадрат со стороной " + r.w();
case Rectangle r -> "Прямоугольник " + r.w() + "x" + r.h();
case Polygon p when p.sides() == 3 -> "Треугольник";
case Polygon p -> "Многоугольник с " + p.sides() + " сторонами";
};
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(15.0),
new Circle(3.0),
new Rectangle(5.0, 5.0),
new Rectangle(3.0, 7.0),
new Polygon(3, 12.5),
new Polygon(6, 45.0)
};
for (Shape s : shapes) {
System.out.println(s + " -> " + describe(s));
}
}
}
Совет по доминированию: всегда начинайте с наиболее специфичных паттернов (с guard-ами) и продвигайтесь к более общим. Если у вас есть case для Circle c when c.radius() > 10 и просто Circle c, то первый должен идти вторым — так, чтобы более узкий случай проверялся первым.
Обработка null в switch с паттернами
В классическом switch с primitives null-значение вызывало NullPointerException. В switch с pattern matching есть специальный null-case, который позволяет безопасно обработать null. Это的重要 отличие от instanceof, который всегда возвращает false для null.
Компилятор требует, чтобы null-case шёл перед другими case для ссылочных типов. Это логично: если null обрабатывается отдельно, остальные case гарантированно работают с non-null объектами.
public class NullSafeProcessor {
static sealed interface Result permits Success, Error, Loading {}
record Success(String data) implements Result {}
record Error(String message, int code) implements Result {}
record Loading(double progress) implements Result {}
public static String formatResult(Result result) {
return switch (result) {
case null -> "Результат отсутствует";
case Success s -> "Успех: " + s.data();
case Error e -> "Ошибка [" + e.code() + "]: " + e.message();
case Loading l -> "Загрузка: " + String.format("%.1f", l.progress() * 100) + "%";
};
}
public static String safeProcess(Object input) {
return switch (input) {
case null -> "Пустой ввод";
case String s when s.isEmpty() -> "Пустая строка";
case String s -> "Строка: " + s;
case Integer n when n < 0 -> "Отрицательное: " + n;
case Integer n -> "Положительное: " + n;
case int[] arr -> "Массив из " + arr.length + " элементов";
default -> "Неизвестный тип: " + input.getClass().getSimpleName();
};
}
public static void main(String[] args) {
Result[] results = {new Success("OK"), new Error("Timeout", 504), null, new Loading(0.75)};
for (Result r : results) {
System.out.println(formatResult(r));
}
System.out.println();
Object[] inputs = {null, "", "Hello", -5, 42, new int[]{1, 2, 3}, 3.14};
for (Object obj : inputs) {
System.out.println(safeProcess(obj));
}
}
}
Pattern matching с records
Records (записи) и pattern matching идеально дополняют друг друга. Records — это неизменяемые структуры данных с автоматически генерируемыми методами equals(), hashCode() и toString(). А component records — это деструктуризация, которая позволяет извлечь компоненты record прямо в паттерне.
Когда вы используете record в switch, вы можете не только проверить тип, но и сразу получить доступ к его компонентам. Это делает код невероятно выразительным.
public class RecordPatterns {
static sealed interface Shape permits Circle, Rectangle, Group {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Group(String name, Shape... shapes) implements Shape {}
static double totalArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Group g -> {
double sum = 0;
for (Shape s : g.shapes()) {
sum += totalArea(s);
}
yield sum;
}
};
}
static String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 20 -> "Огромный круг (r=" + c.radius() + ")";
case Circle c when c.radius() > 10 -> "Большой круг (r=" + c.radius() + ")";
case Circle c -> "Круг (r=" + c.radius() + ")";
case Rectangle r when r.width() == r.height() -> "Квадрат (" + r.width() + ")";
case Rectangle r -> "Прямоугольник (" + r.width() + "x" + r.height() + ")";
case Group g -> "Группа '" + g.name() + "' (" + g.shapes().length + " фигур)";
};
}
public static void main(String[] args) {
Shape smallCircle = new Circle(5.0);
Shape bigCircle = new Circle(25.0);
Shape square = new Rectangle(10.0, 10.0);
Shape rect = new Rectangle(3.0, 7.0);
Shape group = new Group("Фигуры", smallCircle, bigCircle, square, rect);
Shape[] all = {smallCircle, bigCircle, square, rect, group};
for (Shape s : all) {
System.out.printf("%s -> %.2f%n", describe(s), totalArea(s));
}
}
}
Вложенные паттерны (nested patterns)
Вложенные паттерны — это одна из самых мощных возможностей Java 21. Вы можете деструктурировать объекты на несколько уровней глубины, извлекая компоненты вложенных record'ов прямо в паттерне. Это позволяет обрабатывать сложные иерархии данных без написания множества вложенных if-else.
Синтаксис: вы указываете тип record и в скобках перечисляете его компоненты. Если компонент тоже record — вы можете деструктурировать его аналогично. Это работает до любой глубины вложенности.
public class NestedPatterns {
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}
record Circle(Point center, double radius) {}
record Triangle(Point a, Point b, Point c) {}
static sealed interface Shape permits Rectangle, Circle, Triangle {}
static sealed interface LocatedShape extends Shape {}
record NamedShape(String name, Shape shape) {}
static String describeLocation(Shape shape) {
return switch (shape) {
case Circle c -> "Круг в точке (" + c.center().x() + ", " + c.center().y() + ")";
case Rectangle r when r.topLeft().x() == 0 && r.topLeft().y() == 0 ->
"Прямоугольник в начале координат";
case Rectangle r ->
"Прямоугольник от (" + r.topLeft().x() + ", " + r.topLeft().y() +
") до (" + r.bottomRight().x() + ", " + r.bottomRight().y() + ")";
case Triangle t -> "Треугольник с вершинами в (" +
t.a().x() + "," + t.a().y() + "), (" +
t.b().x() + "," + t.b().y() + "), (" +
t.c().x() + "," + t.c().y() + ")";
};
}
static double distance(Point p1, Point p2) {
int dx = p1.x() - p2.x();
int dy = p1.y() - p2.y();
return Math.sqrt(dx * dx + dy * dy);
}
static String classifyByArea(Shape shape) {
return switch (shape) {
case Circle c -> c.radius() > 5 ? "Большой круг" : "Маленький круг";
case Rectangle r -> {
double w = r.topLeft().x() - r.bottomRight().x();
double h = r.topLeft().y() - r.bottomRight().y();
yield Math.abs(w * h) > 100 ? "Большой прямоугольник" : "Маленький прямоугольник";
}
case Triangle t -> {
double a = distance(t.a(), t.b());
double b = distance(t.b(), t.c());
double c = distance(t.c(), t.a());
double s = (a + b + c) / 2;
double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
yield area > 50 ? "Большой треугольник" : "Маленький треугольник";
}
};
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(new Point(5, 5), 3.0),
new Circle(new Point(0, 0), 15.0),
new Rectangle(new Point(0, 0), new Point(10, 10)),
new Rectangle(new Point(2, 2), new Point(20, 20)),
new Triangle(new Point(0, 0), new Point(10, 0), new Point(5, 8)),
new Triangle(new Point(0, 0), new Point(3, 0), new Point(1, 2))
};
for (Shape s : shapes) {
System.out.println(describeLocation(s) + " -> " + classifyByArea(s));
}
}
}
Вложенные паттерны работают особенно эффективно с record'ами, потому что у record'ов предсказуемая структура. Вы можете деструктурировать record вложенный на 3-4 уровня без потери читаемости. Однако не стоит злоупотреблять глубиной — 2-3 уровня вложенности обычно достаточно.
Boxed-паттерны
Boxed-паттерны позволяют работать с обёрнутыми примитивами (boxed primitives) в switch. Это полезно, когда у вас есть переменная типа Object, которая может содержать как boxed примитив (Integer, Double, Long), так и ссылочные типы. С boxed-паттернами вы можете проверить тип примитива и его значение одновременно.
public class BoxedPatterns {
public static String classify(Object obj) {
return switch (obj) {
case null -> "null";
case Integer n when n < 0 -> "Отрицательное целое: " + n;
case Integer n when n == 0 -> "Ноль";
case Integer n -> "Положительное целое: " + n;
case Double d when d < 0.0 -> "Отрицательное дробное: " + d;
case Double d -> "Положительное дробное: " + d;
case Long l -> "Длинное целое: " + l;
case Float f -> "Float: " + f;
case Boolean b -> "Булево: " + b;
case String s when s.isEmpty() -> "Пустая строка";
case String s -> "Строка длины " + s.length() + ": \"" + s + "\"";
case int[] arr -> "Массив int[" + arr.length + "]";
default -> "Неизвестный тип";
};
}
public static void main(String[] args) {
Object[] values = {-5, 0, 42, 3.14, -2.7, 100L, 1.5f, true, "", "Hello", new int[]{1,2,3}, 4.0};
for (Object v : values) {
System.out.println(classify(v));
}
}
}
Практика: парсер математических выражений
Давайте создадим полноценный парсер математических выражений, используя pattern matching, sealed classes и records. Это классическая задача, которая демонстрирует всю мощь паттернов.
Наш парсер будет:
- Представлять выражения как абстрактное синтаксическое дерево (AST)
- Вычислять значения выражений
- Форматировать выражения в читаемый вид
- Упрощать выражения (например, умножение на 1)
- Обрабатывать вложенные выражения любой глубины
public class MathExprParser {
sealed interface Expr permits Num, Var, Add, Sub, Mul, Div, Neg, Pow {}
record Num(double value) implements Expr {}
record Var(String name) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Sub(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record Div(Expr left, Expr right) implements Expr {}
record Neg(Expr expr) implements Expr {}
record Pow(Expr base, Expr exponent) implements Expr {}
static double evaluate(Expr expr, java.util.Map vars) {
return switch (expr) {
case Num n -> n.value();
case Var v -> vars.getOrDefault(v.name(), 0.0);
case Add a -> evaluate(a.left(), vars) + evaluate(a.right(), vars);
case Sub s -> evaluate(s.left(), vars) - evaluate(s.right(), vars);
case Mul m -> evaluate(m.left(), vars) * evaluate(m.right(), vars);
case Div d -> evaluate(d.left(), vars) / evaluate(d.right(), vars);
case Neg n -> -evaluate(n.expr(), vars);
case Pow p -> Math.pow(evaluate(p.base(), vars), evaluate(p.exponent(), vars));
};
}
static String format(Expr expr) {
return switch (expr) {
case Num n -> String.valueOf(n.value());
case Var v -> v.name();
case Add a -> "(" + format(a.left()) + " + " + format(a.right()) + ")";
case Sub s -> "(" + format(s.left()) + " - " + format(s.right()) + ")";
case Mul m -> "(" + format(m.left()) + " * " + format(m.right()) + ")";
case Div d -> "(" + format(d.left()) + " / " + format(d.right()) + ")";
case Neg n -> "(-" + format(n.expr()) + ")";
case Pow p -> "(" + format(p.base()) + " ^ " + format(p.exponent()) + ")";
};
}
static Expr simplify(Expr expr) {
return switch (expr) {
case Num n -> n;
case Var v -> v;
case Neg n -> {
Expr inner = simplify(n.expr());
if (inner instanceof Num num) {
yield new Num(-num.value());
}
yield new Neg(inner);
}
case Add a -> {
Expr l = simplify(a.left());
Expr r = simplify(a.right());
if (l instanceof Num n1 && n1.value() == 0) yield r;
if (r instanceof Num n2 && n2.value() == 0) yield l;
if (l instanceof Num n1 && r instanceof Num n2) yield new Num(n1.value() + n2.value());
yield new Add(l, r);
}
case Mul m -> {
Expr l = simplify(m.left());
Expr r = simplify(m.right());
if (l instanceof Num n1 && n1.value() == 0) yield new Num(0);
if (r instanceof Num n2 && n2.value() == 0) yield new Num(0);
if (l instanceof Num n1 && n1.value() == 1) yield r;
if (r instanceof Num n2 && n2.value() == 1) yield l;
if (l instanceof Num n1 && r instanceof Num n2) yield new Num(n1.value() * n2.value());
yield new Mul(l, r);
}
case Sub s -> {
Expr l = simplify(s.left());
Expr r = simplify(s.right());
if (r instanceof Num n2 && n2.value() == 0) yield l;
if (l instanceof Num n1 && r instanceof Num n2) yield new Num(n1.value() - n2.value());
yield new Sub(l, r);
}
case Div d -> {
Expr l = simplify(d.left());
Expr r = simplify(d.right());
if (l instanceof Num n1 && n1.value() == 0) yield new Num(0);
if (r instanceof Num n2 && n2.value() == 1) yield l;
if (l instanceof Num n1 && r instanceof Num n2) yield new Num(n1.value() / n2.value());
yield new Div(l, r);
}
case Pow p -> {
Expr b = simplify(p.base());
Expr e = simplify(p.exponent());
if (e instanceof Num n2 && n2.value() == 0) yield new Num(1);
if (e instanceof Num n2 && n2.value() == 1) yield b;
if (b instanceof Num n1 && e instanceof Num n2) yield new Num(Math.pow(n1.value(), n2.value()));
yield new Pow(b, e);
}
};
}
static Expr parse(String input) {
return new Add(
new Mul(new Var("x"), new Num(2)),
new Pow(new Var("y"), new Num(3))
);
}
public static void main(String[] args) {
Expr expr = new Add(
new Mul(new Num(0), new Var("x")),
new Add(new Num(5), new Num(0))
);
System.out.println("Исходное: " + format(expr));
Expr simplified = simplify(expr);
System.out.println("Упрощённое: " + format(simplified));
Expr complex = new Add(
new Mul(new Num(1), new Var("a")),
new Sub(new Num(10), new Num(0))
);
System.out.println("\nИсходное: " + format(complex));
System.out.println("Упрощённое: " + format(simplify(complex)));
Expr power = new Pow(new Num(2), new Num(10));
System.out.println("\nИсходное: " + format(power));
System.out.println("Упрощённое: " + format(simplify(power)));
System.out.println("Значение: " + evaluate(simplify(power), java.util.Map.of()));
}
}
Сравнение: старый vs новый подход
Давайте наглядно сравним, как выглядел бы один и тот же код до и после появления pattern matching. Разница поразительна — и по объёму кода, и по безопасности, и по читаемости.
До pattern matching (Java 8-15):
- Каждая проверка типа — минимум 2 строки
- Ручное приведение типа — риск ClassCastException
- Длинные цепочки if-else без проверки полноты
- Невозможно безопасно добавить guard-условие
- Код трудно рефакторить — добавление нового типа требует изменения всех мест
После pattern matching (Java 16-21):
- Проверка типа и приведение — одна строка
- Компилятор гарантирует безопасность приведения
- Компактный switch с проверкой полноты
- Guard-паттерны с when для дополнительных условий
- Добавление нового типа автоматически выявит неполные обработки
Pattern matching и ClassCastException
Одна из главных проблем классической Java — ClassCastException. Это运行时异常, которая возникает, когда вы пытаетесь привести объект к несовместимому типу. Pattern matching полностью решает эту проблему для проверок instanceof, потому что:
- Переменная паттерна создаётся только если instanceof вернул true
- Компилятор проверяет, что переменная используется только в безопасном контексте
- Не нужно писать ручной каст — компилятор делает это за вас
Это особенно важно вlarge codebase'ах, где классы могут эволюционировать, добавляя новые подтипы. Pattern matching + sealed classes гарантируют, что все новые типы будут обработаны.
Pattern matching не отменяет полностью ClassCastException — вы всё ещё можете получить его, если явно приведёте объект через каст. Но в коде, который использует pattern matching, необходимость в явных кастах исчезает. Это не просто удобство — это архитектурное преимущество: ваш код становится по своей природе более безопасным.
Продвинутые паттерны: unnamed variables
Начиная с Java 22, появилась возможность использовать unnamed variables (именованные переменные) с подчёркиванием _. Это полезно, когда вы проверяете тип, но не планируете использовать переменную. Подчёркивание показывает намерение: «я знаю, что этот объект есть, но мне он не нужен».
public class UnnamedVariableDemo {
static sealed interface Event permits Click, KeyPress, MouseMove {}
record Click(int x, int y) implements Event {}
record KeyPress(char key) implements Event {}
record MouseMove(int x, int y, int dx, int dy) implements Event {}
static String handleEvent(Event event) {
return switch (event) {
case Click _ -> "Клик";
case KeyPress kp -> "Нажата клавиша: " + kp.key();
case MouseMove _ -> "Движение мыши";
};
}
static String detailedHandle(Event event) {
return switch (event) {
case Click c -> "Клик в точке (" + c.x() + ", " + c.y() + ")";
case KeyPress kp when Character.isLetter(kp.key()) -> "Буква: " + kp.key();
case KeyPress kp when Character.isDigit(kp.key()) -> "Цифра: " + kp.key();
case KeyPress _ -> "Другая клавиша";
case MouseMove m when Math.abs(m.dx()) > Math.abs(m.dy()) -> "Горизонтальное движение";
case MouseMove _ -> "Вертикальное движение";
};
}
public static void main(String[] args) {
Event[] events = {
new Click(100, 200),
new KeyPress('A'),
new KeyPress('7'),
new KeyPress('#'),
new MouseMove(50, 50, 10, 2),
new MouseMove(50, 50, 1, 8)
};
for (Event e : events) {
System.out.println(detailedHandle(e));
}
}
}
List patterns (предварительный просмотр)
List patterns — это экспериментальная возможность (preview в Java 21, стабильная в Java 23+), которая позволяет деструктурировать списки и массивы прямо в switch. Вы можете проверить размер списка и его содержимое одним паттерном.
Синтаксис: case List[a, b, c] — проверяет, что список содержит ровно три элемента, и присваивает их переменным a, b, c. Вы также можете использовать case List[first, second, ...rest] для захвата первого и второго элемента и остального списка.
import java.util.List;
public class ListPatternDemo {
static String analyze(List<Object> list) {
return switch (list) {
case null -> "Список null";
case List[] -> "Пустой список";
case List[Integer n] -> "Одно число: " + n;
case List[String s] -> "Одна строка: " + s;
case List[Integer a, Integer b] -> "Два числа: " + a + " и " + b;
case List[String s, _] -> "Строка и что-то: " + s;
case List[a, b, c] -> "Три элемента: " + a + ", " + b + ", " + c;
case List[first, _, _, last] -> "Первый: " + first + ", последний: " + last;
default -> "Список из " + list.size() + " элементов";
};
}
static String classifyCommand(List<String> parts) {
return switch (parts) {
case List["quit"] -> "Выход";
case List["add", item] -> "Добавить: " + item;
case List["remove", item] -> "Удалить: " + item;
case List["move", dir, steps] -> "Движение " + dir + " на " + steps + " шагов";
case List["set", key, value] -> "Установить " + key + " = " + value;
case List[cmd, _] -> "Команда: " + cmd;
default -> "Неизвестная команда";
};
}
public static void main(String[] args) {
List<Object>[] lists = {
List.of(),
List.of(42),
List.of("hello"),
List.of(1, 2),
List.of("a", 3.14),
List.of(1, "two", 3.0),
List.of("x", 1, 2, 3),
List.of(1, 2, 3, 4, 5)
};
for (List<Object> list : lists) {
System.out.println(analyze(list));
}
System.out.println();
List<String>[] commands = {
List.of("quit"),
List.of("add", "apple"),
List.of("remove", "banana"),
List.of("move", "north", "5"),
List.of("set", "color", "red"),
List.of("unknown", "param")
};
for (List<String> cmd : commands) {
System.out.println(classifyCommand(cmd));
}
}
}
List patterns в сочетании с pattern matching для switch создают невероятно мощный инструмент для обработки структурированных данных. Это особенно полезно при парсинге команд, обработке JSON и работе с API, которые возвращают списки фиксированной структуры.
Итоги урока
Pattern matching — это не просто синтаксический сахар. Это фундаментальное изменение парадигмы работы с типами в Java. Вот ключевые моменты, которые стоит запомнить:
- instanceof pattern matching объединяет проверку типа и приведение в одну операцию, устраняя ClassCastException
- Switch с pattern matching позволяет компактно обрабатывать объекты по типу с проверкой полноты
- Guard-паттерны с when добавляют гибкость — можно различать объекты одного типа по значениям
- Sealed classes + switch гарантируют, что все подтипы обработаны — компилятор проверяет это
- Вложенные паттерны позволяют деструктурировать сложные структуры данных
- Null-safety встроена в switch — отдельный case для null
- Unnamed variables (_) показывают намерение не использовать переменную
- List patterns расширяют паттерны на коллекции
Pattern matching делает код безопаснее, короче и выразительнее. Он позволяет компилятору проводить глубокий анализ вашего кода и находить ошибки на этапе компиляции, а не во время выполнения. Это важнейший инструмент для создания надёжного и поддерживаемого кода на Java.