跳到主要内容

JEP 456:未命名变量和模式

概括

使用未命名变量和未命名模式增强 Java 编程语言,当需要变量声明或嵌套模式但从未使用时可以使用它们。两者均由下划线字符 表示_

历史

未命名变量和未命名模式首先通过JEP 443在 JDK 21 中预览,其标题为_未命名模式和变量_。我们在此建议不做任何更改地完成此功能。

目标

  • 捕获开发人员的意图,即给定的绑定或 lambda 参数未使用,并强制执行该属性,以便澄清程序并减少出错的机会。

  • 通过识别必须声明(例如,在catch子句中)但未使用的变量来提高所有代码的可维护性。

  • 允许多个模式出现在单个case标签中,前提是它们都没有声明任何模式变量。

  • 通过消除不必要的嵌套类型模式来提高记录模式的可读性。

非目标

动机

开发人员有时会声明他们不打算使用的变量,无论是出于代码风格的原因还是因为该语言需要在某些上下文中声明变量。不使用的意图在编写代码时是已知的,但如果没有明确捕获它,那么后来的维护者可能会意外地使用该变量,从而违反意图。如果我们能够避免意外使用此类变量,那么代码将提供更多信息、更具可读性并且更不易出错。

未使用的变量

在副作用比其结果更重要的代码中,需要声明一个从未使用过的变量尤其常见。例如,此代码计算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,这会导致不对称,并可能导致有关忽略返回值的静态分析警告,否则

  • 声明未使用的变量,可能会收到有关未使用变量的静态分析警告。

未使用的变量经常出现在另外两个关注副作用的语句中:

  • -with-resources语句try总是因其副作用而被使用,即自动关闭资源。在某些情况下,资源代表块代码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"));

此代码生成一个映射,将每个键映射到相同的占位符值。由于未使用 lambda 参数v,因此其名称无关紧要。

在所有这些情况下,如果变量未被使用并且它们的名称不相关,那么如果我们可以简单地声明没有名称的变量,那就更好了。这将使维护人员不必理解不相关的名称,并避免静态分析工具未使用时的误报。

可以合理地不使用名称声明的变量类型是那些在方法外部不可见的变量:局部变量、异常参数和 lambda 参数,如上所示。这些类型的变量可以重命名或未命名,而不会受到外部影响。相比之下,字段(即使它们是private)通过方法传达对象的状态,而未命名的状态既没有帮助,也无法维护。

未使用的模式变量

局部变量也可以通过类型模式来声明——这样的局部变量称为_模式变量_——因此类型模式也可以声明未使用的变量。考虑以下代码,它在切换类实例的语句case的标签中使用类型模式:switch``sealed``Ball

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模式变量redblue和。如果我们可以省略这些变量名,这段代码会更清晰。green``case

现在假设我们定义一个记录类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比前一个更复杂,因此省略嵌套类型模式中未使用的变量的名称将进一步提高可读性。

未使用的嵌套模式

我们可以在记录中嵌套记录,从而导致数据结构的形状与其中的数据项同样重要的情况。例如:

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来测试变量是否为 a ColoredPoint,如果是,则提取其两个分量值。

诸如此类的记录模式ColoredPoint(Point p, Color c)具有令人愉快的描述性,但程序通常仅使用某些组件值进行进一步处理。例如,上面的代码仅pif块中使用,而不是c.每次我们进行这样的模式匹配时,为记录类的所有组件写出类型模式是很费力的。此外,从视觉上看并不清楚整个Color组件是不相关的;这也使得块中的条件if更难以阅读。当记录模式嵌套在组件内提取数据时,这一点尤其明显,如下所示:

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

我们可以使用未命名的模式变量来减少视觉成本,例如,但是类型模式中类型ColoredPoint(Point(int x, int y), Color _)的存在会分散注意力。Color我们可以使用var,eg来删除它ColoredPoint(Point(int x, int y), var _),但是嵌套类型模式var _仍然具有过多的权重。最好通过完全省略不必要的组件来进一步降低视觉成本。通过消除代码中的混乱,这既可以简化编写记录模式的任务,又可以提高可读性。

描述

使用下划线字符 (U+005F) 来声明_未命名变量_,_以代表局部变量声明语句中的局部变量的名称、子句中的异常参数catch或 lambda 表达式中的 lambda 参数。

_未命名的模式变量_是通过使用下划线字符来代表类型模式中的模式变量来声明的。

_未命名模式_由下划线字符表示,相当于未命名类型模式var _。它允许在模式匹配中省略记录组件的类型和名称。

单个下划线字符是表示名称缺失的最简单合理的语法。它通常用于其他语言,例如 Scala 和 Python,用于此目的。单个下划线最初是 Java 1.0 中的有效标识符,但后来我们将其回收用于未命名的变量和模式:当下划线在 Java 8 (2014) 中用作标识符时,我们开始发出编译时警告,并且我们删除了此类在 Java 9(2017, JEP 213)中,语言规范中的标识符,从而将这些警告转化为错误。

在长度为 2 或以上的标识符中使用下划线的能力没有改变,因为下划线仍然是 Java 字母和 Java 字母或数字。例如,诸如_ageandMAX_AGE__(两个下划线)之类的标识符仍然是合法的。

使用下划线作为数字分隔符的能力也没有改变。例如,诸如123_456_789和 之类的数字文字0b1010_0101仍然是合法的。

未命名变量

以下类型的声明可以引入命名变量(由标识符表示)或未命名变量(由下划线表示):

  • 块中的局部变量声明语句(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) ...
    }

    如果程序只需要处理x1x2等坐标,则可以在多个赋值语句中使用未命名变量:

    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资源中:

    try (var _ = ScopedContext.acquire()) {    // Unnamed variable
    ... no use of acquired resource ...
    }
  • 参数不相关的 lambda:

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

未命名的模式变量

未命名的模式变量可以出现在类型模式 (JLS §14.30.1) 中,包括var类型模式,无论类型模式是出现在顶层还是嵌套在记录模式中。例如,Ball现在可以将示例编写为:

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

以及BoxBall示例:

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具有不同模式但右侧相同的子句。例如,在BoxandBall示例中,前两个子句具有相同的右侧但模式不同:

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();
}

因此,我们将开关标签的语法(JLS §14.11.1)修改为

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

并将具有多个模式的标签的语义定义case为与某个值匹配(如果该值与任何模式匹配)。

如果case标签具有多个模式,则任何模式声明任何模式变量都会出现编译时错误。

case具有多个图案的标签可以case有一个防护罩。守卫控制的是case整体,而不是个体模式。例如,假设有一个int变量xcase则可以进一步限制上一个示例中的第一个变量:

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 _,未命名模式可以嵌套在记录模式中。但是,它不能用作instanceof表达式或标签等中的顶级模式case

因此,前面的示例可以Color完全省略组件的类型模式:

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

同样,我们可以提取Color组件值,同时忽略组件的记录模式Point

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

在深度嵌套的位置中,使用未命名模式可以提高执行复杂数据提取的代码的可读性。例如:

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

此代码提取x嵌套的坐标Point,同时明确表示不提取y和组件值。Color

重新审视BoxandBall示例,我们可以通过使用未命名模式而不是 来进一步简化其最终 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 302Lambda Leftovers)检查了未使用的 lambda 参数的问题,并确定了下划线的作用来表示它们,但也涵盖了许多其他问题,这些问题通过其他方式得到了更好的处理。此 JEP 解决了 JEP 302 中探讨的未使用 lambda 参数的使用问题,但没有解决其中探讨的其他问题。