JEP draft: Enhanced Local Variable Declarations (Preview)
Расширенные объявления локальных переменных, версия Preview (предварительная версия)
| Ответственный | Angelos Bimpoudis |
| Тип | Feature |
| Область | SE |
| Статус | Draft |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | S |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2025/05/21 13:08 |
| Обновлён | 2026/05/21 15:56 |
| Задача | 8357464 |
Аннотация
Расширить объявления локальных переменных, чтобы из одного исходного значения можно было извлекать сразу несколько значений с помощью Pattern Matching (сопоставление с образцом). Так вычисления, ориентированные на данные, можно выражать кратко и безопасно, без дополнительного потока управления. Это Preview-возможность языка.
Цели
- Позволить извлекать сразу несколько значений из одного исходного значения, чтобы код был понятнее и яснее выражал намерение.
- Опираться на проверки применимости и исчерпываемости Pattern Matching, чтобы сделать извлечение информации безопаснее и сократить число неудачных сопоставлений в корректно типизированном коде.
- Сделать расширенные циклы
forвыразительнее, совместив итерацию с извлечением нужных значений из каждого элемента и сократив избыточный код. - Упростить рефакторинг, по возможности согласовав существующие объявления локальных переменных с расширенными объявлениями локальных переменных.
Что не является целью
- Изменение поведения Pattern Matching в
instanceofилиswitchне является целью.
Мотивация
Объявлять локальные переменные в Java просто, но это становится неудобно, когда нужно извлечь несколько связанных значений из одного исходного значения. Например, пусть вы хотите вычислить ограничивающий прямоугольник окружности:
record Circle(Point center, double radius) {}
record Point(int x, int y) {}
void boundingBox(Circle c) {
if (c != null) { // ┐
Point ctr = c.center(); // │ laborious / mechanical:
if (ctr != null) { // │ - null guards
int x = ctr.x(), y = ctr.y(); // │ - extraction with accessors
double radius = c.radius(); // ┘
int minX = (int) Math.floor(x - radius), maxX = (int) Math.ceil(x + radius);
int minY = (int) Math.floor(y - radius), maxY = (int) Math.ceil(y + radius);
... use minX, maxX, etc ...
}
}
}
Код, извлекающий данные из единственного исходного значения c, громоздок и заслоняет собственно вычисления: проверки null и вызовы методов доступа, из-за которых читателю приходится просматривать несколько строк, обёрнуты в условные конструкции, которые к тому же вложены друг в друга на пути к основной логике.
Pattern Matching позволяет извлекать несколько значений из одного исходного значения и инициализировать локальные переменные без лишних усилий. Например, метод boundingBox можно записать так:
void boundingBox(Circle c) {
if (c instanceof Circle(Point(int x, int y), double radius)) {
int minX = ..., maxX = ...
int minY = ..., maxY = ...
... use minX, maxX, etc ...
}
}
Этот код пытается сопоставить c с шаблоном Record Patterns (шаблоны записей) и, если сопоставление успешно, инициализирует сразу все три локальные переменные, объявленные в шаблоне. Шаблон извлекает компоненты из нескольких объектов, начиная с c: x и y — из center Point, принадлежащего c, а radius — из самого c.
Однако сейчас шаблоны можно использовать только в условных конструкциях: в instanceof, как показано выше, и в метках case оператора switch, из-за чего основная логика снова оказывается вложенной:
switch (c) {
case Circle(Point(int x, int y), double radius) -> {
int minX = ..., maxX = ...
int minY = ..., maxY = ...
... use minX, maxX, etc ...
}
}
В Pattern Matching for switch (JEP 441) появилось понятие исчерпывающего набора шаблонов, то есть набора, который охватывает все (разумные) случаи селектора. Было бы удобно, если бы шаблон, исчерпывающий для статического типа значения c, можно было использовать и в других, безусловных конструкциях. Тогда можно было бы объявлять и инициализировать сразу несколько локальных переменных в обычном коде блока, не добавляя дополнительный поток управления вокруг основного вычисления.
Мы предлагаем расширить объявления локальных переменных, чтобы вместо традиционного декларатора переменной, такого как int x, можно было использовать исчерпывающий record-шаблон. Тогда метод boundingBox можно записать так:
void boundingBox(Circle c) {
Circle(Point(int x, int y), double radius) = c;
int minX = ..., maxX = ...
int minY = ..., maxY = ...
... use minX, maxX, etc ...
}
Первая строка метода — это оператор расширенного объявления локальной переменной. Он сопоставляет c с исчерпывающим record-шаблоном слева от =. Знак = в этом операторе расширенного объявления локальной переменной выполняет Pattern Matching, а не присваивание. Поскольку шаблон должен быть применим и исчерпывающим для статического типа c, обычные несоответствия формы исключаются статически; оставшиеся ошибки во время выполнения — это остаточные случаи, рассмотренные ниже, и это именно то, что нужно, когда ваш код утверждает строгий инвариант о форме данных в этой точке программы.
Аналогично мы предлагаем разрешить расширенные объявления локальных переменных в заголовке расширенного цикла for. Тогда заголовок цикла может одновременно перебирать коллекцию и извлекать нужные значения из каждого элемента:
for (Circle(Point(int x, int y), double radius) : circles) {
... x ... y ... radius ...
}
Описание
Расширенное объявление локальной переменной P [ = e ] вводит record-шаблон, который обязан быть исчерпывающим для статического типа значения, с которым он сопоставляется, чтобы инициализировать переменные шаблона, содержащиеся в P. Объявление может содержать выражение e, которое сопоставляется с шаблоном, либо выражение предоставляется конструкцией, в которой находится объявление.
Расширенное объявление локальной переменной объявляет локальные переменные в шаблоне P и инициализирует их, сопоставляя P со значением выражения e.
Оператор расширенного объявления локальной переменной включает расширенное объявление локальной переменной и имеет следующий вид: P = e ;, где e всегда присутствует в объявлении, а за самим объявлением следует ;. Так можно извлечь состояние объекта в несколько переменных за одну операцию:
Circle(Point center, double radius) = getCircle();
... center ... radius ...
Оператор расширенного объявления локальной переменной разбирает экземпляр record справа на его компоненты. Record-шаблоны можно вкладывать друг в друга, чтобы дальше разбирать эти компоненты на их подкомпоненты:
Circle(Point(int x, int y), double radius) = getCircle();
... x ... y ... radius ...
Чтобы опустить неинтересные компоненты, в шаблоне можно использовать _. Как и в других контекстах шаблонов, допускается и var _; например, если вам нужна только координата x центральной точки:
Circle(Point(int x, var _), _) = getCircle();
... x ...
Это Preview-возможность языка, по умолчанию она отключена
Чтобы опробовать описанные здесь изменения, нужно включить Preview-возможности:
-
Скомпилируйте программу с
javac --release 27 --enable-preview Main.javaи запустите её сjava --enable-preview Main; или -
Если вы используете средство запуска исходного кода, запустите программу с
java --enable-preview Main.java; или -
Если вы используете
jshell, запустите его сjshell --enable-preview.
Синтаксис и семантика
Расширенные объявления локальных переменных — это новая форма объявления, а операторы расширенного объявления локальной переменной — новый вид оператора блока в грамматике Java.
BlockStatement:
LocalClassOrInterfaceDeclaration
LocalVariableDeclarationStatement
EnhancedLocalVariableDeclarationStatement
Statement
EnhancedLocalVariableDeclarationStatement:
EnhancedLocalVariableDeclaration ;
EnhancedLocalVariableDeclaration:
RecordPattern [ = Expression ]
где:
RecordPattern:
ReferenceType ( [ComponentPatternList] )
ComponentPatternList:
ComponentPattern {, ComponentPattern }
ComponentPattern:
Pattern
_
Расширенное объявление локальной переменной P [ = e ] выражает намерение автора: record-шаблон P гарантированно совпадает с экземпляром record из e. Для расширенного объявления локальной переменной P [ = e ] действуют следующие правила (в примерах предполагается record Point(int x, int y) {...}):
-
Pдолжен быть record-шаблоном с той же арностью, что и record-тип, указанный вP; -
Pдолжен быть применим к типуe, то есть допустимость сопоставления шаблона с выражением проверяется во время компиляции; и -
P(одноэлементное множество{ P }) является исчерпывающим дляe, то есть способность шаблона совпасть со всеми возможными значениями справа проверяется во время компиляции.
Для оператора расширенного объявления локальной переменной P = e; действуют следующие правила:
-
У оператора расширенного объявления локальной переменной всегда есть инициализатор
e. Например, нельзя написатьPoint(int x, int y);само по себе: за этим должны следовать=и выражение. -
Оператор расширенного объявления локальной переменной вводит новые локальные переменные с именами, объявленными в шаблоне. Эти имена не должны затенять переменные, которые уже находятся в области видимости в точке объявления, — так же, как переменные шаблона в
instanceofиswitch. После объявления эти переменные — обычные локальные переменные, к которым применяются обычные правила области видимости и присваивания. Например:Point(int x, int y) = p; x = 42; // okно повторно использовать имя существующей локальной переменной нельзя:
int x = 42; Point(int x, int y) = p; // error -
Оператор расширенного объявления локальной переменной может находиться везде, где может находиться традиционный оператор объявления локальной переменной: в теле метода или непосредственно в блоке такого оператора, как
if,while,forи т. д. Так Pattern Matching можно свободно чередовать с другим кодом. Локальные переменные, объявленные оператором расширенного объявления локальной переменной, находятся в области видимости до конца объемлющего блока — так же, как локальные переменные, объявленные оператором объявления локальной переменной.Circle resize(Circle c, int factor) { Circle(Point center, double radius) = c; assert center != null; Point(int x, int y) = center; return new Circle(new Point(x+factor, y+factor), radius+factor); } -
Оператор расширенного объявления локальной переменной не объявляет имя для самого record-шаблона. Если нужны и значение целиком, и его компоненты, объявите всё значение отдельно, а затем разберите его:
Circle wholeCircle = getCircle(); Circle(Point center, double radius) = wholeCircle; // data-oriented operations wholeCircle.method(); // any other possible computationsТак операции извлечения, ориентированные на данные, остаются в шаблоне, а любое другое использование значения целиком остаётся явным. Написать
Circle(Point center, double radius) wholeCircle = getCircle();нельзя. Это был бы именованный record-шаблон, который не входит в этот JEP и по-прежнему рассматривается как отдельная возможность, относящаяся к Pattern Matching в целом; см. обсуждение. -
У оператора расширенного объявления локальной переменной, как и у традиционных операторов объявления локальной переменной, нет формы выражения. Это значит, что нельзя написать
Point p = (Point(int x, int y) = e);, так же как нельзя написатьPoint p = (Point p2 = point);.
Оператор расширенного объявления локальной переменной мощнее традиционного оператора объявления локальной переменной. В одном операторе объявления локальной переменной можно инициализировать несколько переменных, но у каждой переменной должен быть свой инициализатор, например int x = center.getX(), y = center.getY();. Оператор расширенного объявления локальной переменной, напротив, инициализирует несколько переменных значениями, извлечёнными из одного инициализатора, например Point(int x, int y) = center;.
Оператор расширенного объявления локальной переменной также наследует композиционность шаблонов: вместо того чтобы постепенно извлекать вложенное состояние несколькими операторами, можно выразить извлечение в одном объявлении с помощью вложенного record-шаблона. Например:
Circle(Point(int x, int y), double radius) = c;
вместо:
Circle(Point center, double radius) = c;
assert center != null;
Point(int x, int y) = center;
Вложенные шаблоны не обязаны умещаться в одной физической строке: при необходимости их можно разбить на несколько строк так же, как другие конструкции Java.
Аналогия между операторами расширенного объявления локальной переменной и Pattern Matching в switch
Оператор расширенного объявления локальной переменной P = e; можно считать эквивалентом оператора switch с e в качестве селектора и блоком switch, содержащим единственный case P:
switch (e) {
case P: ...
}
поскольку правила для switch уже требуют, чтобы:
- шаблон
Pбыл применим к типуeи - одноэлементное множество
{ P }было исчерпывающим дляe.
Например,
Circle(Point center, double radius) = getCircle();
допустимо по той же причине, по которой допустим этот switch:
switch (getCircle()) {
case Circle(Point center, double radius) -> ... center ... radius ...
}
У switch, где больше одного case или один case вместе с default или case null, нет аналога в виде оператора расширенного объявления локальной переменной.
Исключения в операторе расширенного объявления локальной переменной
Оператор расширенного объявления локальной переменной отличается от традиционного оператора объявления локальной переменной, когда инициализатор справа от = вычисляется в null. В традиционном объявлении локальной переменной значение null присваивается локальной переменной:
Point p = getPoint(); // if getPoint() returns null, p is null
а в операторе расширенного объявления локальной переменной выбрасывается NullPointerException, потому что оператор выполняет Pattern Matching над значением инициализатора, а на верхнем уровне ни один record-шаблон не совпадает с null:
Point(int x, int y) = getPoint(); // if getPoint() returns null, throw NPE
Если null — допустимый результат, пользователь может построить код так, чтобы обрабатывать его явно, и выполнять оператор расширенного объявления локальной переменной, только когда есть значение, отличное от null:
if (p == null) {
// handle null case
} else {
Point(int x, int y) = p;
// use x, y
}
Как и при Pattern Matching в switch, расширенное объявление локальной переменной всё же может не совпасть во время выполнения из-за остаточных значений. Множество остаточных значений содержит значения, с которыми шаблон не совпадает, хотя во время компиляции оператор был признан исчерпывающим. Когда оператор завершает выполнение, не сопоставив значение справа, потому что это значение входит в остаточное множество, он выбрасывает MatchException. Таких ситуаций две, плюс третья, в которой оператор завершается аварийно во время разбора и тоже выбрасывает MatchException.
-
Когда
nullвходит в остаточное множество из-за вложенного record-шаблона:Рассмотрим этот оператор, где у экземпляра
Circleсправа центр равен null:Circle(Point(int x, int y), double r) = new Circle(null, 2.0); // throws MatchExceptionОстаточное множество вложенных record-шаблонов, таких как
Point(int x, int y), всегда содержитnull: они никогда не могут совпасть сnull. В результате оператор не совпадает с экземпляромCircleи выбрасываетMatchException, поскольку расширенное объявление локальной переменной завершило выполнение, не сопоставив значение справа.Хотя вложенные record-шаблоны никогда не совпадают с
null, некоторые шаблоны типа могут совпадать, если во время компиляции доказано, что они всегда будут совпадать со всеми возможными значениями этого типа во время выполнения. Рассмотрим приведённый выше оператор, но с шаблоном типаPoint pвместо record-шаблонаPoint(int x, int y):Circle(Point p, double r) = new Circle(null, 2.0);Этот оператор не выбрасывает исключение: шаблон типа
Point pможет совпасть со всеми значениямиPointсоответствующего компонента (включаяnull). Поэтому оператор совпадает со значением справа, иpбудет инициализирован значениемnull. -
Когда в остаточном множестве оказываются значения нового типа из-за проблемы раздельной компиляции:
Остаточные значения могут появиться, когда иерархии Sealed Classes (запечатанные классы) изменились. Рассмотрим метод ниже, скомпилированный с интерфейсом
S, у которого есть только одна разрешённая реализация:// library V1 sealed interface S permits Only { } record Only(int v) implements S { } // user code compiled against V1 static void m(S s) { Only(int v) = s; System.out.println(v); }Если позже кто-то разрешит вторую реализацию:
// later library evolves to V2 without recompiling meth sealed interface S permits Only, Other {} record Only(int v) implements S { } record Other(int w) implements S {} // a new subtypeно метод не будет перекомпилирован, то все значения типа
Otherпопадут в остаточное множество. Поскольку оператор расширенного объявления локальной переменной в методе не совпадает со значениями типаOther, оператор выброситMatchException, и вызывающему коду придётся с этим разбираться:m(new Other(42)); // throws MatchException -
Когда деконструкция выбрасывает исключение при извлечении данных из правой части.
Возьмём следующую запись, в которой метод доступа к компоненту
xпереопределён так, что не возвращает значение компонента.record Point(int x, int y) { @Override public int x() { throw new IllegalStateException("bad x"); } }Попытка обратиться к компоненту
xобычным вызовом метода выбрасывает ожидаемое исключение:Point exceptional = new Point(1, 2); exceptional.x(); // throws IllegalStateExceptionПоскольку оператор расширенного объявления локальной переменной определяет каждый компонент записи, вызывая соответствующий метод доступа, то если метод доступа выбрасывает исключение, оператор выбрасывает
MatchExceptionс причинойe:Point exceptional = new Point(1, 2); Point(int x, int y) = exceptional; // throws MatchException
Единая модель LHS = RHS и простой рефакторинг
Оператор объявления локальной переменной T x = e; традиционно читается как объявление одной новой локальной переменной x типа T и её инициализация значением e. Если e вычисляется в null, то x инициализируется значением null.
Объявление локальной переменной и шаблон типа имеют одинаковую форму — T x — и одинаковое назначение: объявить одну новую переменную, инициализированную из некоторого экземпляра. Соответственно, оператор объявления локальной переменной T x = e; можно также рассматривать как switch с единственной меткой case, шаблон верхнего уровня в которой — шаблон типа T x:
switch (e) {
case T x -> ...
}
Это даёт удобную единую мысленную модель для операторов вида LHS = RHS: они инициализируют объявленные переменные значением правой части либо завершаются неудачей. Одно важное исключение — обработка null. Оператор объявления локальной переменной T x = e; по-прежнему инициализирует x значением null, тогда как соответствующий оператор switch выбросил бы NullPointerException.
Этот JEP расширяет такое прочтение LHS = RHS, позволяя левой части быть не только традиционным объявлением локальной переменной, но и более выразительными формами шаблонов. В частности, оператор расширенного объявления локальной переменной P(...) = e; также можно рассматривать как оператор switch с единственной меткой case, шаблон верхнего уровня в которой — record-шаблон P(...):
switch (e) {
case P(...) -> ...
}
Record-шаблон P(...) идёт на шаг дальше: он рекурсивно извлекает состояние экземпляра в одну или несколько новых переменных. Более того, если e вычисляется в null, то оператор расширенного объявления локальной переменной P(...) = e; выбрасывает NullPointerException и не инициализирует ни одну из переменных шаблона в P. Отсюда следует мысленная модель, в которой операторы расширенного объявления локальной переменной являются обобщением операторов объявления локальной переменной: вместо того чтобы вводить одно локальное имя для одного значения, левая часть может вводить несколько локальных имён для нескольких значений, извлечённых из одного исходного значения.
Из этого обобщения следует простой рефакторинг. Поэтому традиционная и расширенная формы должны согласовываться везде, где это возможно. Без такого согласования простой рефакторинг может приводить к появлению или исчезновению приведения типа только потому, что изменилась форма левой части. Sealed Classes и sealed-интерфейсы (JEP 409) позволяют авторам библиотек предоставлять стабильный контракт, сохраняя строгий контроль над поведением и инвариантами. В частности, авторы библиотек могут объявить абстрактный sealed-класс или sealed-интерфейс для своего публичного API и разрешить ровно один подкласс — свой собственный. Это распространённый приём программирования, за которым следует необходимость сразу же «восстановить» этот подкласс в локальном коде, обычно с помощью явного нисходящего приведения. В качестве примера рассмотрим sealed-иерархию:
sealed interface Foo permits FooImpl {
String data();
}
record FooImpl(String data) implements Foo { }
Внутри библиотеки её автор часто будет писать операторы объявления локальной переменной с нисходящим приведением к разрешённому подклассу, например:
public void libraryMethod(Foo f) {
FooImpl fi = (FooImpl) f;
String s = fi.data();
... s ...
}
Это шаблонный код: он выражает инвариант, который уже следует из sealed-иерархии. Кроме того, такое явное нисходящее приведение чревато ошибками. Если sealed-иерархию изменить, чтобы поддержать больше разрешённых подтипов, этот инвариант нарушится, но нисходящее приведение по-прежнему будет компилироваться; единственным сигналом о сбое будет возможное исключение ClassCastException во время выполнения.
При единой модели LHS = RHS ту же операцию должно быть возможно выразить и оператором расширенного объявления локальной переменной, напрямую извлекая компонент:
public void libraryMethod(Foo f) {
FooImpl(String s) = f;
... s ...
}
Это допустимо, потому что шаблон FooImpl(String s) является исчерпывающим для Foo. В Pattern Matching тому факту, что Foo является sealed с единственной разрешённой реализацией, придаётся больший вес, чем возможности того, что Foo может быть независимо изменён, чтобы разрешить больше реализаций.
Если FooImpl(String s) = f; допустимо, потому что FooImpl является исчерпывающим для Foo, то FooImpl fi = f; тоже должно быть допустимо, когда программист хочет дать имя всему значению, а не сразу его деконструировать.
Однако без соответствующей поддержки для традиционных объявлений локальных переменных рефакторинг между двумя формами требовал бы вставки или удаления приведения типа без какой-либо семантической причины. Поэтому мы предлагаем усилить статическую проверку в языке, позволив компилятору использовать сведения о sealed-типах с единственной реализацией при проверке таких присваиваний: экземпляр, реализующий интерфейс, можно присвоить переменной класса, реализующего этот интерфейс, если интерфейс является sealed и единственная разрешённая реализация — этот класс. Это значит, что автор библиотеки может использовать оператор объявления локальной переменной без нисходящего приведения:
public void libraryMethod(Foo f) {
FooImpl fi = f; // previously: (FooImpl) f
... fi.data() ...
}
Так обычная форма лучше согласуется с исчерпывающими шаблонами типа, а рефакторинг между именованием всего значения и именованием его компонентов становится простым. Это также безопаснее явного приведения: если sealed-иерархия позже расширится, перекомпиляция любой из форм даст ошибку компиляции, а не оставит на месте устаревшее приведение:
public void libraryMethod(Foo f) {
FooImpl fi = f;
String s = fi.data();
... s ...
}
public void libraryMethod(Foo f) {
FooImpl(String s) = f;
... s ...
}
Если объявление sealed-интерфейса Foo позже изменить так, чтобы разрешить дополнительную реализацию, например:
sealed interface Foo permits FooImpl, BarImpl {
String data();
}
то FooImpl(...) больше не является исчерпывающим для Foo, поэтому перекомпиляция оператора объявления локальной переменной FooImpl fi = f; или оператора расширенного объявления локальной переменной FooImpl(String s) = f; даёт ошибку компиляции, а не оставляет на месте устаревшее явное приведение. Если вместо перекомпиляции вы просто запустите старые class-файлы как есть, оператор объявления локальной переменной FooImpl fi = f; ведёт себя так, как если бы проверяемое приведение (FooImpl) f было записано явно, и выбрасывает ClassCastException; он не транслируется в switch или в оператор расширенного объявления локальной переменной. Оператор расширенного объявления локальной переменной FooImpl(String s) = f; выбрасывает MatchException, потому что кандидат на сопоставление f ссылается на остаточное значение.
Возможность присвоить выражение типа Foo переменной типа FooImpl не «переносится» на типы массивов: нельзя присвоить выражение типа Foo[] переменной типа FooImpl[], даже если Foo — интерфейс с модификатором sealed, у которого FooImpl является единственной разрешённой реализацией. Например, если fs имеет тип Foo[], то FooImpl[] fis = fs; недопустимо. Причина в том, что JVM материализует типы массивов и обеспечивает ковариантность массивов: объект Foo[] никогда нельзя привести вниз к FooImpl[], даже если все элементы объекта Foo[] являются объектами FooImpl.
Следствием такого усиления типизации присваиваний является то, что нисходящие приведения от интерфейса или суперкласса к единственной разрешённой реализации больше не требуются в следующих местах:
-
Присваивание с помощью простого оператора присваивания
=:FooImpl fi = ... fi = f; // previously: (FooImpl) f; -
Присваивание в расширенном операторе
for:for(FooImpl fi : fs) { } // error before, no way to cast -
Присваивание элементам массива:
Foo f = ... FooImpl[] fi = new FooImpl[1]; fi[0] = f; // previously: (FooImpl) f; -
Присваивание через инициализаторы массивов:
Foo f = ... FooImpl[] fis = { f, f, f }; -
Возврат из метода:
FooImpl m() { Foo f = ... return f; } -
Ссылки на методы:
record Box(Foo foo) {} Box b = new Box(new FooImpl("test")); Supplier<FooImpl> sup = b::foo; -
Лямбда-выражения:
Supplier<FooImpl> sup = () -> { Foo f = new FooImpl("test"); return f; };
Это улучшение также применяется к классам, которые являются abstract и sealed и имеют только один подкласс в качестве разрешённой реализации.
sealed abstract class A permits B {}
final class B extends A {}
void m(A a) {
B b = a; // now legal
}
Одно ограничение этого единого прочтения (помимо null в качестве RHS) возникает с примитивными шаблонами (предложение, добавляющее Primitive Types in Patterns (примитивные типы в шаблонах) в instanceof и switch, см. в разделе Зависимости). Оператор объявления локальной переменной T x = e;, когда T — примитивный тип, может не иметь эквивалента в виде switch с единственной меткой case. Оператор объявления локальной переменной включает особые правила преобразования констант, которые не согласуются с исчерпываемостью в Pattern Matching. Например, byte b = 42; разрешено благодаря сужающему преобразованию константного выражения типа int к byte, поскольку статически известно, что 42 представимо в диапазоне byte. Однако в switch ниже case byte b не является исчерпывающим для селектора типа 42, и требуется default:
switch (42) { // error: not exhaustive
case byte b -> {...}
}
Расширенные объявления локальных переменных в расширенных операторах for
Операторы расширенного объявления локальной переменной можно использовать в теле расширенного оператора for, например:
for (Circle c : circles) {
Circle(Point(int x, _), double radius) = c;
... x ... radius ...
}
Оператор расширенного объявления локальной переменной часто будет использовать переменную цикла (c) в правой части. Этот JEP предлагает разрешить использовать расширенные объявления локальных переменных непосредственно в заголовке цикла:
for (Circle(Point(int x, _), double radius) : circles) {
... x ... radius ...
}
Грамматика расширенного оператора for соответственно расширяется:
EnhancedForStatement:
for ( LocalVariableDeclaration : Expression ) Statement
for ( EnhancedLocalVariableDeclaration : Expression ) Statement
Интеграция Pattern Matching в заголовок расширенного оператора for поддерживает прямое, ориентированное на данные извлечение в точке итерации, а не введение нового имени для «элемента цикла» лишь затем, чтобы тут же разобрать его с помощью методов доступа. В типичном случае любое имя для элемента цикла было бы избыточным: как только record-шаблон связал интересующие компоненты (и проигнорировал остальные), сам элемент для выражения вычисления больше не нужен.
Если расширенное объявление локальной переменной используется в заголовке расширенного оператора for, то объявление обрабатывается так, как если бы у него было сопоставляемое выражение e, тип которого — тип итерации расширенного оператора for. Это не механизм фильтрации или частичного совпадения; здесь действуют те же требования применимости и исчерпываемости, что и для формы оператора.
Расширенное объявление локальной переменной в заголовке расширенного оператора for, как и в операторе расширенного объявления локальной переменной, объявляет новые локальные переменные и не может объявлять переменные, затеняющие существующие локальные переменные.
Во время выполнения при выполнении расширенного оператора for for (P : e) ... на каждой итерации цикла выполняется Pattern Matching: P сопоставляется с последовательными значениями e. Исключения, которые могут возникнуть во время выполнения, те же, что и в операторе расширенного объявления локальной переменной.
В результате расширенный оператор for, выполняющий Pattern Matching, в отличие от традиционного расширенного оператора for, не допускает значений null, получаемых из выражения Iterable: попытка сопоставить null приведёт к выбросу NPE. Если вы хотите обрабатывать значения null иначе, например пропускать их, нужно явно проверить на null, а затем использовать оператор расширенного объявления локальной переменной в теле цикла:
for (Circle c : circles) {
if (c != null) {
Circle(Point(int x, _), double radius) = c;
... x ... radius ...
}
}
Зависимости
Для обеспечения корректности расширенные объявления локальных переменных опираются на правила применимости и исчерпываемости. «Primitive Types in Patterns» (JEP 530) расширяет эти правила, разрешая все типы — примитивные и ссылочные — в шаблонах типа в instanceof и switch. Сочетание этих двух Preview-возможностей даёт пользователю более точный контроль над вложенными шаблонами в левой части:
record Point(int x, int y) {}
Point p = ...
Point(long x, long y) = p;
Альтернативы
В качестве альтернативы можно было бы ввести новую форму оператора с новым ключевым словом, например let, для обозначения объявления с шаблоном, например:
let Circle(...) = e;
Исторически Java развивается не за счёт новых ключевых слов для каждой новой идиомы: со временем язык повторно использует и расширяет существующие конструкции на более высоком уровне (хороший пример — Pattern Matching через instanceof и switch). В этом духе мы предпочитаем расширить операторы объявления локальных переменных, а не добавлять новый оператор «let».
Новое ключевое слово также плохо масштабировалось бы на другие места, где сегодня встречаются объявления локальных переменных. Например, новый синтаксис добавил бы визуальный шум и ухудшил бы читаемость в расширенном операторе for (например, for (let Circle(...) : circles) ...) и усложнил бы любое будущее расширение объявлений локальных переменных в любом из других мест, где они сейчас разрешены.
Наконец, нам следует закладывать возможность развития и сохранять согласованность. Расширяя операторы объявления локальных переменных, мы можем усилить и традиционные объявления локальных переменных: на них будут распространяться те же рассуждения, основанные на исчерпываемости, и благодаря этому рефакторинг от старых форм к новым становится более плавным. Новое ключевое слово для новой конструкции (без усиления старой) создало бы в языке асимметрию, которой можно избежать.
Риски и допущения
Риск добавления операторов расширенного объявления локальных переменных в язык Java в том, что они как будто «перегружают» оператор =. Традиционно = обозначает инициализацию в объявлениях локальных переменных (JLS 14.4.2) и присваивание в выражениях, объявлениях полей и элементах аннотаций (JLS 15.26.1; например, @Foo(x=1, y="Hello")). В операторе расширенного объявления локальной переменной = обозначает Pattern Matching. Мы ожидаем, что путаница будет минимальной, по двум причинам:
-
Левая часть расширенного объявления локальной переменной — это шаблон вида Record Patterns, который визуально отличается от одиночной переменной.
-
Как и у традиционного объявления локальной переменной, у расширенного объявления локальной переменной нет формы выражения. Поэтому нельзя написать
bar(Point(int x, int y) = e);, чтобы объявить и инициализироватьxиyв вызывающем коде (так же как нельзя написатьbar(int p = e);, чтобы объявить локальную переменнуюp). Оператор присваивания в Java, напротив, порождает выражение присваивания (x = e), которое присваивает значение существующей переменной и может быть встроено в более крупные выражения (например,foo(x = 5);); оно также может выступать как оператор-выражение (x = e;).
Существует риск, что перенос кода с традиционного оператора объявления локальной переменной на оператор расширенного объявления локальной переменной может изменить поведение для значений null. Первый может без проблем инициализировать переменную значением null, тогда как второй выполняет Pattern Matching и выбросит NullPointerException, если выражение в правой части вычисляется в null. Например, рефакторинг Point p = getPoint(); в Point(int x, int y) = getPoint(); меняет поведение, когда getPoint() возвращает null, из-за чего ранее работавшие ветви кода могут начать давать сбой во время выполнения. Мы предполагаем, что IDE и инструменты автоматического рефакторинга помогут, предлагая подходящие проверки, сохраняющие исходный замысел.
Дальнейшая работа
После введения расширенных объявлений локальных переменных в операторах и в заголовке расширенных операторов for мы можем рассмотреть следующее:
-
Неисчерпывающий аналог, в котором неудачное сопоставление не исключается самим объявлением.
-
Дополнительные контексты расширенного объявления локальных переменных, например спецификации ресурсов в
try-with-resources, параметры исключений в предложенияхcatchи параметры лямбда-выражений. -
Если Record Patterns получат именованную деконструкцию, расширенные объявления локальных переменных автоматически получат эту возможность.
-
Аналогично, если Record Patterns позже получат шаблоны массивов и шаблоны для переменного числа аргументов (vararg), расширенные объявления локальных переменных автоматически получат эту возможность, и реже придётся выписывать длинные хвосты неинтересных компонентов с помощью
_.