Сопоставление с образцом стало намного эффективнее

Сопоставление с образцом — это декларативный и компонуемый подход, который создает более мощный и выразительный код для навигации и обработки структур данных. В Java 16 добавлено сопоставление шаблонов для оператора instanceof (JEP 394), и ранее в этой серии мы рассматривали сопоставление шаблонов для switch (Сопоставление шаблонов переключения).

Сегодня пришло время бегло взглянуть на другой вид сопоставления с образцом: Запись шаблонов (JEP 440).

Больше, чем просто сопоставление типов

До сих пор сопоставление с образцом в Java в основном ограничивалось сопоставлением типов:

// BEFORE JAVA 16
if (obj instanceof String) {
  String str = (String) obj;
  System.out.println(str);
}

// JAVA 16+
if (obj instanceof String str) {
  System.out.println(str);
}

Java 21 расширила эту концепцию, чтобы ее можно было использовать в switch операторах и выражениях:

// BEFORE JAVA 21
static String asStringValue(Object anyValue) {
  String result = null;

  if (anyValue instanceof String str) {
    result = str;
  } else if (anyValue instanceof BigDecimal bd) {
    result = bd.toEngineeringString();
  } else if (anyValue instance Integer i) {
    result = Integer.toString(i);
  } else {
    result = "n/a";
  }

  return result;
}

// JAVA 21+
static String asStringValue(Object anyValue) {
  return switch (anyValue) {
    case String str    -> str;
    case BigDecimal bd -> bd.toEngineeringString();
    case Integer i     -> Integer.toString(i);
    default            -> "n/a";
  };
}

Как видите, полученный код выглядит лучше и более упорядоченным, чем раньше. Но сопоставление типов — это только один из возможных вариантов использования сопоставления с образцом.

Деконструкция записей

Записи — это класс специального назначения, позволяющий легко агрегировать данные в практически неизменяемой форме. Они структурированы на основе компонентов, подобно полям в POJO или JavaBean. Их методы доступа, конструктор «все компоненты» (канонический) и вспомогательный метод, связанный с Object (toString, equals, hashCode), доступны в разумных реализациях без необходимости какого-либо дополнительного кода.

Если вы хотите узнать больше о Records, вы можете прочитать мою книгу Функциональный подход к Java, в которой эта тема обсуждается на более чем 30 страницах.

Сопоставление шаблонов записей — это способ сопоставления типа записи и доступа к ее компонентам за один шаг. Изображение простой записи, представляющей двухмерную точку:

record Point(int x, int y) {
  // no body
}

Сопоставление его типа и доступ к одному из его компонентов выглядит следующим образом:

Object maybePoint = ...;

if (maybePoint instanceof Point p) {
  System.out.println("Point => " + p.x() + "/" + p.y());
}

Чтобы соответствовать не Точке, а ее компонентам, они должны быть явно указаны в шаблоне:

Object maybePoint = ...;

if (maybePoint instanceof Point(int x, int y)) {
  System.out.println("Point => " + x + "/" + y);
}

Если вы похожи на меня, когда я впервые взглянул на эту функцию, вы можете подумать: «Хорошо, но как это должно быть лучше/проще, чем раньше?»

Что ж, в определенном смысле такое мышление правильное. Повторение определения записи для доступа к ее компонентам кажется утомительным. Но если мы посмотрим дальше, чем такой простой пример, то потенциал того, что сопоставление шаблонов записей может сделать для нас, откроется!

Вложенные записи

На мой взгляд, деконструкция простой записи не имеет большого преимущества, по крайней мере, без функции, которую я собираюсь вскоре обсудить. Реальная сила деконструкции Записей проявляется, если Одна Запись содержит другую Запись.

Давайте создадим запись, представляющую рамку окна, включая ее начало и размер на экране:

record Size(int width, int height) { }
record Point(int x, int y) { }
record WindowFrame(Point origin, Size size) { }

Чтобы получить доступ к компоненту height компонента WindowFrame во вложенном компоненте Size, нам потребуется несколько совпадений:

if (obj instanceof WindowFrame wf) {
  if (wf.size() != null) {
    System.out.println("Height: " + wf.size().height());
  }
}

С деконструкцией дела обстоят не намного лучше:

if (obj instanceof WindowFrame(Point origin, Size size)) {
  if (size != null) {
    System.out.println("Height: " + size.height());
  }
}

Однако деконструкция записей может быть вложенной, что устраняет необходимость проверки null и делает код более разумным в этом процессе:

if (obj instanceof WindowFrame(Point origin, Size(int width, int height))) {
    System.out.println("Height: " + height);
}

Разница здесь в том, что простое WindowFrame(Point origin, Size size) соответствует, даже если Size size равно null. Однако когда вы деконструируете Size, оно совпадает только в том случае, если size не является null.

По сути, либо шаблон полный соответствует, либо ни один.

Более простые шаблоны с выводом типа

Требование полной декларации Records для деструктуризации кажется рутинной работой. Компоненты должны совпадать, иначе компилятор будет недоволен:

record Point(int x, int y) { }

if (obj instanceof Point(long x, int y)) {
  // ...
}

// Error:
// incompatible types: pattern of type long is not applicable at int
// if (obj instanceof Point(long x, int y)) {
//                          ^----^

Поскольку необходимые компоненты фиксируются типом записи, мы можем использовать выведение типа локальной переменной, заменив фактические типы компонентов ключевым словом var:

if (obj instanceof Point(var x, var y)) {
  // ...
}

Это также не нарушит код, если изменится тип компонента записи. С другой стороны, явное объявление типа может быть более выразительным, и я уверен, что IDE помогут дополнить компоненты в будущем выпуске.

Еще более простые узоры с JEP 443

Еще одна будущая функция, доступная только в предварительной версии в Java 21, — это JEP 443: Безымянные шаблоны и переменные.

Эта функция улучшает читаемость нашего кода, позволяя нам заменять неиспользуемую переменную на _ (подчеркивание). Не нужно больше @SuppressWarnings("unused"), чтобы заткнуть все эти надоедливые предупреждения!

Безымянные переменные весьма полезны во многих сценариях, например, переменная Exception в блоке catch или конструкциях, имеющих только побочный эффект:

try {
  //...
} catch (Exception _) {
  // we don't need the actual exception
}

int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) { 
        // the actual order is not used
    }
}

Что касается сопоставления шаблонов записей, этот JEP максимально упрощает (вложенные) вызовы:

if (obj instanceof WindowFrame(_, Size(_, int height))) {
    System.out.println("Height: " + height);
}

Теперь шаблон уменьшен до необходимого соответствия, с меньшим количеством окружающего шума.

Заключение

Сопоставление с образцом — это функция, которая долгое время отсутствовала в Java или была доступна лишь в незначительной форме по сравнению с другими языками. Добавление деструктуризации записей — еще одно замечательное дополнение, позволяющее сократить разрыв в функциях и еще больше улучшить основу Java.

Деконструкция структур данных на основе записей с использованием более простого подхода к навигации по ним приводит к более разумному и чистому коду, особенно с вложенными записями. Однако, если честно, поначалу я не увидел особого преимущества сопоставления с образцом записи, особенно по сравнению с влиянием других функций Java 21.

Когда я попытался лучше понять преимущества (написав об этом статью), мне это не очень понравилось, особенно повторяющийся синтаксис. Но чем больше я с этим экспериментировал, тем яснее становилось, что это не будет «повседневной» функцией, как сами Records, по крайней мере для меня.

Тем не менее, я считаю, что это интересное и стоящее дополнение к языку, так что попробуйте (и немного времени), возможно, оно вам понравится! А с появлением новых функций, таких как безымянные шаблоны (JEP 443), сопоставление шаблонов записи станет еще лучше!

Заинтересованы в использовании функциональных концепций и методов в своем Java-коде? Прочтите мою книгу Функциональный подход к Java!

Ресурсы