Сопоставление шаблонов в Java: изменение правил игры для вашего кода
В этой статье мы рассмотрим основы сопоставления с образцом в Java (до Java 19) и узнаем, как использовать его в наших проектах.
О чем все это?
Не путать с сопоставлением регулярных выражений. Сопоставление с образцом — это метод, который позволяет нам сопоставлять определенные шаблоны или формы данных. Слово «шаблон» относится к форме или структуре данных, а «сопоставление» относится к процессу идентификации и реагирования на эту конкретную форму. Другими словами, мы сопоставляем форму данных с типом или значением.
В Java сопоставление с образцом является частью проекта Amber OpenJDK, того же проекта, который принес нам текстовые блоки, записи и выражения переключения. На сайте проекта вы можете отслеживать статус JEP, связанных с сопоставлением с образцом и другими.
Более конкретно, есть несколько шаблонов для сопоставления:
- Типы: сопоставление ссылки с типом.
- Записи: сопоставление со структурой записи.
- Произвольные классы: сопоставление со структурой любого класса.
- Массивы: сопоставление со структурой/содержимым массива.
Мы увидим примеры первых двух, поскольку мы можем протестировать их в режиме предварительного просмотра Java (19 на момент написания).
Зачем мне это нужно?
В основном эта функция позволяет создавать более эффективный и выразительный код, устраняя необходимость в подробных операторах if-else и приведении типов, как мы увидим в примерах позже. Это также обеспечивает дополнительную проверку компилятора, что означает меньшее количество потенциальных ошибок во время выполнения.
Согласно José Paumard, сопоставление с образцом станет третьей революцией в языке java после введения дженериков в java 5 и типов функций в java 8.
Где я могу его использовать?
случай
В JEP 394, выпущенном в Java 16, доступно сопоставление шаблонов instanceof.
Во-первых, давайте рассмотрим пример сопоставления шаблонов типов в экземпляре. В следующем примере мы можем увидеть синтаксис, технически говоря, мы говорим, что мы сопоставляем ссылку типа Object с шаблонами «Integer i» и «Double d». соответственно.
static void handleNumber(Object o) { if (o == null) { System.out.println("got a null"); } if (o instanceof Integer i) { handleInteger(i); } else if (o instanceof Double d) { handleDouble(d); } else { System.out.println("unknown value"); } }
Область действия i и d находится в пределах области действия if, если только она не инвертирована следующим образом:
void scopeExample(Object o) { if (!(o instanceof Number n)) { return; } // n available here System.out.println(n); }
Далее рассмотрим сопоставление шаблона записи всегда в контексте экземпляра. Это все еще находится в предварительной версии (JEP 405 и 432).
record Pair(String k, String v) { } private static void handleObject(Object o) { // we can omit p if we're not going to use it if (o instanceof Pair(String k, String v) p) { System.out.printf("got a Pair with k %s and v : %s%n", k, v); System.out.printf("pair : %s%n", p); } }
Как мы видим, мы сопоставили o с шаблоном записи «Pair(String k, String v) p», это похоже на деструктурирование объектов в JavaScript. kи vбудут ссылаться на поля записи, а pбудет ссылаться на соответствующий объект как Pairrecord. Этот синтаксис также поддерживает вложенность, поэтому, если у нас есть запись, которая имеет другую запись в качестве атрибута, она также может быть сопоставлена. Тем не менее, слишком много вложенности в этом случае не является хорошей идеей.
Случай переключения
Это все еще функция предварительного просмотра, начиная с java 17 (JEP 406), и 4-й предварительный просмотр появится в JEP 433, запланированном с java 20. Если мы перепишем первый пример с переключателем, мы получим следующее:
static void handleNumber(Object o) { switch (o) { case null -> System.out.println("got a null"); case Integer i -> handleInteger(i); case Double d -> handleDouble(d); default -> System.out.println("unknown value"); } }
Обратите внимание, что это расширенная форма переключателя или выражение переключателя, но мы можем без проблем использовать сопоставление с образцом со старым переключателем. Когда указанный объект соответствует типу дела, мы получаем новую ссылку этого типа для использования.
Несколько замечаний о сопоставлении с образцом в случаях переключения:
- Мы можем легко обработать случай null (первый случай в предыдущем коде).
- Компилятор может проверить полноту случаев. Например, предположим, что мы переключаемся на запечатанный тип (т. е. подтипы известны во время компиляции). Если случаи исчерпывающие, нет необходимости в ветке по умолчанию, иначе компилятор предупредит нас о пропущенных случаях.
- Когда один случай охватывает следующие случаи, это означает, что он является родительским классом для следующих случаев, как в приведенном ниже примере:
static void handleNumberWithParentClass(Object o) { switch (o) { case null -> System.out.println("got a null"); case Number n -> System.out.println(""); case Integer i -> handleInteger(i); case Double d -> handleDouble(d); default -> System.out.println("unknown value"); } }
компилятор будет жаловаться, что «В метке доминирует предшествующая метка регистра «Число n»».
4. Уточнение случая, как описано в JEP, позволяет нам добавить условие к случаю, как показано ниже.
static void handleNumberWithCaseRefinement(Object o) { switch (o) { case null -> System.out.println("got a null"); case Integer i when i > 10 -> handleInteger(i); case Double d -> handleDouble(d); default -> System.out.println("unknown value"); } }
Код этих примеров можно найти по адресу https://github.com/maximz101/pattern-matching-java. Попробуйте сами.
Рекомендации
https://openjdk.org/projects/amber/
https://youtu.be/aKaw9W789wU
https://youtu.be/lBOwPYvdvLQ
https ://youtu.be/gFqzFlhEJaY