未来を形作るテクノロジーの深掘り記事。

Javaは進化し続けているのに誰も気づかない

Javaの半年ごとのリリースが言語を静かに変えた。パターンマッチング、仮想スレッド、レコードにより、もはや別物と言えるほどになった。

薄暗い古いオフィスが明るいモダンな空間に変わる中、輝くコーヒーカップが置かれている

Javaはプログラミング言語界のロドニー・ダンジャーフィールドだ。とにかく尊敬されない。30歳未満の多くの開発者にJavaの話をすると、XMLの設定ファイル、エンタープライズBean、AbstractSingletonProxyFactoryBeanといったものを思い浮かべるだろう。この言語の評判は2008年で止まっている。当時の最新版はJava 6で、Javaを書くことは定型コードに溺れることを意味していた。

ところが2017年にJavaが半年ごとのリリースサイクルに移行して以来、注目すべきことが起きている。Java 6、7、8の間の4年ごとのブランクの頃には想像もできなかったペースで、この言語は進化しているのだ。レコード。シールドクラス。パターンマッチング。仮想スレッド。文字列テンプレート。構造化並行性。どのリリースも、Javaのコードを短く、表現力豊かに、そして書いていて楽しくする機能を追加している。Java 26もこの流れを続けている。「今のJava」と「人々が想像しているJava」のギャップは、これまでになく広がっている。

変化をもたらした機能たち

Java 8以降Javaを書いていないなら、この10年分の改善を見逃していることになる。現在の言語の姿を紹介しよう。

レコード(Java 14)は不変のデータクラスだ。かつてはフィールド、コンストラクタ、ゲッター、equals、hashCode、toStringといった50行の定型コードが必要だったが、今では1行で済む。

// Java 8: 50 lines of boilerplate
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
// Java 14+: one line
record Point(int x, int y) {}

パターンマッチング(Java 16〜21、現在も進化中)では、switch式やinstanceofチェックでオブジェクトを分解できる。これにより、Javaコードを悩ませてきたif-else-instanceofの連鎖が解消される。

// Pattern matching with sealed types and records
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive pattern matching — compiler verifies all cases covered
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}

仮想スレッド(Java 21)は、OSではなくJVMが管理する軽量スレッドだ。OSのスレッドハンドルやメモリを使い果たすことなく、何百万個でも作成できる。これにより、最もシンプルな並行処理モデルである「リクエストごとに1スレッド」が大規模環境でも現実的になった。

// Create a million concurrent tasks — try this with OS threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
// Each task gets its own virtual thread
// Blocking I/O (HTTP, DB, file) automatically yields
var response = httpClient.send(request, BodyHandlers.ofString());
return response.body();
});
});
}
// All done — executor auto-closes, waits for completion

仮想スレッドは、GoのgoroutineやKotlinのコルーチンに対するJavaの答えだ。違いは、特別な構文が要らず、関数に色を付ける必要もないことだ。通常のブロッキングコードがそのまま動く。JVMがブロッキングポイントで仮想スレッドを透過的に中断・再開する。既存のブロッキングHTTPクライアント、JDBCドライバ、ファイルI/Oのコードも、修正なしで恩恵を受けられる。

なぜ誰も気づかないのか

Javaの改善の軌跡は、間違いなく目覚ましい。それなのに、なぜ世間のイメージは2008年で止まったままなのか。

第一に、エンタープライズの慣性だ。多くの組織は、大規模なコードベースのアップグレードにコストとリスクがかかるため、Java 8や11を使い続けている。開発者が「Java」と言うとき、それは最新リリースではなく、自社が使っているバージョンのことを指している。Java 8の開発者は、レコード、パターンマッチング、仮想スレッドを経験していない。

第二に、フレームワーク層だ。Spring Boot、Jakarta EE、その他のエンタープライズ向けフレームワークは、言語の上に独自の複雑さを積み上げる。開発者はSpringのアノテーションだらけの儀式を「Java」のせいにするが、それはフレームワークの問題であって言語の問題ではない。重いフレームワークを使わないモダンなJavaは、驚くほどすっきりしている。

第三に、文化的な惰性だ。テック業界では、言語にも「部族」が割り当てられる。Javaは「エンタープライズ」の言語とされ、大企業、官僚的なプロセス、過剰設計と結び付けられている。この評判は当然のものだった。2005年頃のJava開発は、批判されて当然だった。しかし評判は、その原因が消えてからも何年も残るものだ。

JVMの優位性

言語の改善ももちろん重要だが、Javaの最大の強みは依然としてJVMにある。30年にわたる最適化を経て、HotSpot JVMは、これまでに作られた中でも最も洗練されたランタイム環境の一つになった。

  • JITコンパイル。JVMは実行中のコードをプロファイルし、ホットなメソッドを最適化されたネイティブコードにコンパイルする。長時間稼働するアプリケーション(サーバー、データ処理)では、JITが静的解析だけでなく実際のランタイムの振る舞いに合わせて最適化できるため、JVMの性能はC++に匹敵する、あるいは上回ることも多い。
  • ガベージコレクション。ZGCやShenandoahといった最新のGC実装は、テラバイト級のヒープでもサブミリ秒の停止時間を実現する。「GCの停止がレイテンシを殺す」という批判は、もう何年も前から当てはまらない。これらのコレクタはアプリケーションと並行して動作するからだ。
  • オブザーバビリティ。JFR(Java Flight Recorder)は、オーバーヘッドがほぼゼロで本番環境でも安全にプロファイリングできる。CPU使用率、メモリ割り当て、ロック競合、I/Oレイテンシ、GCの振る舞いを、アプリケーションを遅くすることなくプロファイルできる。
  • エコシステムの成熟度。Maven Centralには数百万のパッケージがある。ビルドツール(Gradle、Maven)は十分に理解されている。テストのエコシステム(JUnit、Mockito、Testcontainers)も優れている。デプロイの面でも(コンテナ、GraalVM native image)大きく成熟した。

GraalVMとネイティブイメージ

Javaの最大の弱点は、昔から起動時間だ。JVMは、初期化、クラスのロード、JITコンパイラのウォームアップに数百ミリ秒かかる。サーバーレス関数やCLIツールにとって、このオーバーヘッドは受け入れがたい。

GraalVM Native Imageは、Javaアプリケーションを事前(AOT)コンパイルし、単体で動く実行ファイルにすることでこの問題に対処する。JVMは不要だ。起動時間は数百ミリ秒から一桁ミリ秒に短縮される。インタプリタもJITコンパイラもクラスメタデータも読み込まないため、メモリ使用量も減る。

トレードオフもある。ネイティブイメージはJVMの適応的最適化を失う。JITコンパイルされたアプリケーションは、JVMがホットパスを学習するにつれて時間とともに速くなる。ネイティブイメージは起動が速いが、その後は良くならない。短命なプロセス(CLIツール、サーバーレス)ではネイティブイメージが勝つ。長時間稼働するサーバーでは、最終的にJVMのJITの方が速いコードを生成する。実行時の特性に合ったツールを選ぼう。

最新のJavaと他の選択肢

Javaを「置き換えた」とされる言語、Go、Kotlin、Rustは、いずれも優れている。しかし、最新のJavaは人々が思う以上に競争力がある。

Goのシンプルさは魅力的だが、Javaには今や仮想スレッド(goroutineに匹敵)があり、はるかに豊かな型システムもある。大規模なコードベースでは型システムが重要になる。特にシールドタイプとパターンマッチングを備えたJavaの型システムは、Goのインターフェースの仕組みでは見逃されるエラーを捕捉する。

Kotlinは、Javaの構文を改善しているが、同じJVM上で動く。Java 8に対するKotlinの利点(null安全性、データクラス、コルーチン)の多くは、モダンなJavaにも部分的または完全な代替がある(Optionalのパターン、レコード、仮想スレッド)。Kotlinの方が依然として簡潔だが、その差は大きく縮まった。

Rustはガベージコレクタのないシステムプログラミングという、別のニッチを担っている。Javaがそこで競う必要はない。しかしサーバーサイドアプリケーション、データ処理、エンタープライズソフトウェアにおいては、Javaの性能、安全性、エコシステムの成熟度の組み合わせは、なかなか超えられるものではない。

真のイノベーションは半年ごとのリリースサイクル

どの単一機能よりも、半年ごとのリリースサイクルがJavaの軌道を変えた。2017年以前、Javaのリリースは3〜4年に一度のイベントで、すべてを一度に出荷しようとしていた。準備ができていない機能は遅れ、それによってリリース全体が遅れ、次のリリースにすべてを詰め込むプレッシャーが生まれ、そのリリースもまた遅れた。Java 9が3年以上かかったのは、モジュールシステムの複雑さも一因だった。

半年ごとのリリースでは、機能は準備ができ次第出荷される。パターンマッチングは、Java 16から24にかけて段階的に提供され、リリースごとに一つずつ要素が追加された。仮想スレッドは、正式版の前に二度のプレビューリリースを経ている。この段階的なアプローチにより、Javaチームは永続的なAPIの決定を下す前に、実世界のフィードバックを集め、設計を調整できる。

この教訓はJavaの枠を超えて当てはまる。小さく頻繁なリリースは、積み重なって劇的な変化になる。Java 17以降の個々のリリースは、どれも控えめなものだった。しかし全体として見ると、言語を再発明してしまった。5年前にJavaを試して冗長さに嫌気がさしたなら、もう一度試してみてほしい。きっと驚くはずだ。