Javaの変数宣言は1995年から進化していなかった ― JEP 8357464が変えること

Javaの変数宣言は1995年から進化していなかった ― JEP 8357464が変えること プログラミング

Javaのローカル変数宣言が、パターンマッチングと統合される日が近づいています。

JEP 8357464「Enhanced Local Variable Declarations (Preview)」が公開されました。
これは、レコードパターンをinstanceofやswitchの外でも使えるようにする提案です。

Redditでは、Javaの言語設計者Brian Goetz氏を含む開発者たちが活発な議論を展開しました。
本記事では、このJEPの核心と、コミュニティで交わされた興味深い論点を整理していきます。

そもそも何が変わるのか

現在のJavaでは、レコードの中身を取り出すとき、アクセサメソッドを一つずつ呼び出す必要があります。

var circle = getCircle();
var center = circle.center();
var x = center.x();
var y = center.y();
var radius = circle.radius();

nullチェックが加わると、さらにネストが深くなっていきます。

void boundingBox(Circle c) {
    if (c != null) {
        Point ctr = c.center();
        if (ctr != null) {
            int x = ctr.x(), y = ctr.y();
            double radius = c.radius();
            // ... 本来の処理
        }
    }
}

このJEPが導入されると、レコードパターンをローカル変数宣言の左辺に直接書けるようになります。

void boundingBox(Circle c) {
    Circle(Point(int x, int y), double radius) = c;
    int minX = (int) Math.floor(x - radius);
    int maxX = (int) Math.ceil(x + radius);
    // ...
}

1行で複数の値を同時に取り出せるわけです。
ネストした構造も、宣言的に分解できます。

ローカル変数宣言とパターンマッチングの統合

このJEPで最も興味深いのは、表面的な構文糖衣(シンタックスシュガー)にとどまらない設計思想です。

Brian Goetz氏がRedditで興味深い指摘をしていました。
従来のローカル変数宣言String s = findMeAString();を見てください。

左辺のString sは、型パターンのString sと構文が同じです。
これは偶然ではありません。

このJEPによって、代入の左辺にパターンを置けるようになります。
すると、ある種の曖昧さが生まれるのです。
「これはローカル変数宣言なのか?それともパターンマッチングなのか?」という疑問です。

しかし、Goetz氏の答えは明快でした。
どちらで解釈しても同じ結果になる、と。
つまり、1995年以来のローカル変数宣言が、パターンマッチングと統合されたのです。

この統合は、後述するパターン変数のfinalデフォルト問題にも関わってきます。

sealed interfaceの型緩和がもたらす安全性

JEPにはもう一つ、見逃しがちな重要な変更が含まれています。
sealed interfaceの実装クラスが1つだけの場合に関する変更です。

具体的には、インターフェース型の変数を、その実装クラス型の変数に直接代入できるようになります。
つまり、型システムの緩和です。

一見すると、型チェックが甘くなるように感じるかもしれません。
Reddit上でも「コードレビューで気づきにくくなる」という懸念の声がありました。

しかし、Goetz氏の説明は逆の結論を示しています。

従来のコードを考えてみましょう。
sealedインターフェースの実装が1つだけだと分かっていても、実装クラスへの変換にはキャストが必要でした。

このキャストはコード中のあちこちに散らばります。
そして、誰かが2つ目の実装クラスを追加したとき、それらのキャストはすべて実行時のClassCastExceptionとなって爆発するのです。

一方、代入を許容すればどうなるか。
コンパイラがその前提を検証してくれます。

2つ目の実装クラスが追加された瞬間にコンパイルエラーとなるため、修正箇所をすぐに特定できるのです。
「緩和」が「より強い型チェック」をもたらすという、直感に反する結果がここにあります。

Goetz氏はさらに補足しています。
exhaustiveなswitch文でdefault句を書かないほうが良い理由と同じだ、と。
前提が崩れたときにコンパイラが教えてくれる仕組みのほうが、実行時エラーに頼るよりもはるかに安全でしょう。

パターン変数をfinalにしない理由

Redditでは「パターンで束縛された変数は、デフォルトでfinalにすべきでは?」という意見も出ていました。
イミュータビリティを推奨する流れから考えれば、自然な発想に思えます。

しかし、Goetz氏はこの「明白に見える」設計を採用しない理由を明かしています。

ポイントは、ローカル変数宣言とパターンの束縛の統合にあります。
パターン変数だけを暗黙的にfinalにすると、変数宣言のセマンティクスが2種類存在することになるのです。
Amberプロジェクトは、こうした不整合を意図的に避けています。

初期には満足感のある設計判断に見えても、将来の統合を妨げる障壁になりかねません。
長期的な一貫性を優先した判断と言えるでしょう。

Kotlinとの比較から見えるJavaの設計哲学

Reddit上では、Kotlinのデストラクチャリングとの比較も活発でした。

Kotlinではval (address, payment, totalAmount) = orderのように位置ベースで分解します。
内部的には、componentN()メソッドの呼び出しに変換される仕組みです。

しかし、Kotlinコミュニティ自身がこのアプローチの問題を認識しています。
そのため、名前ベースのデストラクチャリングへの移行が議論されています(KEEP-0438)。

一方、Javaのレコードパターンは型駆動で構造的なアプローチを取ります。
コンパイラは、レコードのクラスファイルに格納されたメタデータからコンポーネントの構造を把握します。

そして、パターンの整合性を静的に検証するのです。
レコードの構造が変われば、パターンはコンパイルエラーになります。
暗黙的に間違ったフィールドが束縛される事態は起きません。

あるコメントでは、Kotlinの問題の根本が指摘されていました。
「デコンストラクタ」が複数のメソッド(component1()、component2()…)に分散していることが原因だ、と。

Javaでは各型に対して1つの正規のデコンストラクタが存在します。
そのため、Kotlinで起きた問題はそのまま当てはまりません。

なお、Javaのパターンでは変数名がレコードコンポーネント名と一致する必要はありません。
次のように、自由な名前を付けられます。

record Circle(double radius, double area) {}

// rとaはradiusやareaと一致しなくてよい
Circle(var r, var a) = circle;

リネーミングの仕組みが最初から組み込まれている点も、Kotlinとの重要な違いです。

「実務で使えるのか」という問い

Redditで最も議論が白熱したのは、「この機能は実際のビジネスコードで役立つのか」という問いでした。

「普通のCRUDアプリケーションを書いている自分には関係ない」と疑問を呈する開発者がいました。
これに対して、複数の開発者が具体例を挙げて応答しています。

たとえば、決済システムにおけるsealed型の活用例です。
銀行口座、クレジットカード、暗号通貨ウォレットなど、異なる種類の資金保持者をsealed interfaceで表現します。
そして、パターンマッチングで処理を分岐させるのです。

sealed interface Order {
    record CustomerOrder(String email, boolean vip) implements Order {}
    record BusinessOrder(String email, byte[] logo) implements Order {}
    enum TestOrder implements Order { INSTANCE }
}

String salutation = switch (order) {
    case CustomerOrder(_, false) -> "Dear customer";
    case CustomerOrder(_, true) -> "Dear valued customer";
    case BusinessOrder(_, _) -> "Dear sir or madam";
    case TestOrder _ -> "it worked";
};

exhaustive switchの力により、新しいOrder型を追加すると、switch文の更新漏れはコンパイルエラーとして検出されます。
if-elseチェーンやenum比較では得られない安全性です。

一方で、「JEPの例は短いクラス名と2〜3個のフィールドに最適化されている」という指摘もありました。
実際のエンタープライズコードでは、コンポーネントがもっと多いケースも珍しくありません。

こうした場合は、段階的に分解する書き方が推奨されています。

CustomerOrder(ShippingAddress address, PaymentMethod payment, double totalAmount) = order;
ShippingAddress(String streetLine1, String streetLine2, String city) = address;
PaymentMethod(String cardNumber, int expiryYear) = payment;

1行で全階層をネストする必要はありません。
読みやすさを優先して段階的に書けます。

構文への慣れと「見た目が変」という反応

新しい構文に対して「見た目が奇妙だ」「clunkyだ」という反応も多く見られました。

これに対するGoetz氏の返答は短く鋭いものです。
「”見た目が変”というのは、たいてい慣れの問題を言い換えただけだ」と。
そして、慣れの問題は構文に触れる回数が増えるにつれて消えていく、と述べています。

実際、Javaの歴史を振り返ってみましょう。
ジェネリクスの導入時にも、ラムダ式の導入時にも、同様の反応がありました。
あるコメントはこの点を指摘しています。

enhanced-forやtry-with-resourcesに対する違和感も、今となっては想像しにくいでしょう。
半年リリースサイクルが定着し、Java 8からの移行が進んだ今、新構文への適応はかつてよりも速く進むはずです。

今後の展望

このJEPは現時点ではdraft段階です。
しかし、Javaのパターンマッチング全体の中で重要な位置を占めています。

将来的には、null制約型(JEP 8303099)との組み合わせが期待されます。
パターンマッチング内のnullチェックが、コンパイラレベルで検証される可能性があるのです。

また、Valhalla(値型)の実現も注目に値します。
ネストされたオブジェクト構造の分解を、ランタイムコストなしで行えるようになるためです。
そうなれば、このパターンマッチングスタイルの価値はさらに高まるでしょう。

enhanced forループとの組み合わせも期待されているポイントです。
将来的には、Mapのエントリを直接分解する記法が実現するかもしれません。

// 将来的に実現するかもしれない記法
for (Entry(var key, var value) : myMap.entrySet()) {
    // key, valueを直接使用
}

まとめ

JEP 8357464は、単にボイラープレートを減らすだけの機能ではありません。

ローカル変数宣言とパターンマッチングの統合。
sealed型の型緩和による安全性の向上。
これらを通じて、Javaの型システムの一貫性を深める変更です。

「見た目が変」「CRUDには不要」「Kotlinのほうが先にやっている」。
こうした反応は、新機能の登場時に毎回繰り返されてきたものでしょう。
しかし、コミュニティの議論を通じて、この機能が目指す設計哲学は明確に伝わってきました。

短期的な便利さよりも長期的な一貫性を。
表面的な緩和よりも構造的な安全性を。
Javaの進化はゆっくりに見えて、着実に正しい方向へ進んでいるのかもしれません。

タイトルとURLをコピーしました