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

JEP 443: Unnamed Patterns and Variables (Preview)

Безымянные шаблоны и переменные в статусе Preview (предварительная версия)

ОтветственныйAngelos Bimpoudis
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск21
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 456: Unnamed Variables & Patterns
РецензентыAlex Buckley
ОдобренBrian Goetz
Создан2022/09/26 08:00
Обновлён2023/12/12 19:23
Задача8294349

Аннотация

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

Цели

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

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

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

Мотивация

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

Records (записи), описанные в JEP 395, и Record Patterns из JEP 440 вместе упрощают обработку данных. Record-класс собирает компоненты элемента данных в экземпляр, а код, получающий экземпляр record-класса, с помощью Pattern Matching (сопоставление с образцом) по Record Patterns разбирает экземпляр на компоненты. Например:

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, а другая с помощью Pattern Matching по instanceof проверяет, является ли переменная ColoredPoint, и если да, извлекает два её компонента.

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

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

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

По мере того как разработчики осваивают ориентированный на данные подход record-классов и сопутствующего им механизма Sealed Classes (запечатанные классы) из JEP 409, мы ожидаем, что Pattern Matching по сложным структурам данных станет обычным делом. Часто форма структуры будет не менее важна, чем отдельные элементы данных в ней. В качестве сильно упрощённого примера рассмотрим следующие классы Ball и Box, а также switch, который исследует содержимое Box:

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

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

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

Каждый case обрабатывает Box в зависимости от его содержимого, но переменные red, blue и green не используются. Раз переменные не используются, этот код читался бы лучше, если бы их имена можно было опустить.

Более того, если бы switch переработали так, чтобы объединить первые два шаблона в одной метке case:

case Box(RedBall red), Box(BlueBall blue) -> processBox(b);

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

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

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

int total = 0;
for (Order order : orders) {
    if (total < LIMIT) { 
        ... 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 — просто лишний шум, так что было бы хорошо его опустить.

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

    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 — передают состояние объекта между методами, а безымянное состояние не приносит пользы и затрудняет сопровождение.

Описание

Безымянный шаблон обозначается символом подчёркивания _ (U+005F). Он позволяет опустить тип и имя компонента записи при Pattern Matching, например:

  • ... instanceof Point(int x, _)
  • case Point(int x, _)

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

  • ... instanceof Point(int x, int _)
  • case Point(int x, int _)

Безымянная переменная объявляется, когда подчёркиванием обозначена локальная переменная в операторе объявления локальной переменной, параметр исключения в предложении catch или параметр лямбда-выражения. Она позволяет опустить идентификатор, следующий за типом или var в операторе или выражении, например:

  • int _ = q.remove();
  • ... } catch (NumberFormatException _) { ...
  • (int x, int _) -> x + x

В случае лямбда-выражений с одним параметром, таких как _ -> "NODATA", безымянную переменную, служащую параметром, не следует путать с безымянным шаблоном.

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

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

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

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

Безымянный шаблон — безусловный шаблон, который ничего не связывает. Его можно использовать во вложенной позиции вместо шаблона типа или record-шаблона. Например,

  • ... instanceof Point(_, int y)

допустимо, а эти варианты — нет:

  • r instanceof _
  • r instanceof _(int x, int y)

Следовательно, в предыдущем примере можно полностью опустить шаблон типа для компонента Color:

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

Аналогично можно извлечь компонент Color, опустив record-шаблон для компонента Point:

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

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

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

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

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

Безымянная переменная шаблона может встречаться в любом шаблоне типа, находится ли он на верхнем уровне или вложен в record-шаблон. Например, оба этих случая допустимы:

  • r instanceof Point _
  • r instanceof ColoredPoint(Point(int x, int _), Color _)

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

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

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

В первых двух ветвях используются безымянные переменные шаблонов, потому что их правые части не используют компонент Box. В третьей, новой ветви используется безымянный шаблон, чтобы сопоставить Box с компонентом null.

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

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

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

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

Безымянный шаблон — сокращённая запись шаблона типа var _. Ни безымянный шаблон, ни var _ нельзя использовать на верхнем уровне шаблона, поэтому все эти варианты запрещены:

  • ... instanceof _
  • ... instanceof var _
  • case _
  • case var _

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

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

  • оператор объявления локальной переменной в блоке (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).

(Возможность объявить безымянную локальную переменную с помощью шаблона, то есть переменную шаблона (JLS 14.30.1), рассмотрена выше.)

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

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

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

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

    int acc = 0;
    for (Order _ : orders) {
        if (acc < LIMIT) { 
            ... acc++ ...
        }
    }

    В части инициализации обычного цикла 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();
       ... new Point(x, y) ...
    }

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

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

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

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

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

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

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

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

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

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

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

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

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