Java22-匿名变量/模式(Unnamed Variables Patterns)
序言
明确赋值
每个通过语句声明的局部变量和每个空白final
字段在访问其值时都必须具有明确赋值。
对其值的访问包括变量的简单名称(或者,对于字段,由限定的字段的简单名称)出现在表达式中的任何位置,除了作为简单赋值运算符,this的左边操作数 "=".
如果右侧操作数的类型与变量的类型不兼容,则会发生编译时错误。
否则,在运行时,将按以下三种方式之一评估表达式。
- 对左侧表达式进行求值(例如O.f,则会获取O的值),如果意外终止,则表达式也终止
- 对右侧操作数求值,如果意外终止,则表达式也终止
- 对左侧变量进行评估,不符合则终止
- 将右侧操作数的值赋予左侧变量
概括匿名变量/模式
使用未命名变量和未命名模式增强 Java 编程语言,当需要声明,但从未使用变量声明或嵌套模式时可以使用它们,两者都用下划线字符 表示_
。
目的
-
捕获开发人员的意图,开发者可以明确表示某个绑定(如变量)或 Lambda 表达式的参数不会被使用,通过工具或规则强制执行这一约定。这么做可以让程序更加清晰,并减少可能出现的错误。这样,开发者和工具都能更好地理解哪些参数是故意未使用的,避免误认为是代码缺陷。
-
增强代码的可维护性,通过识别那些必须声明但未使用的变量(例如
catch
语句中的变量),减少不必要的代码。 -
允许在
switch
语句的case
标签中出现多个模式,只要这些模式没有声明任何模式变量。 -
改善record模式的可读性,通过省略不必要的嵌套类型模式,使代码更加简洁明了。
非目的
-
允许未命名的字段或方法参数并不是目标。
-
例如,在明确赋值分析中,改变局部变量的语义并不是目标 。
动机
开发人员有时会声明他们不打算使用的变量,无论是出于代码风格还是因为语言在某些上下文中需要变量声明。不使用该变量的意图在编写代码时是已知的,但如果没有明确捕获,那么以后的维护者可能会意外使用该变量,从而违反意图。如果能够使意外使用此类变量成为不可能,那么代码将更具信息性、更易读,并且更不容易出错。
匿名变量(Unused variables)
声明一个未使用过的变量的需求在副作用比结果更重要的代码中尤其常见。
例如,此代码计算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块的代码执行的上下文;代码不直接使用上下文,因此资源变量的名称无关紧要。例如,假设ScopeContext资源是可自动关闭的,则以下代码将获取并自动释放上下文:
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"));
此代码生成一个映射,将每个键映射到相同的占位符值。由于未使用lambda参数v,因此其名称无关紧要。
所有这些情况,变量都是未使用的,其变量名是无意义的,如果我们可以简单地声明没有名称的变量,那就更好了。这将使维护人员不用区理解无意义的名称,并避免静态分析工具对未使用变量的警告。
可以合理地声明没有名称的变量是那些在方法外部不可见的变量:
- 局部变量
- 异常参数
- lambda参数
如上所示。这些类型的变量可以重命名或匿名,而不会受到外部影响(也可以理解为不会对外部产生影响)。相比之下,字段(即使它们是私有的)在方法之间传递对象的状态,而匿名名的状态既没有帮助也不可维护,所以字段不适合被声明为匿名变量。
匿名模式变量(Unused pattern variables)
局部变量也可以通过类型模式声明——这样的局部变量被称为模式变量——因此类型模式也可以声明未使用的变量。考虑以下代码,它在切换密封类Ball实例的switch语句的case标签中使用类型模式:
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的case使用类型模式检查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比前一个更复杂,因此省略嵌套类型模式中未使用的变量的名称将进一步提高可读性。
匿名嵌套模式(Unused nested patterns)
我们可以在record类中嵌套record类,从而导致数据结构的形状与其中的数据项同样重要。例如:
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
,如果是,则提取它的两个组成值。
ColoredPoint(Point p, Color c)
具有非常好的描述性,但程序通常只使用部分组件值进行进一步处理。
例如,上面的代码仅p
在 if
块中使用,而不是c。每次在进行模式匹配时都需要为record类的所有组件编写类型模式,显得有些繁琐。此外,如果某个组件(如 Color
)完全没有用到,这样的条件判断也不够直观,这可能会让代码的可读性变差,尤其是在记录模式嵌套的情况下。当记录模式嵌套以提取组件内的数据时,这一点尤其明显,如下所示:
if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
... x ... y ...
}
我们可以使用匿名的模式变量来降低视觉成本。
在 Java 的模式匹配中,如果我们只需要 ColoredPoint(Point(int x, int y), Color _)
中的 Point
,可以用 _
来忽略 Color
。不过,Color
类型仍显得冗余。我们可以用 var
简化成 ColoredPoint(Point(int x, int y), var _)
,但嵌套的 var _
仍显得过重。因此,进一步减少不必要的部分(如完全省略 Color
),可以简化代码并提升可读性。
说明(Description)
匿名变量是使用下划线字符_
(U+005F) 来声明的,它代替局部变量声明语句中的局部变量的名称、子句中的异常参数的名称catch
或 lambda 表达式中的 lambda 参数的名称。
匿名模式变量是使用下划线字符来声明的,以代替类型模式中的模式变量。
匿名模式用下划线字符表示,相当于匿名类型模式var _
。它允许在模式匹配中省略记录组件的类型和名称。
以下类型的声明可以引入命名变量(用标识符表示)或匿名变量(用下划线表示):
- 块中的局部变量声明语句(JLS§14.4.2)
- -with-resources 语句的资源规范
try
(JLS §14.20.3) - 基本for循环的头部(JLS §14.14.1)
- 增强for循环的头部(JLS §14.14.2)
- catch块的异常参数(JLS §14.20)
- lambda表达式的形参(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
在ry
-with-resources中使用:
try (var _ = ScopedContext.acquire()) { // Unnamed variable
... no use of acquired resource ...
}
一个参数无关的lambda中使用:
...stream.collect(Collectors.toMap(String::toUpperCase,
_ -> "NODATA")) // Unnamed variable
匿名模式变量
匿名的模式变量可以出现在类型模式中,包括var类型模式,无论该类型模式是出现在顶层还是嵌套在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中都更加清晰。
至此,Java匿名变量到此为止了,参考文章:JEP456