Java22 で追加される無名変数と無名パターン (JEP 456 Unnamed Variables & Patterns)

blog1.mammb.com


はじめに

JDK 21 でプレビュー公開された Unnamed Variables & Patterns が、JDK 22 で JEP 456 正式リリースされることになりました。プレビュー版からは特に機能の変更は入らずに正式リリースとなります。

Unnamed Variables & Patterns により、未使用の、変数宣言やネストされたパターンを _ 文字で省略して書くことができるようになります。省略表記は、大きく以下の3種に大別されます。

  • Unnamed variables (無名変数)
    • ローカル変数宣言文のローカル変数名を _ で省略表記
    • つまり int xint _ の様に記載( var _ も可)
  • Unnamed pattern variables (無名パターン変数)
    • 型パターンのパターン変数を _ で宣言
    • つまり case RedBall red -> foo();case RedBall _ -> foo(); の様に記載
  • Unnamed pattern (無名パターン)
    • レコードコンポーネントの型と名前の両方_ で宣言
    • 無名型パターン var _ と等価
    • つまり ColoredPoint(Point(int x, int y), Color c) というパターンを ColoredPoint(Point(int x, int y), _) のように記載

最後の 「Unnamed pattern (無名パターン)」 がこのJEPのポイントで、最初の2つはおまけ程度と考えておけば良いと思います。

また、本JEPでは、Unnamed method parameters は対象外なので、メソッドの引数に無名変数を使うことは現状ではできません。


Unnamed variables (無名変数)

無名変数は以下で使用することができます。

  • ブロック内のローカル変数宣言文
  • try-with-resources文のリソース指定
  • 基本的な for ループのヘッダー
  • 拡張 for ループのヘッダー
  • キャッチブロックの例外パラメータ
  • ラムダ式の正規パラメータ
ブロック内のローカル変数宣言文
while (q.size() >= 3) {
    var x = q.remove();
    var _ = q.remove(); // var y = q.remove();
    var _ = q.remove(); // var z = q.remove();
    ... new Point(x, 0) ...
}

無名変数は名前を持たないので、同じブロック内で複数の無名変数を宣言することができます。

try-with-resources文のリソース指定
// try (var acquiredContext = ScopedContext.acquire()) {
try (var _ = ScopedContext.acquire()) {
    ... no use of acquired resource ...
}
基本的な for ループのヘッダー
// for (int i = 0, ignore = sideEffect(); i < 10; i++) {
for (int i = 0, _ = sideEffect(); i < 10; i++) {
    ...
}
拡張 for ループのヘッダー
int total = 0;
for (Order _ : orders) // for (Order order : orders)
    total++;
キャッチブロックの例外パラメータ
String s = ...
try {
    int i = Integer.parseInt(s);
    ... i ...
} catch (NumberFormatException _) {  // } catch (NumberFormatException ignore) {
    System.out.println("Bad number: " + s);
}

もちろん無名変数を複数利用することもできます。

try { ... }
catch (Exception _) { ... } // catch (Exception ignore)
catch (Throwable _) { ... } // catch (Throwable ignore)
ラムダ式の正規パラメータ
stream.collect(Collectors.toMap(
     String::toUpperCase,
        _ -> "NODATA")); // ignore -> "NODATA"));


Unnamed pattern variables (無名パターン変数)

型パターンに無名パターン変数を利用することができます。これは型パターンがトップレベルでも、レコードパターンの中にネストされていても利用できます。

以下のような3種の Ball クラスがあった場合

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

パターン変数を省略して以下のように書くことができます。

switch (ball) {                         // パターン変数 `red`、`blue`、`green` は右辺で使われない
  case RedBall _   -> process(ball);    // case RedBall   red   -> process(ball);
  case BlueBall _  -> process(ball);    // case BlueBall  blue  -> process(ball);
  case GreenBall _ -> stopProcessing(); // case GreenBall green -> stopProcessing();
}

任意の Ballnull を保持できる Box レコードがあった場合、

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

このようなネストしたケースにおいてもパターン変数を右辺で使わない場合は、以下のように無名パターン変数を使うことができます(instanceof 演算子の型パターンについても同様です)。

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

加えて、スイッチラベルの文法が、ケースパターンを連続して記述できるよう拡張し、以下のように書くことができるようになりました(この例は、無名パターンによりさらに簡略化できることを後述します)。

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

スイッチラベルの文法は以下のように変更されています。

SwitchLabel:
    case CaseConstant {, CaseConstant}
    case null [, default]
    case CasePattern {, CasePattern } [Guard]    (旧来:case CasePattern [Guard])
    default


Unnamed pattern (無名パターン)

2次元座標と色を持つ ColoredPoint というレコードクラスがあった場合を考えます。

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

record ColoredPoint(Point p, Color c) { }

ColoredPointx 座標だけがほしい場合、以下のようにパターンマッチで抽出することができます。 このような記述は、オブジェクトのネスト構造に応じて複雑になります。

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

無名パターンでは、レコードコンポーネントの型と名前を合わせて _ で宣言することができ、前述は以下のように簡略化できます。

if (r instanceof ColoredPoint(Point(int x, _), _)) {
    System.out.println(x);
}

x y 座標が必要な場合は以下のように書けます。

if (r instanceof ColoredPoint(Point(int x, int y), _)) {
    System.out.println("%d,%d", x, y);
}

Colorコンポーネントのみを抽出する場合は以下のようになります。

if (r instanceof ColoredPoint(_, Color c)) {
    System.out.println(c);
}

深くネストされたクラスの一部分を抽出するケースにおいて、無名パターンは可読性の大きな向上を提供します。