openjdk.ruOpenJDK на русском

JEP 456: Unnamed Variables & Patterns

Безымянные переменные и шаблоны

ОтветственныйAngelos Bimpoudis
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск22
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 443: Unnamed Patterns and Variables (Preview)
РецензентыBrian Goetz
ОдобренBrian Goetz
Создан2023/07/10 16:17
Обновлён2026/07/08 17:28
Задача8311828

Аннотация

Расширить язык программирования Java безымянными переменными и безымянными шаблонами. Их можно использовать там, где объявления переменных или вложенные шаблоны обязательны, но никогда не используются. И те, и другие обозначаются символом подчёркивания _.

История

Безымянные переменные и безымянные шаблоны впервые появились в статусе Preview (предварительная версия) в JDK 21 в JEP 443, который назывался Unnamed Patterns and Variables. Здесь мы предлагаем сделать эту возможность окончательной без изменений.

Цели

  • Фиксировать намерение разработчика оставить данную привязку или параметр лямбда-выражения неиспользуемыми и обеспечивать соблюдение этого свойства, чтобы сделать программы понятнее и уменьшить возможности для ошибок.

  • Улучшить сопровождаемость любого кода, явно выделяя переменные, которые необходимо объявить (например, в конструкциях catch), но которые не используются.

  • Сократить церемонии, когда у разных ветвей switch одно и то же действие, позволив указывать в одной ветви несколько шаблонов в качестве альтернатив.

  • Улучшить читаемость Record Patterns (шаблоны записей), опуская ненужные вложенные шаблоны типов.

Что не является целью

Мотивация

Иногда разработчики объявляют переменные, которые не собираются использовать: то ли из соображений стиля кода, то ли потому, что язык требует объявления переменных в определённых контекстах. Намерение не использовать переменную известно в момент написания кода, но если оно не зафиксировано явно, то те, кто позже будет сопровождать код, могут случайно использовать переменную и тем самым нарушить это намерение. Если бы мы могли сделать случайное использование таких переменных невозможным, код стал бы информативнее, читаемее и менее подвержен ошибкам.

Неиспользуемые переменные

Необходимость объявлять переменную, которая никогда не используется, особенно часто возникает в коде, побочный эффект которого важнее его результата. Например, этот код вычисляет total как побочный эффект цикла, не используя переменную цикла order:

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order order : orders)    // order is unused
        total++;
    return total;
}

Заметность объявления order неудачна, учитывая, что order не используется. Объявление можно сократить до var order, но избежать присвоения этой переменной имени нельзя. Само имя можно сократить, например, до o, но этот синтаксический приём не выражает намерения никогда не использовать переменную. Кроме того, инструменты статического анализа обычно выдают предупреждения о неиспользуемых переменных, даже когда разработчик намеренно их не использует и может не иметь способа подавить эти предупреждения.

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

Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2 ..
while (q.size() >= 3) {
   int x = q.remove();
   int y = q.remove();
   int z = q.remove();            // z is unused
    ... new Point(x, y) ...
}

Третий вызов remove() даёт нужный побочный эффект — извлечение элемента из очереди — независимо от того, присваивается ли его результат переменной, поэтому объявление z можно было бы опустить. Однако ради сопровождаемости автор этого кода может захотеть единообразно обозначать результат remove(), объявляя переменную. Сейчас у него два варианта, и оба неприятны:

  • Не объявлять переменную z, что приводит к асимметрии и, возможно, к предупреждению статического анализа об игнорировании возвращаемого значения, либо

  • Объявить переменную, которая не используется, и, возможно, получить предупреждение статического анализа о неиспользуемой переменной.

Неиспользуемые переменные часто встречаются ещё в двух операторах, ориентированных на побочные эффекты:

  • Оператор try-with-resources всегда используется ради его побочного эффекта, а именно автоматического закрытия ресурсов. В некоторых случаях ресурс представляет контекст, в котором выполняется код блока try; код не использует контекст напрямую, поэтому имя переменной ресурса не имеет значения. Например, если ресурс ScopedContext является AutoCloseable, следующий код захватывает и автоматически освобождает контекст:

    try (var acquiredContext = ScopedContext.acquire()) {
        ... acquiredContext not used ...
    }

    Имя acquiredContext — просто лишний шум, так что было бы хорошо его опустить.

  • Исключения — это предельный случай побочного эффекта, и их обработка часто порождает неиспользуемую переменную. Например, большинство разработчиков писали блоки catch такого вида, где параметр исключения ex не используется:

    String s = ...;
    try {
        int i = Integer.parseInt(s);
        ... i ...
    } catch (NumberFormatException ex) {
        System.out.println("Bad number: " + s);
    }

Даже коду без побочных эффектов иногда приходится объявлять неиспользуемые переменные. Например:

...stream.collect(Collectors.toMap(String::toUpperCase,
                                   v -> "NODATA"));

Этот код создаёт отображение, которое сопоставляет каждому ключу одно и то же значение-заглушку. Поскольку параметр лямбда-выражения v не используется, его имя не имеет значения.

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

Без имени разумно объявлять те виды переменных, которые не видны за пределами метода: локальные переменные, параметры исключений и параметры лямбда-выражений, как показано выше. Такие переменные можно переименовать или сделать безымянными без внешних последствий. Поля же — даже если они private — передают состояние объекта между методами, а безымянное состояние не полезно и не поддаётся сопровождению.

Неиспользуемые переменные шаблона

Локальные переменные могут объявляться и шаблонами типов — такие локальные переменные называются переменными шаблона, — поэтому шаблоны типов тоже могут объявлять неиспользуемые переменные. Рассмотрим следующий код, в котором шаблоны типов используются в метках case оператора switch, выполняющего выбор по экземпляру класса Ball с модификатором sealed:

sealed abstract class Ball permits RedBall, BlueBall, GreenBall { }
final  class RedBall   extends Ball { }
final  class BlueBall  extends Ball { }
final  class GreenBall extends Ball { }

Ball ball = ...
switch (ball) {
    case RedBall   red   -> process(ball);
    case BlueBall  blue  -> process(ball);
    case GreenBall green -> stopProcessing();
}

Ветви switch проверяют тип Ball с помощью шаблонов типов, но переменные шаблона red, blue и green не используются в правых частях конструкций case. Этот код был бы понятнее, если бы мы могли опустить имена этих переменных.

Теперь предположим, что мы определяем record-класс Box, который может содержать любой тип Ball, но может содержать и значение null:

record Box<T extends Ball>(T content) { }

Box<? extends Ball> box = ...
switch (box) {
    case Box(RedBall   red)     -> processBox(box);
    case Box(BlueBall  blue)    -> processBox(box);
    case Box(GreenBall green)   -> stopProcessing();
    case Box(var       itsNull) -> pickAnotherBox();
}

Вложенные шаблоны типов по-прежнему объявляют переменные шаблона, которые не используются. Поскольку этот switch сложнее предыдущего, отказ от имён неиспользуемых переменных во вложенных шаблонах типов ещё больше улучшил бы читаемость.

Неиспользуемые вложенные шаблоны

Одни Records (записи) можно вкладывать в другие, и тогда форма структуры данных оказывается так же важна, как и содержащиеся в ней элементы данных. Например:

record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }

... new ColoredPoint(new Point(3,4), Color.GREEN) ...

if (r instanceof ColoredPoint(Point p, Color c)) {
    ... p.x() ... p.y() ...
}

В этом коде одна часть программы создаёт экземпляр ColoredPoint, а другая использует шаблон instanceof, чтобы проверить, является ли переменная ColoredPoint, и если да, извлечь два значения её компонентов.

Record Patterns, такие как ColoredPoint(Point p, Color c), приятно наглядны, но программы часто используют для дальнейшей обработки лишь некоторые значения компонентов. Например, приведённый выше код использует в блоке if только p, а не c. Утомительно выписывать шаблоны типов для всех компонентов record-класса каждый раз, когда мы выполняем такой Pattern Matching (сопоставление с образцом). Кроме того, визуально не ясно, что весь компонент Color не имеет значения; из-за этого условие в операторе if тоже труднее читать. Особенно это заметно, когда Record Patterns вложены друг в друга для извлечения данных из компонентов, как здесь:

if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
    ... x ... y ...
}

Можно визуально уменьшить компонент Color, заменив Color на var, то есть ColoredPoint(Point(int x, int y), var c), но лучше было бы вовсе опустить ненужные компоненты. Это и упростило бы написание Record Patterns, и улучшило бы читаемость, убрав из кода лишний шум.

Описание

Безымянная переменная объявляется с помощью символа подчёркивания _ (U+005F), который ставится вместо имени локальной переменной в операторе объявления локальной переменной, или параметра исключения в конструкции catch, или параметра лямбда-выражения в лямбда-выражении.

Безымянная переменная шаблона объявляется с помощью символа подчёркивания, который ставится вместо переменной шаблона в шаблоне типа.

Безымянный шаблон целиком обозначается символом подчёркивания. Он позволяет при Pattern Matching опустить и тип, и имя компонента записи.

Одиночный символ подчёркивания — самый лёгкий разумный синтаксис для обозначения отсутствия имени. Для этой цели он широко используется в других языках, например в Scala и Python. Изначально одиночное подчёркивание было допустимым идентификатором в Java 1.0, но позже мы забрали его для безымянных переменных и шаблонов: в Java 8 (2014) мы начали выдавать предупреждения при компиляции, когда подчёркивание использовалось как идентификатор, а в Java 9 (2017, JEP 213) удалили такие идентификаторы из спецификации языка, превратив эти предупреждения в ошибки.

Возможность использовать подчёркивание в идентификаторах длиной два символа и более не меняется, поскольку подчёркивание по-прежнему является «буквой Java» и «буквой или цифрой Java». Например, такие идентификаторы, как _age, MAX_AGE и __ (два подчёркивания), остаются допустимыми.

Возможность использовать подчёркивание как разделитель разрядов тоже не меняется. Например, числовые литералы, такие как 123_456_789 и 0b1010_0101, остаются допустимыми.

Безымянные переменные

Следующие конструкции могут объявлять безымянную переменную, используя подчёркивание (_) вместо идентификатора:

  • оператор объявления локальной переменной в блоке (JLS §14.4.2),
  • спецификация ресурса оператора try-with-resources (JLS §14.20.3),
  • заголовок обычного цикла for (JLS §14.14.1),
  • заголовок расширенного цикла for (JLS §14.14.2),
  • параметр исключения блока catch (JLS §14.20) и
  • формальный параметр лямбда-выражения (JLS §15.27.1).

Объявление безымянной переменной не вводит имя в область видимости, поэтому после инициализации в переменную нельзя записать значение и её нельзя прочитать. Для безымянной переменной, объявленной в операторе объявления локальной переменной или в спецификации ресурса оператора try-with-resources, необходимо указать инициализатор.

Безымянная переменная никогда не затеняет другие переменные, поскольку у неё нет имени, поэтому в одном блоке можно объявить несколько безымянных переменных.

Ниже приведены примеры из предыдущего раздела, переписанные с использованием безымянных переменных.

  • Расширенный цикл for с побочными эффектами:

    static int count(Iterable<Order> orders) {
        int total = 0;
        for (Order _ : orders)    // Unnamed variable
            total++;
        return total;
    }

    В инициализации простого цикла for тоже можно объявлять безымянные локальные переменные:

    for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }
  • Оператор присваивания, в котором результат выражения в правой части не нужен:

    Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
    while (q.size() >= 3) {
       var x = q.remove();
       var y = q.remove();
       var _ = q.remove();        // Unnamed variable
       ... new Point(x, y) ...
    }

    Если программе нужно обрабатывать только координаты x1, x2 и т. д., безымянные переменные можно использовать в нескольких операторах присваивания:

    while (q.size() >= 3) {
        var x = q.remove();
        var _ = q.remove();       // Unnamed variable
        var _ = q.remove();       // Unnamed variable
        ... new Point(x, 0) ...
    }
  • Блок catch:

    String s = ...
    try {
        int i = Integer.parseInt(s);
        ... i ...
    } catch (NumberFormatException _) {        // Unnamed variable
        System.out.println("Bad number: " + s);
    }

    Безымянные переменные можно использовать в нескольких блоках catch:

    try { ... }
    catch (Exception _) { ... }                // Unnamed variable
    catch (Throwable _) { ... }                // Unnamed variable
  • В try-with-resources:

    try (var _ = ScopedContext.acquire()) {    // Unnamed variable
        ... no use of acquired resource ...
    }
  • Лямбда-выражение, параметр которого не имеет значения:

    ...stream.collect(Collectors.toMap(String::toUpperCase,
                                       _ -> "NODATA"))    // Unnamed variable

Безымянные переменные шаблона

Шаблон типа объявляет переменную шаблона, которая является безымянной, если обозначена подчёркиванием (_). Безымянная переменная шаблона может находиться в шаблоне типа верхнего уровня или быть вложенной в record-шаблон. Например, пример с Ball теперь можно записать так:

switch (ball) {
    case RedBall _   -> process(ball);          // Unnamed pattern variable
    case BlueBall _  -> process(ball);          // Unnamed pattern variable
    case GreenBall _ -> stopProcessing();       // Unnamed pattern variable
}

и пример с Box и Ball:

switch (box) {
    case Box(RedBall _)   -> processBox(box);   // Unnamed pattern variable
    case Box(BlueBall _)  -> processBox(box);   // Unnamed pattern variable
    case Box(GreenBall _) -> stopProcessing();  // Unnamed pattern variable
    case Box(var _)       -> pickAnotherBox();  // Unnamed pattern variable
}

Шаблон типа с безымянной переменной шаблона называется безымянным шаблоном типа. Позволяя опускать имена, безымянные шаблоны типов делают исследование данных во время выполнения на основе шаблонов типов визуально понятнее — как в блоках switch, так и с оператором instanceof.

Несколько шаблонов в метках case

Сейчас метки case могут содержать не более одного шаблона. С появлением безымянных переменных шаблонов и безымянных шаблонов повышается вероятность того, что в одном блоке switch окажется несколько ветвей case с разными шаблонами, но одинаковой правой частью. Например, в примере с Box и Ball первые две ветви имеют одинаковую правую часть, но разные шаблоны:

switch (box) {
    case Box(RedBall _)   -> processBox(box);
    case Box(BlueBall _)  -> processBox(box);
    case Box(GreenBall _) -> stopProcessing();
    case Box(var _)       -> pickAnotherBox();
}

Можно было бы упростить код, если разрешить первым двум шаблонам находиться в одной метке case:

switch (box) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(box);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(var _)                      -> pickAnotherBox();
}

Поэтому мы меняем грамматику меток switch (JLS §14.11.1) на

SwitchLabel:
    case CaseConstant {, CaseConstant}
    case null [, default]
    case CasePattern {, CasePattern } [Guard]
    default

и определяем семантику метки case с несколькими шаблонами так: она сопоставляется со значением, если значение соответствует любому из шаблонов.

Если метка case содержит несколько шаблонов, то объявление переменных шаблона в любом из этих шаблонов является ошибкой компиляции.

Метка case с несколькими шаблонами case может иметь охранное условие. Охранное условие относится к case целиком, а не к отдельным шаблонам. Например, если есть переменная x типа int, первую ветвь case из предыдущего примера можно дополнительно ограничить:

case Box(RedBall _), Box(BlueBall _) when x == 42 -> processBox(b);

Охранные условия — это свойства меток case, а не отдельных шаблонов внутри метки case, поэтому указывать больше одного охранного условия запрещено:

case Box(RedBall _) when x == 0, Box(BlueBall _) when x == 42 -> processBox(b);
    // compile-time error

Безымянный шаблон

Безымянный шаблон — это безусловный шаблон, который соответствует чему угодно, но ничего не объявляет и не инициализирует. Он эквивалентен безымянному шаблону типа var _. Безымянный шаблон может быть вложен в шаблон вида Record Patterns, но не может использоваться как шаблон верхнего уровня, например, в выражении instanceof или в метке case.

Поэтому в приведённом ранее примере можно полностью опустить шаблон типа для компонента Color:

if (r instanceof ColoredPoint(Point(int x, int y), _)) { ... x ... y ... }

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

if (r instanceof ColoredPoint(_, Color c)) { ... c ... }

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

if (r instanceof ColoredPoint(Point(int x, _), _)) { ... x ... }

Этот код извлекает координату x вложенного Point и при этом ясно показывает, что значения компонентов y и Color не извлекаются.

Вернёмся к примеру с Box и Ball: его последнюю метку case можно ещё упростить, если использовать безымянный шаблон вместо var _:

switch (box) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(box);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

Риски и допущения

  • Мы предполагаем, что в активно сопровождаемом коде подчёркивание в качестве имени переменной используется редко или не используется вовсе. Разработчики, которые переходят с Java 7 на Java 22 и не видели предупреждений, выдававшихся в Java 8, или ошибок, выдающихся начиная с Java 9, могут быть удивлены. Они рискуют столкнуться с ошибками компиляции при чтении или записи переменных с именем _ и при объявлении любого другого элемента (класса, поля и т. д.) с именем _.

  • Мы ожидаем, что разработчики инструментов статического анализа учтут новую роль подчёркивания для безымянных переменных и не будут помечать неиспользование таких переменных в современном коде.

Альтернативы

  • Можно определить аналогичное понятие безымянных параметров методов. Однако у него есть неочевидные взаимодействия со спецификацией (например, что значит переопределить метод с безымянными параметрами?) и с инструментами (например, как писать JavaDoc для безымянных параметров?). Это может стать темой будущего JEP.

  • JEP 302 (Lambda Leftovers) рассматривал проблему неиспользуемых параметров лямбда-выражений и предложил обозначать их подчёркиванием, но также охватывал многие другие вопросы, которые лучше решаются иными способами. Этот JEP касается использования неиспользуемых параметров лямбда-выражений, рассмотренных в JEP 302, но не затрагивает другие вопросы, изученные там.