【Java 25 LTS入門】Java 25 LTS移行ガイド:構造化並行性と仮想スレッドで進化するJava 21からの移行戦略

はじめに:なぜ今、最新LTSのJava 25へ移行するべきなのか?

2025年9月、新たな長期サポート(LTS)バージョンである Java 25 がリリースされました。
今回のJava 25は、単なる定期アップデートとは一線を画します。
Java 21でプレビューされた多くの機能が洗練され、開発体験、アプリケーション性能、そしてコードの保守性を向上させる、Java開発の新たな標準となるバージョンです。

特に、Java 21で登場した仮想スレッド(Project Loom)を強力にサポートする 構造化並行性(Structured Concurrency) は、Java 25で5回目のプレビューを迎え、より安定性が向上しています。
これにより、複雑な非同期処理を、より安全かつシンプルに記述できるようになります。
従来コールバックや手動でのスレッド管理で発生しがちだった問題が軽減されるでしょう。

本記事では、既存のJavaシステム(特にJava 17や21)を最新LTSであるJava 25へ安全かつスムーズに移行するための具体的な手順、考慮事項、そしてJava 25がもたらす新機能の活用法を、ITアーキテクトの視点から解説します。

この記事が、あなたのJavaプロジェクトをモダン化するための一助となることを目指します。


目次


対象読者

  • Javaの基本的な知識はあるが、最近のバージョンアップを追えていない開発者
  • Java 21やJava 25のプレビュー機能に興味がある中級以上のJava開発者
  • 今後のJavaプロジェクトで新しい言語機能を活用したいと考えているアーキテクトやチームリーダー

動作検証環境

この記事は、以下の環境で検証しています。

  • OS : macOS Tahoe Version 26.1
  • ハードウェア : MacBook Air 2024 M3 24GB
  • VS Code: 最新版 (記事執筆時点)
  • Java Extension Pack: 最新版 (記事執筆時点)
  • Java: OpenJDK 25.0.1 LTS (Temurin)

1. 移行前の準備:プロジェクトの現状把握とリスク評価

Java 25へのバージョンアップは、新しい機能の恩恵を受ける一方で、潜在的なリスクも伴います。
無計画な移行は、予期せぬコンパイルエラーやランタイムの挙動変化を引き起こす可能性があります。

まずは、あなたのプロジェクトの現状を正確に把握し、リスクを評価することから始めましょう。


1-1. 依存ライブラリの互換性チェック

プロジェクトが依存しているライブラリが、Java 25に対応しているかの確認は、移行作業の最初の、そして最も重要なステップです。

主要フレームワークの対応状況:

  • Spring Boot:
    • Spring Boot 3.3以降 がJava 25を公式にサポートしています。もし古いバージョンを利用している場合は、まずSpring Bootのバージョンアップを計画する必要があります。
  • Quarkus, Micronautなど:
    • 利用している他のフレームワークについても、公式ドキュメントでJava 25への対応状況を必ず確認してください。
  • Maven Central / Gradle Plugins:
    • 各ライブラリのMaven CentralやGradle Plugin Portalで、最新バージョンがJava 25に対応しているかを確認します。
  • jdepsによる依存関係の静的分析:
    • JDKに同梱されている jdeps は、Javaクラスファイルの依存関係を分析する非常に強力な静的分析ツールです。
    • 特に、Java 9以降で強化されたモジュールシステムの恩恵を受け、バージョンアップ時に問題となりがちな 内部APIへの依存 や、意図しないライブラリの混入を特定するのに不可欠です。

1-2. jdeps:よく使うコマンド例

以下に、移行準備やトラブルシューティングで役立つ、シナリオ別の jdeps コマンド例と、その実行結果例を紹介します。

シナリオ1: 依存関係の全体像を素早く把握する

依存しているパッケージとJDKモジュールの概要を素早く確認したい場合に使用します。

jdeps -summary your-app.jar

実行結果例

your-app.jar -> java.base
your-app.jar -> java.lang
your-app.jar -> org.springframework.boot
your-app.jar -> org.springframework.web.bind.annotation
...

シナリオ2: 内部APIへの依存を洗い出す(移行時の最重要チェック)

Javaのバージョンアップで最も問題になりやすいのが、JDKの内部APIへの依存です。--jdk-internals オプションは、これらの危険な依存を検出します。

# --jdk-internals は -jdkinternals と短縮できます
jdeps --jdk-internals your-app.jar

実行結果例(問題あり)

JDK Internal API                         Suggested Replacement
----------------                         -------------------
sun.misc.Unsafe                          java.lang.invoke.VarHandle

WARNING: A command line option has enabled access to unsupported API
Use --illegal-access=warn to enable warnings of further illegal reflective access

your-app.jar -> JDK internal API
   com.example.MyUnsafeClass  -> sun.misc.Unsafe  JDK internal API

解説: このような出力が得られた場合、com.example.MyUnsafeClass が内部APIである sun.misc.Unsafe を使用していることがわかります。Javaのバージョンによっては動作しなくなる可能性が非常に高いため、java.lang.invoke.VarHandle などの公式APIへの置き換えが必須です。

実行結果例(問題なし)
コマンド実行後、上記のような警告や依存関係が何も出力されなければ、内部APIへの直接的な依存はなく、クリーンな状態です。


シナリオ3: すべての推移的依存関係を分析する

アプリケーションが直接依存しているライブラリだけでなく、そのライブラリがさらに依存しているライブラリ(推移的依存)まで含めて、すべての依存関係を再帰的に分析します。問題の根本原因を探るのに役立ちます。

jdeps -recursive your-app.jar

シナリオ4: 依存関係グラフを可視化する

複雑な依存関係をテキストで追うのが難しい場合、Graphviz(別途インストールが必要)と連携して依存関係グラフを画像として出力すると、全体像を一目で把握できます。

# 1. 依存関係をDOT形式で出力
jdeps -dotoutput dot-files your-app.jar

# 2. dotコマンドでPNG画像に変換
dot -Tpng -o dependencies.png dot-files/your-app.jar.dot

生成された dependencies.png を見ることで、どのライブラリがどのモジュールに依存しているかを視覚的に理解できます。


1-3. ビルドツールの対応状況確認(Maven, Gradleなど)

MavenやGradleといったビルドツールと、そのプラグインもJava 25に対応している必要があります。

  • Maven:
    • maven-compiler-plugin はバージョン 3.13.0以降 がJava 25をサポートしています。pom.xml<release>25</release> または <java.version>25</java.version> を設定します。
    • maven-surefire-plugin など、テスト実行やパッケージングに関わる他の主要プラグインも最新バージョンに更新しましょう。
  • Gradle:
    • Gradleはバージョン 8.8以降 がJava 25をサポートしています。
    • build.gradle または build.gradle.kts で、sourceCompatibilitytargetCompatibilityJavaVersion.VERSION_25 に設定します。

1-4. JDKのインストールと環境設定

移行作業を開始する前に、Java 25のJDKをインストールし、開発環境を適切に設定しておく必要があります。

  • SDKMAN! / Homebrew: 複数のJDKバージョンを簡単に切り替えて管理するために、SDKMAN!(Linux/macOS)やHomebrew(macOS)のようなツールを利用することを強くお勧めします。
# SDKMAN! を使ったJava 25のインストール例
sdk install java 25.0.1-tem
sdk use java 25.0.1-tem
  • IDEの設定: IntelliJ IDEAやVS CodeなどのIDEで、プロジェクトが使用するJDKのバージョンをJava 25に切り替えます。

2. Java 25の主要新機能ハイライト

Java 25には、開発者の生産性を向上させる多くの強力な機能が搭載されています。ここでは、特に注目すべき3つの機能を紹介します。


2-1. 構造化並行性 (Structured Concurrency) – 5回目のプレビュー

Java 21からプレビューが続く構造化並行性は、Java 25で 5回目のプレビュー として提供されます(JEP 505)。これは、Javaの並行処理を大きく前進させる機能であり、今回のプレビューでAPIの設計に大きな変更が加えられました。

何が解決されるのか?

複数の非同期タスクを扱う際のエラーハンドリングとライフサイクル管理を大幅に簡素化します。タスクがリークしたり、エラー発生時に他のタスクがキャンセルされずに実行され続けたりといった、並行処理で発生しがちな問題を解決することを目的としています。

JDK 25での主要な変更点

JDK 25では、APIの責務がより明確になるように設計が見直されました。

  • StructuredTaskScope: サブタスクのフォーク(fork())とライフサイクル管理に専念します。
  • Joiner: スコープのポリシー(成功・失敗条件)を定義する新しいインターフェースです。

これにより、スコープの生成方法が new を使ったコンストラクタ呼び出しから、Joiner を引数に取る静的ファクトリメソッド open() に変更されました。

コード例:StructuredTaskScopeJoiner

StructuredTaskScope.open() を使うと、複数のタスクを1つのスコープで管理できます。以下の例では、「すべてのタスクが成功した場合のみ成功」というポリシー (Joiner.allSuccessfulOrThrow()) を適用しています。

// --enable-preview が必要
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

// 【成功ケース】
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.allSuccessfulOrThrow())) {
    Subtask<String> user = scope.fork(() -> findUser(true));
    Subtask<Integer> order = scope.fork(() -> fetchOrder(true));

    // スコープ内の全タスクが完了するまで待機
    scope.join();

    // 両方のタスクが成功した場合のみ、結果を処理
    System.out.println("User: " + user.get() + ", Order: " + order.get());
}

// 【失敗ケース】
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.allSuccessfulOrThrow())) {
    Subtask<String> user = scope.fork(() -> findUser(true));
    // fetchOrderで失敗をシミュレート
    Subtask<Integer> order = scope.fork(() -> fetchOrder(false));

    // fetchOrderの失敗により、ここでFailedExceptionがスローされる
    // Joinerポリシーにより、findUserタスクは自動的にキャンセルされる
    scope.join(); 

    // この行は実行されない
    System.out.println("User: " + user.get() + ", Order: " + order.get());

} catch (StructuredTaskScope.FailedException e) {
    // エラーハンドリング:いずれかのタスクが失敗すると、もう一方は自動的にキャンセルされる
    System.err.println("Operation failed: " + e.getCause().getMessage());
}

// --- スタブメソッド ---
private static String findUser(boolean success) throws InterruptedException {
    System.out.println("Finding user...");
    Thread.sleep(Duration.ofMillis(100));
    if (!success) {
        throw new RuntimeException("Failed to find user");
    }
    System.out.println("Found user.");
    return "John Doe";
}

private static Integer fetchOrder(boolean success) throws InterruptedException {
    System.out.println("Fetching order...");
    Thread.sleep(Duration.ofMillis(200));
    if (!success) {
        throw new RuntimeException("Failed to fetch order");
    }
    System.out.println("Fetched order.");
    return 12345;
}

ポイント:例外処理の変更

JDK 25のプレビュー版では、scope.throwIfFailed() メソッドが不要になりました。
scope.join() を呼び出した際に Joiner ポリシーに違反した場合(例えば、allSuccessfulOrThrow で1つでもタスクが失敗した場合)、join() メソッド自体が StructuredTaskScope.FailedException をスローします。これにより、例外処理の記述がよりシンプルになりました。

※注意: これはまだプレビュー機能のため、製品利用にはコンパイル時と実行時の両方で --enable-preview フラグが必要であり、将来のバージョンでAPIが変更される可能性がある点に注意してください。


2-2. インスタンスmainメソッド – よりシンプルなプログラム起動

Java 25では、プログラムのエントリーポイントをより直感的に記述できるようになりました(JEP 445, JEP 512)。これにより、特にJava初学者にとっての学習障壁が大きく下がります。

何が変わったのか?
従来の public static void main(String[] args) というお決まりのフレーズが不要になり、インスタンスメソッドを main メソッドとして宣言できます。

コード例:シンプルな “Hello, World!”

// コンパイル: javac HelloWorld.java --release 25
// 実行: java HelloWorld
void main() {
    System.out.println("Hello, Java 25!");
}

クラスを宣言する必要すらなく、トップレベルに main メソッドを記述するだけでプログラムを実行できます。


2-3. プリミティブ型のパターンマッチング (第3プレビュー)

Java 21で強化されたパターンマッチングが、Java 25ではプリミティブ型にも拡張されました(JEP 507, Third Preview)。これはまだプレビュー機能ですが、将来のJavaの記述性をさらに高めるものです。

何が便利になるのか?
switch 式で intdouble などのプリミティブ型を直接パターンとして扱えるようになり、より柔軟で直感的な分岐処理が可能になります。

コード例:プリミティブ型での switch

// --enable-preview が必要
String checkType(Object obj) {
    return switch (obj) {
        case int i -> "It's an int: " + i;
        case long l -> "It's a long: " + l;
        case double d -> "It's a double: " + d;
        case String s -> "It's a String: " + s;
        default -> "Unknown type";
    };
}

これにより、instanceof を使った型チェックとキャストの連鎖を、よりクリーンな switch 式で表現できます。


3. Java 21から25への具体的な移行ステップ

準備が整ったら、いよいよ移行作業を開始します。ここでは、段階的かつ安全に進めるための具体的なステップを解説します。


3-1. コンパイルとテストの実行

まず、コードを一切変更せずに、Java 25でプロジェクトをコンパイルし、既存の単体テスト、結合テストをすべて実行します。

# Mavenの場合
mvn clean install

# Gradleの場合
./gradlew clean build

この段階で、非互換なAPIの利用や、削除された機能に起因するコンパイルエラーが明らかになります。テストを実行することで、実行時の挙動変化やリグレッションを早期に検出できます。


3-2. 主要な非互換変更点への対応

Java 21から25の間にも、いくつかの非互換変更点が存在します。コンパイルエラーや警告メッセージを注意深く読み、以下のような代表的な変更点に対応します。

  • 削除されたAPI:
    • 長年非推奨だったAPIが削除されている可能性があります。jdepsの分析結果やコンパイルエラーを元に、公式ドキュメントで代替手段を確認し、コードを修正します。
  • 内部APIの制限強化:
    • sun.misc.Unsafe に代表される内部APIへのアクセスは、さらに厳しく制限されています。これらのAPIに依存している場合は、公式にサポートされている代替APIへの移行が必須です。

3-3. 既存コードのリファクタリング推奨

コンパイルとテストが通ったら、次はJava 25の機能を活用するためのリファクタリングを検討しましょう。

  • synchronizedブロックの見直し:
    • Java 24以降、synchronizedキーワードによる仮想スレッドのピン留めは ほぼ解消 されました。
    • そのため、パフォーマンスのためだけに既存の synchronized ブロックを ReentrantLock に書き換える必要性は低くなりました。
    • ただし、ネイティブメソッド(JNI)を呼び出す箇所は依然としてピン留めの原因となるため、注意が必要です。
  • パターンマッチングの全面的な活用:
    • 複雑な if-else チェーンや従来の switch 文は、Java 25のパターンマッチングで置き換えることを検討します。
    • case null による安全なnullチェックや、when 句によるガード付きパターンを活用することで、コードの簡潔性と堅牢性が向上します。

リファクタリング例:

// 修正前: 古いスタイルの型チェック
if (obj instanceof String) {
    String s = (String) obj;
    if (s.length() > 5) {
        System.out.println("Long string: " + s);
    }
}

// 修正後: ガード付きパターンで簡潔に
if (obj instanceof String s when s.length() > 5) {
    System.out.println("Long string: " + s);
}
  • 構造化並行性の活用:
    • ExecutorService を使った複雑な非同期処理は、StructuredTaskScope を使って書き換えることを検討する良い機会です。
    • これにより、エラーハンドリングとリソース管理の信頼性が向上します。前述の「主要新機能ハイライト」のコード例を参考に、非同期処理のコードを更新することを推奨します。

4. 移行時のトラブルシューティングとよくある落とし穴

移行作業中に遭遇しやすいトラブルとその解決策、そして避けるべき落とし穴について解説します。

  • コンパイルエラー:
    • 最も一般的な問題です。削除されたAPIや内部APIの利用、言語レベルの変更などが原因で発生します。エラーメッセージを注意深く読み、公式ドキュメントを参照して対応策を講じましょう。
  • ランタイムエラー:
    • NoClassDefFoundErrorNoSuchMethodError などは、依存ライブラリのバージョンミスマッチや、Java 25で削除されたモジュールを参照している場合に発生しやすいです。
    • ビルドツールの依存関係解決機能(mvn dependency:treegradle dependencies)を活用して、依存関係ツリーをクリーンに保ちましょう。
  • パフォーマンス低下:
    • 仮想スレッドの不適切な利用(特にネイティブメソッド呼び出しによるピン留め)や、古いGC設定のまま新しいJavaバージョンを使用している場合に発生することがあります。
    • JFR (Java Flight Recorder) を使ってパフォーマンスを分析し、ボトルネックを特定します。
  • テストの重要性:
    • 移行作業中は、既存の単体テスト、結合テスト、システムテストを徹底的に実行することが不可欠です。
    • これにより、予期せぬ副作用やリグレッションを早期に発見できます。

5. 移行後のパフォーマンス検証と最適化

移行が完了したら、Java 25環境でのアプリケーションのパフォーマンスを検証し、必要に応じて最適化を行います。

ベンチマークテスト:

  • 移行前と移行後で、主要なビジネスロジックやAPIエンドポイントのパフォーマンスを比較するベンチマークテストを実施します。

プロファイリングによる深掘り:

  • JFR (Java Flight Recorder): JDKに標準で付属するJFRは、Java 25環境の分析に最適なツールです。特に、仮想スレッドのピン留めを監視する jdk.VirtualThreadPinned イベントは必ず確認しましょう。
# JFRを有効にしてアプリケーションを実行
java -XX:StartFlightRecording=filename=recording.jfr,duration=60s -jar your-app.jar

# JFRファイルからピン留めイベントを確認
jfr print --events jdk.VirtualThreadPinned recording.jfr
  • VisualVMなど: VisualVMやYourKitといったプロファイリングツールも、CPU使用率、メモリ使用量、GC活動、スレッドの状態を詳細に分析するのに役立ちます。

GCチューニング:

  • Java 25では、Generational ZGC の利用が推奨されます。非常に低いレイテンシを維持しつつ、スループットも向上させます。以下のオプションで有効化できます。
-XX:+UseZGC -XX:+ZGenerational
  • Generational ZGCは自己チューニング機能が非常に強力なため、多くの場合、-Xmx(最大ヒープサイズ)を適切に設定するだけで十分なパフォーマンスが得られます。過度なチューニングは避け、まずはデフォルトの挙動を観察することから始めましょう。

まとめ:Java 25と共に、Java開発の新たなステージへ

Java 25は、単なるLTSの更新ではありません。
プレビューが重ねられてきた構造化並行性をはじめとする数々の機能強化により、Javaは、現代的なアプリケーションを開発するための、より安全で、生産性の高いプラットフォームへと大きく進化しました。

このバージョンへの移行は、技術的な負債を返済するだけでなく、アプリケーションをよりモダンで保守性の高いものへと進化させるための戦略的な判断と言えるでしょう。
仮想スレッドのポテンシャルを活用し、クリーンで堅牢なコードを、より容易に記述できるようになりました。

本記事で解説した準備、移行ステップ、そして最適化のヒントを参考に、あなたのプロジェクトを最新LTSであるJava 25へとアップデートしてください。

Javaエコシステムは、次期LTSであるJava 29を見据え、これからも進化を続けます。本記事で解説した内容が、進化を続けるJavaエコシステムを活用し、あなたのプロジェクトを成功に導く一助となれば幸いです。


免責事項

この記事で紹介しているJava 25の機能は、プレビュー版またはインキュベーター版に基づいています。正式リリース時には仕様が変更される可能性があります。本記事の情報は、2025年12月時点のものであり、その正確性や完全性を保証するものではありません。


SNSでもご購読できます。

コメントを残す

*