Java 17 新機能まとめ

公開日: 2025-10-01 21:00 更新日: 2026-08-01 21:00 10225文字 52 min read

issyuu avatar

issyuu

静かな湖面、揺れる湖心小築(Lakeheart Retreat)、静かな水辺で紡ぐ思考と日常の歌。

Java 17(LTS)の 14 個の JEP を、そのまま実行できるサンプルコードと実行結果で解説する。sealed クラス・switch のパターンマッチング・新しい乱数 API・強いカプセル化を中心に、Java 11 から移行するときに押さえておきたい Java 12〜16 の機能と、各機能の JDK 26 までの「その後」も整理した。

はじめに

Java 17 は 2021 年 9 月にリリースされた LTS(長期サポート)版である。Java 11 の次の LTS で、Spring Boot 3 が最低要件にしたこともあり、今でも多くのプロジェクトの基準になっている。一方で Oracle の Premier Support は 2026 年 9 月に終わるので、「17 で止まっているプロジェクトを棚卸しする」にはちょうどよい時期でもある。

本記事では、Java 17 の 14 個の JEP を 実際に動かしながら 見ていく。サンプルコードはすべて 1 ファイルで完結しており、外部ライブラリも不要である。コピーしてそのまま java コマンドで実行できる。

Java 17 の JEP には、削除や非推奨のような「地味な」ものも多い。しかし、Java 11 から移行する場合は、Java 12〜16 で正式化された record やテキストブロックなども一緒に手に入る。そこで、本題の前にそれらも簡単に振り返っておく。また各機能の最後に 「その後」 として、JDK 26(2026 年 3 月 GA)までの動きを補足した。

検証環境とサンプルの実行方法

  • JDK:OpenJDK 17(17.0.20)
  • OS / CPU:macOS(Apple Silicon、8 コア)
  • 実行はソースファイルを直接起動する方式(JEP 330、Java 11 から)。コンパイルは不要である。
java SealedClassesDemo.java                                 # 正式機能
java --enable-preview --source 17 SwitchPatternDemo.java    # プレビュー機能
  • サンプルコードは GitHub の smile-lxy/java-new-features にもまとめてある。コメントは日本語・中国語・英語の 3 言語で書いてあり、本記事には日本語のコメントだけを載せた。
  • 各サンプルの先頭コメント(// Run:)に実行コマンドを書いてある。
  • 出力中の時間やハッシュ値(@6b1274d2 など)は、実行環境や実行のたびに変わる。

全体像:Java 17 の 14 個の JEP

JEP機能Java 17 での状態その後(〜JDK 26)
306厳密な浮動小数点演算の復活正式―
356擬似乱数生成器の刷新正式JDK 19 で Random.from() を追加
382macOS の新しい描画パイプライン追加(既定は OpenGL)JDK 19 で Metal が既定に
391macOS/AArch64 ポート正式―
398Applet API の削除予定化削除予定JDK 26 で削除
403JDK 内部の強いカプセル化正式sun.misc.Unsafe のメモリアクセスも段階的に制限
406Pattern Matching for switchプレビューJDK 21 で正式化
407RMI Activation の削除削除―
409Sealed Classes正式―
410実験的な AOT / JIT コンパイラの削除削除AOT は Project Leyden として別の形で復活
411Security Manager の削除予定化削除予定JDK 24 で恒久的に無効化
412Foreign Function & Memory APIインキュベーターJDK 22 で正式化
414Vector API2nd インキュベーターJDK 26 でも 11th インキュベーター
415コンテキスト別のデシリアライズフィルター正式―

これらは大きく 4 つに分けられる。

flowchart LR
    J17["Java 17"] --> L["言語機能"]
    J17 --> A["ライブラリ"]
    J17 --> S["セキュリティ・保守"]
    J17 --> P["プラットフォーム"]
    L --> L1["Sealed Classes(409)"]
    L --> L2["switch のパターン(406)"]
    L --> L3["strictfp の整理(306)"]
    A --> A1["乱数 API(356)"]
    A --> A2["FFM API(412)"]
    A --> A3["Vector API(414)"]
    S --> S1["強いカプセル化(403)"]
    S --> S2["デシリアライズフィルター(415)"]
    S --> S3["削除・非推奨(398 / 407 / 410 / 411)"]
    P --> P1["macOS/AArch64(391)"]
    P --> P2["Metal 描画(382)"]
    classDef star fill:#fde68a,stroke:#b45309,color:#78350f
    class L1,A1,S1 star

本記事では Java 12〜16 の振り返り → 言語機能 → ライブラリ → セキュリティ・保守 → プラットフォーム の順に見ていく。特に、日々のコードに直接効く 3 つ(黄色)は詳しく扱う。

前提:Java 12〜16 で正式化された機能

Java 11 から 17 に上げると、間の 12〜16 で正式化された機能もまとめて使えるようになる。Java 17 の sealed クラスや switch のパターンマッチングは、これらを土台にしているので、先に押さえておく。

バージョン主な機能
Java 12Collectors.teeing、String.indent / transform
Java 14switch 式(JEP 361)、わかりやすい NullPointerException(JEP 358)
Java 15テキストブロック(JEP 378)、ZGC・Shenandoah の正式化
Java 16record(JEP 395)、instanceof のパターンマッチング(JEP 394)、Stream.toList()、jpackage
flowchart TB
    R["record(16)<br/>データの形を宣言"] --> S["sealed(17)<br/>型の種類を閉じる"]
    I["instanceof パターン(16)<br/>型で分岐して取り出す"] --> P["switch パターン(17 プレビュー)<br/>型ごとに分岐"]
    W["switch 式(14)<br/>値を返す switch"] --> P
    S --> P
    P --> D["データ指向プログラミング<br/>(Java 21 で完成)"]

record で「データの形」を、sealed で「型の種類」を宣言し、パターンで型ごとに処理を分ける。この流れは Java 21 のレコードパターンで完成するが、その半分以上は Java 17 までにそろっている。

// Java 12-16: features that became standard between Java 11 and Java 17
// Run: java Java12To16Demo.java

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Java12To16Demo {

    // record(JEP 395、Java 16):コンストラクタ・アクセサ・equals・hashCode・toString を自動生成
    record Order(String id, int amount) {
        // コンパクトコンストラクタで値を検証する
        Order {
            if (amount < 0) throw new IllegalArgumentException("amount < 0");
        }
    }

    record Customer(String name, Address address) {}
    record Address(String city) {}

    public static void main(String[] args) {
        // テキストブロック(JEP 378、Java 15)
        String json = """
                {
                  "id": "A-1",
                  "amount": 300
                }
                """;
        System.out.print("--- text block ---\n" + json);

        // switch 式(JEP 361、Java 14):値を返し、break も不要
        for (int day : new int[] {1, 6, 8}) {
            String type = switch (day) {
                case 1, 2, 3, 4, 5 -> "weekday";
                case 6, 7 -> "weekend";
                default -> {
                    String message = "invalid day " + day;
                    yield message;
                }
            };
            System.out.println("day " + day + " -> " + type);
        }

        // instanceof のパターンマッチング(JEP 394、Java 16):キャストが不要
        Object o = new Order("A-1", 300);
        if (o instanceof Order order && order.amount() > 100) {
            System.out.println("large order: " + order);
        }
        System.out.println("equals by value: " + o.equals(new Order("A-1", 300)));

        // Stream.toList()(Java 16)と Collectors.teeing(Java 12)
        List<Integer> numbers = Stream.of(3, 1, 4, 1, 5, 9).toList();
        double average = numbers.stream().collect(Collectors.teeing(
                Collectors.summingInt(i -> i), Collectors.counting(), (sum, n) -> (double) sum / n));
        System.out.println("numbers = " + numbers + ", average = " + average);

        // わかりやすい NullPointerException(JEP 358、Java 14 で追加、Java 15 からデフォルトで有効)
        Customer customer = new Customer("Alice", null);
        try {
            System.out.println(customer.address().city());
        } catch (NullPointerException e) {
            System.out.println("NPE: " + e.getMessage());
        }
    }
}
--- text block ---
{
  "id": "A-1",
  "amount": 300
}
day 1 -> weekday
day 6 -> weekend
day 8 -> invalid day 8
large order: Order[id=A-1, amount=300]
equals by value: true
numbers = [3, 1, 4, 1, 5, 9], average = 3.8333333333333335
NPE: Cannot invoke "Java12To16Demo$Address.city()" because the return value of "Java12To16Demo$Customer.address()" is null

最後の NullPointerException のメッセージに注目してほしい。Java 11 までは null だった部分が、どの式が null だったのか まで教えてくれる。これだけでも移行する価値がある。

言語機能

Sealed Classes(JEP 409)

クラスやインターフェースを 継承できる型を限定 できるようになった。sealed を付け、permits に継承を許す型を並べる。

flowchart TB
    S["sealed interface Shape<br/>permits Circle, Square, Polygon"] --> C["record Circle<br/>(暗黙的に final)"]
    S --> Q["final class Square"]
    S --> P["non-sealed class Polygon"]
    P --> T["class Triangle<br/>(誰でも継承できる)"]
    S -.->|"コンパイルエラー"| H["record Hexagon"]
    style H fill:#fecaca,stroke:#b91c1c,color:#7f1d1d

許可された子クラスは、必ず次のどれかを宣言する。

修飾子意味
finalこれ以上継承させない(record と enum は暗黙的に final)
sealedさらに permits で継承できる型を限定する
non-sealed制限を解除し、ここから先は誰でも継承できる
// JEP 409: Sealed Classes
// Run: java SealedClassesDemo.java

import java.util.Arrays;
import java.util.List;

public class SealedClassesDemo {

    // Shape を継承できるのは permits に書いた 3 つだけ
    sealed interface Shape permits Circle, Square, Polygon {}

    // record は暗黙的に final
    record Circle(double radius) implements Shape {}

    // final:これ以上継承させない
    static final class Square implements Shape {
        final double side;
        Square(double side) { this.side = side; }
    }

    // non-sealed:ここから先は誰でも継承できる
    static non-sealed class Polygon implements Shape {
        final int sides;
        Polygon(int sides) { this.sides = sides; }
    }

    static class Triangle extends Polygon {
        Triangle() { super(3); }
    }

    // 次の行はコンパイルエラーになる(Shape は permits に含まれない型を拒否する)
    // record Hexagon() implements Shape {}

    // Java 17 の switch はまだパターンを書けない(プレビュー)ので、instanceof で分岐する
    static double area(Shape shape) {
        if (shape instanceof Circle c) {
            return Math.PI * c.radius() * c.radius();
        } else if (shape instanceof Square s) {
            return s.side * s.side;
        } else if (shape instanceof Polygon p) {
            // 辺の数だけでは面積は決まらない
            return Double.NaN;
        }
        throw new AssertionError("unreachable: Shape is sealed");
    }

    public static void main(String[] args) {
        // リフレクションで「封印」の情報を読める(JDK 17 で追加された API)
        System.out.println("Shape.isSealed()          = " + Shape.class.isSealed());
        System.out.println("permitted subclasses      = " + Arrays.stream(Shape.class.getPermittedSubclasses())
                .map(Class::getSimpleName).toList());
        System.out.println("Polygon.isSealed()        = " + Polygon.class.isSealed());
        System.out.println("Circle is a record        = " + Circle.class.isRecord());
        System.out.println();

        for (Shape s : List.of(new Circle(1), new Square(2), new Triangle())) {
            System.out.printf("%-9s area = %.2f%n", s.getClass().getSimpleName(), area(s));
        }
    }
}
Shape.isSealed()          = true
permitted subclasses      = [Circle, Square, Polygon]
Polygon.isSealed()        = false
Circle is a record        = true

Circle    area = 3.14
Square    area = 4.00
Triangle  area = NaN

コメントアウトしてある record Hexagon() implements Shape {} を有効にすると、次のようにコンパイルが失敗する。

error: class is not allowed to extend sealed class: Shape (as it is not listed in its permits clause)
    record Hexagon() implements Shape {}
    ^

「継承されたくないなら final にすればよい」と思うかもしれない。しかし final では、自分で用意した子クラスまで作れなくなる。sealed は 「この 3 種類で全部」というドメインの知識を、型として表現する ための仕組みである。そして、種類が閉じていることをコンパイラが知っていれば、次の switch で「全部の場合を処理したか」を確認できるようになる。

permits で指定する型は、同じモジュール(無名モジュールなら同じパッケージ)に置く必要がある。また、子クラスが同じファイルにあるときは、permits を省略できる。

Pattern Matching for switch(JEP 406・プレビュー)

switch の case に 型パターン を書けるようになった。Java 16 の instanceof のパターンマッチングを switch に広げたものである。Java 17 ではプレビューなので、--enable-preview --source 17 が必要になる。

// JEP 406: Pattern Matching for switch (preview in Java 17)
// Run: java --enable-preview --source 17 SwitchPatternDemo.java

import java.util.Arrays;

public class SwitchPatternDemo {

    sealed interface Shape permits Circle, Square, Rect {}
    record Circle(double radius) implements Shape {}
    record Square(double side) implements Shape {}
    record Rect(double width, double height) implements Shape {}

    // 従来:instanceof の if-else を連ねる
    static String formatOld(Object o) {
        if (o instanceof Integer i) {
            return "int " + i;
        } else if (o instanceof Long l) {
            return "long " + l;
        } else if (o instanceof String s) {
            return "String \"" + s + "\"";
        }
        return "other";
    }

    // Java 17(プレビュー):case に型パターンを書ける。case null も書ける
    static String formatNew(Object o) {
        return switch (o) {
            case null      -> "null!";
            case Integer i -> "int " + i;
            case Long l    -> "long " + l;
            case String s  -> "String \"" + s + "\"";
            case int[] a   -> "int[] " + Arrays.toString(a);
            default        -> "other";
        };
    }

    // case null がない switch は、これまでどおり null で NullPointerException を投げる
    static String withoutNullCase(Object o) {
        return switch (o) {
            case String s -> "String";
            default       -> "other";
        };
    }

    // ガード付きパターン:Java 17 では && で条件を付ける(JDK 19 で when に変わった)
    static String size(Shape shape) {
        return switch (shape) {
            case Circle c && c.radius() > 10 -> "large circle";
            case Circle c                    -> "small circle";
            case Square s                    -> "square " + s.side();
            case Rect r                      -> "rect " + r.width() + "x" + r.height();
            // Shape は sealed なので default は不要(網羅性をコンパイラが確認する)
        };
    }

    public static void main(String[] args) {
        Object[] values = {42, 42L, "hello", new int[] {1, 2}, 3.14, null};
        System.out.printf("%-16s | %s%n", "formatNew", "formatOld");
        for (Object v : values) {
            System.out.printf("%-16s | %s%n", formatNew(v), formatOld(v));
        }
        try {
            withoutNullCase(null);
        } catch (NullPointerException e) {
            System.out.println("withoutNullCase(null) -> NullPointerException");
        }
        System.out.println();
        for (Shape s : new Shape[] {new Circle(20), new Circle(1), new Square(2), new Rect(2, 3)}) {
            System.out.println(s + " -> " + size(s));
        }
    }
}
Note: SwitchPatternDemo.java uses preview features of Java SE 17.
Note: Recompile with -Xlint:preview for details.
formatNew        | formatOld
int 42           | int 42
long 42          | long 42
String "hello"   | String "hello"
int[] [1, 2]     | other
other            | other
null!            | other
withoutNullCase(null) -> NullPointerException

Circle[radius=20.0] -> large circle
Circle[radius=1.0] -> small circle
Square[side=2.0] -> square 2.0
Rect[width=2.0, height=3.0] -> rect 2.0x3.0

ポイントは 3 つある。

  1. 型パターン:case Integer i のように、型の判定と変数への代入を同時に行う。if-else を連ねるより読みやすく、コンパイラが最適化しやすい。
  2. case null:これまでの switch は、null を渡すと必ず NullPointerException を投げた。case null を書けば、null も 1 つの場合として扱える。書かなければ、従来どおり例外になる。
  3. 網羅性のチェック:Shape は sealed なので、Circle・Square・Rect の 3 つを書けば default は不要になる。Shape に 4 つ目の型を追加すると、この switch がコンパイルエラーになり、修正漏れに気づける。

Java 17 のガード付きパターンは case Circle c && c.radius() > 10 のように && で書く。この構文は JDK 19 で when に置き換えられたため、このサンプルは JDK 21 ではコンパイルできない。

SwitchPatternDemo.java:48: error: : or -> expected
            case Circle c && c.radius() > 10 -> "large circle";
                         ^

switch のパターンマッチングは、正式化までに 4 回のプレビューを経ている。

flowchart TB
    A["JDK 17<br/>1st プレビュー<br/>(406)"] --> B["JDK 18<br/>2nd<br/>(420)"]
    B --> C["JDK 19<br/>3rd(427)<br/>&& → when"]
    C --> D["JDK 20<br/>4th<br/>(433)"]
    D --> E["JDK 21<br/>正式(441)<br/>+レコードパターン"]
    style E fill:#bbf7d0,stroke:#15803d,color:#14532d

その後:JDK 21 で正式化された(JEP 441)。同時にレコードパターン(JEP 440)も正式化され、case Circle(double r) when r > 10 のように record を分解しながら分岐できるようになった。JDK 23 からは case int i のようなプリミティブ型のパターンがプレビューになっており、JDK 26 でも 4th プレビュー(JEP 530)である。

厳密な浮動小数点演算の復活(JEP 306)

浮動小数点の計算が、strictfp の有無にかかわらず 常に IEEE 754 どおり厳密に 行われるようになった。

かつての x87 FPU は、内部で 80 ビットの拡張精度を使って計算していた。厳密に IEEE 754 の 64 ビットで計算すると遅かったため、Java 1.2 で「既定は少し緩く、厳密にしたいときだけ strictfp」という 2 つの意味を持たせた。今の CPU(SSE2 以降や ARM)では厳密な計算でも遅くならないので、Java 1.2 より前の「常に厳密」に戻したのが JEP 306 である。

// JEP 306: Restore Always-Strict Floating-Point Semantics
// Run: java StrictFpDemo.java

public class StrictFpDemo {

    // Java 16 までは、strictfp の有無で中間結果の扱いが変わる可能性があった
    static strictfp double withStrictfp(double x) {
        return x * 10 / 10;
    }

    // Java 17 からは、strictfp がなくても常に厳密(IEEE 754)に計算される
    static double withoutStrictfp(double x) {
        return x * 10 / 10;
    }

    public static void main(String[] args) {
        // x * 10 は double の最大値(約 1.8e308)を超えるので、途中で Infinity になる
        double x = 1e308;
        System.out.println("with strictfp    : " + withStrictfp(x));
        System.out.println("without strictfp : " + withoutStrictfp(x));
        System.out.println("0.1 + 0.2        : " + (0.1 + 0.2));
    }
}
StrictFpDemo.java:7: warning: [strictfp] as of release 17, all floating-point expressions are evaluated strictly and 'strictfp' is not required
    static strictfp double withStrictfp(double x) {
                           ^
1 warning
with strictfp    : Infinity
without strictfp : Infinity
0.1 + 0.2        : 0.30000000000000004

strictfp は付けても意味がなくなったので、javac が警告を出す。計算結果はどちらも同じで、途中の x * 10 がオーバーフローして Infinity になる。古い拡張精度の環境では、strictfp のない側が 1.0E308 に戻ることがあった。

実務への影響はほとんどない。strictfp を書いているコードがあれば、削除してかまわない。

擬似乱数生成器の刷新(JEP 356)

乱数生成器(PRNG)に 共通のインターフェース RandomGenerator が導入され、用途に応じてアルゴリズムを選べるようになった。

共通インターフェース RandomGenerator

これまでの Java には Random、ThreadLocalRandom、SplittableRandom があったが、共通のインターフェースがなく、アルゴリズムを差し替えにくかった。Java 17 では、どれも RandomGenerator を実装し、さらに性質ごとのサブインターフェースが用意された。

flowchart TB
    RG["RandomGenerator<br/>nextInt / nextLong / nextDouble …"] --> ST["StreamableGenerator<br/>独立した生成器の Stream"]
    ST --> SP["SplittableGenerator<br/>split()"]
    ST --> JU["JumpableGenerator<br/>jump()"]
    JU --> LE["LeapableGenerator<br/>leap()"]
    LE --> AJ["ArbitrarilyJumpable<br/>Generator"]
    SP --> LXM["LXM 系(L64X128MixRandom など)<br/>SplittableRandom"]
    LE --> XO["Xoshiro256PlusPlus<br/>Xoroshiro128PlusPlus"]
    RG --> LG["Random / SecureRandom<br/>ThreadLocalRandom"]
// JEP 356: Enhanced Pseudo-Random Number Generators
// Run: java RandomGeneratorDemo.java

import java.util.Comparator;
import java.util.Random;
import java.util.random.RandomGenerator;
import java.util.random.RandomGenerator.SplittableGenerator;
import java.util.random.RandomGenerator.JumpableGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomGeneratorDemo {

    public static void main(String[] args) {
        // 使えるアルゴリズムの一覧(グループ・状態ビット数・性質)
        System.out.println("--- algorithms ---");
        RandomGeneratorFactory.all()
                .sorted(Comparator.comparing(RandomGeneratorFactory<RandomGenerator>::group)
                        .thenComparing(RandomGeneratorFactory::name))
                .forEach(f -> System.out.printf("%-10s %-22s %9s  %s%n", f.group(), f.name(),
                        f.stateBits() == Integer.MAX_VALUE ? "-" : f.stateBits() + " bits",
                        f.isSplittable() ? "splittable" : f.isJumpable() ? "jumpable" : f.isStreamable() ? "streamable" : ""));

        // 既存のクラスも共通インターフェース RandomGenerator を実装している
        System.out.println();
        System.out.println("--- one interface ---");
        // 1 つ目は従来の Random、2 つ目は名前で選んだもの、3 つ目は既定のアルゴリズム
        RandomGenerator[] generators = {
                new Random(42),
                RandomGenerator.of("L64X128MixRandom"),
                RandomGenerator.getDefault(),
        };
        for (RandomGenerator g : generators) {
            System.out.printf("%-34s nextInt(100) = %d%n", g.getClass().getName(), g.nextInt(100));
        }

        // 同じシードなら同じ乱数列(再現性)
        System.out.println();
        System.out.println("--- seeds ---");
        RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Xoshiro256PlusPlus");
        System.out.println("seed 2021 run 1: " + factory.create(2021).ints(5, 0, 100).boxed().toList());
        System.out.println("seed 2021 run 2: " + factory.create(2021).ints(5, 0, 100).boxed().toList());

        // split():統計的に独立した子ジェネレーターを作る(並列処理向き)
        System.out.println();
        System.out.println("--- split / jump ---");
        SplittableGenerator parent = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(7);
        System.out.println("child 1: " + parent.split().ints(3, 0, 100).boxed().toList());
        System.out.println("child 2: " + parent.split().ints(3, 0, 100).boxed().toList());

        // jump():状態を 2^128 個先まで一気に進める(重ならない部分列を作る)
        JumpableGenerator jumpable = (JumpableGenerator) RandomGeneratorFactory.of("Xoshiro256PlusPlus").create(7);
        JumpableGenerator copy = jumpable.copy();
        jumpable.jump();
        System.out.println("before jump: " + copy.nextInt(100) + ", after jump: " + jumpable.nextInt(100)
                + " (jump distance = 2^" + Math.round(Math.log(jumpable.jumpDistance()) / Math.log(2)) + ")");
    }
}
--- algorithms ---
LXM        L128X1024MixRandom     1152 bits  splittable
LXM        L128X128MixRandom       256 bits  splittable
LXM        L128X256MixRandom       384 bits  splittable
LXM        L32X64MixRandom          96 bits  splittable
LXM        L64X1024MixRandom      1088 bits  splittable
LXM        L64X128MixRandom        192 bits  splittable
LXM        L64X128StarStarRandom   192 bits  splittable
LXM        L64X256MixRandom        320 bits  splittable
Legacy     Random                   48 bits  
Legacy     SecureRandom                   -  
Legacy     SplittableRandom         64 bits  splittable
Xoroshiro  Xoroshiro128PlusPlus    128 bits  jumpable
Xoshiro    Xoshiro256PlusPlus      256 bits  jumpable

--- one interface ---
java.util.Random                   nextInt(100) = 30
jdk.random.L64X128MixRandom        nextInt(100) = 61
jdk.random.L32X64MixRandom         nextInt(100) = 72

--- seeds ---
seed 2021 run 1: [83, 80, 16, 88, 25]
seed 2021 run 2: [83, 80, 16, 88, 25]

--- split / jump ---
child 1: [62, 27, 16]
child 2: [38, 52, 12]
before jump: 26, after jump: 73 (jump distance = 2^128)

アルゴリズムの選び方の目安は次のとおり。

用途選択肢
とくにこだわりがないRandomGenerator.getDefault()(現在は L32X64MixRandom)
並列処理(ストリームや Fork/Join)LXM 系を split() して、スレッドごとに渡す
シミュレーションで重ならない部分列が欲しいXoshiro 系の jump() / leap()
暗号・トークン引き続き SecureRandom

既存の Random もそのまま使えるし、RandomGenerator を引数に取るメソッドに new Random() を渡すこともできる。

実測:スレッド間で Random を共有するコスト

java.util.Random はスレッドセーフだが、内部の状態(シード)を AtomicLong の CAS で更新している。複数のスレッドで 1 つの Random を共有すると、この CAS を奪い合うことになる。8 スレッドでそれぞれ 200 万回 nextLong() を呼び、時間を比べた。

// JEP 356: Enhanced Pseudo-Random Number Generators - sharing one Random across threads vs split()
// Run: java RandomContentionDemo.java

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.IntFunction;
import java.util.random.RandomGenerator;
import java.util.random.RandomGenerator.SplittableGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomContentionDemo {

    static final int THREADS = 8;
    static final int PER_THREAD = 2_000_000;
    static volatile long sink;

    // THREADS 本のスレッドで、それぞれ PER_THREAD 回 nextLong() を呼び、かかった時間(ms)を返す
    static long run(IntFunction<RandomGenerator> generatorForThread) throws InterruptedException {
        Thread[] threads = new Thread[THREADS];
        long start = System.nanoTime();
        for (int t = 0; t < THREADS; t++) {
            int id = t;
            threads[t] = new Thread(() -> {
                // ジェネレーターはスレッドの中で取得する(ThreadLocalRandom.current() のため)
                RandomGenerator g = generatorForThread.apply(id);
                long sum = 0;
                for (int i = 0; i < PER_THREAD; i++) sum += g.nextLong();
                sink += sum;
            });
            threads[t].start();
        }
        for (Thread th : threads) th.join();
        return (System.nanoTime() - start) / 1_000_000;
    }

    public static void main(String[] args) throws InterruptedException {
        Random shared = new Random(1);
        SplittableGenerator root = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(1);
        SplittableGenerator[] children = new SplittableGenerator[THREADS];
        for (int t = 0; t < THREADS; t++) children[t] = root.split();

        System.out.printf("%d threads x %,d nextLong() calls%n%n", THREADS, PER_THREAD);
        // 1 回目は JIT のウォームアップなので、2 回目以降の 3 回を表示する
        for (int round = 0; round <= 3; round++) {
            // ① 1 つの Random を全スレッドで共有(内部の AtomicLong を CAS で奪い合う)
            long a = run(t -> shared);
            // ② ThreadLocalRandom(スレッドごとの状態)
            long b = run(t -> ThreadLocalRandom.current());
            // ③ split() した L64X128MixRandom をスレッドごとに 1 つ
            long c = run(t -> children[t]);
            if (round > 0) {
                System.out.printf("round %d: shared Random %5d ms | ThreadLocalRandom %4d ms | split() %4d ms%n", round, a, b, c);
            }
        }
    }
}
8 threads x 2,000,000 nextLong() calls

round 1: shared Random  3197 ms | ThreadLocalRandom   22 ms | split()   39 ms
round 2: shared Random  2545 ms | ThreadLocalRandom   40 ms | split()   72 ms
round 3: shared Random  1226 ms | ThreadLocalRandom   27 ms | split()   41 ms

8 スレッド × 200 万回の nextLong()(実測、3 回の平均、目盛りの単位:s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 500millisecond
    section Random (shared)
    2323 ms :crit, 0, 2323
    section ThreadLocalRandom
    30 ms :active, 0, 30
    section split()
    51 ms :active, 0, 51

共有した Random は、スレッドごとに状態を持つ方式に比べて 数十倍遅い。CAS が失敗するたびにやり直しが発生し、スレッドを増やしても速くならない(むしろ遅くなる)。

ThreadLocalRandom が最も速いのは、状態を Thread オブジェクトに直接持ち、JIT コンパイラが特別扱いしているためである。並列処理で 再現性(同じシードなら同じ結果)が必要な場合は、ThreadLocalRandom ではシードを指定できないので、split() を使う。

その後:JDK 19 で Random.from(RandomGenerator) が追加され、Random を要求する古い API に新しいアルゴリズムを渡せるようになった。

JDK 内部の強いカプセル化(JEP 403)

JDK の内部 API(sun.* や java.* の private メンバー)に、リフレクションでアクセスできなくなった。Java 9 のモジュールシステム導入から段階的に進めてきた制限の、最終段階である。

flowchart TB
    A["JDK 9〜15<br/>--illegal-access=permit<br/>(警告だけで許可)"] --> B["JDK 16(JEP 396)<br/>既定が deny に"]
    B --> C["JDK 17(JEP 403)<br/>--illegal-access を廃止<br/>(指定しても無視)"]
    style C fill:#fecaca,stroke:#b91c1c,color:#7f1d1d
// JEP 403: Strongly Encapsulate JDK Internals
// Run: java EncapsulationDemo.java

import java.lang.reflect.Field;

public class EncapsulationDemo {

    public static void main(String[] args) throws Exception {
        // ① java.lang.String の private フィールド value にリフレクションでアクセスする
        Field value = String.class.getDeclaredField("value");
        try {
            value.setAccessible(true);
            byte[] bytes = (byte[]) value.get("hello");
            System.out.println("(1) String.value opened: " + bytes.length + " bytes");
        } catch (RuntimeException e) {
            System.out.println("(1) " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }

        // ② JDK 内部パッケージ(sun.nio.ch)のクラスもリフレクションでは開けない
        try {
            Class<?> util = Class.forName("sun.nio.ch.Util");
            util.getDeclaredMethod("getTemporaryDirectBuffer", int.class).setAccessible(true);
            System.out.println("(2) sun.nio.ch.Util opened");
        } catch (RuntimeException e) {
            System.out.println("(2) " + e.getClass().getSimpleName());
        }

        // ③ sun.misc.Unsafe は jdk.unsupported モジュールにあり、Java 17 でも使える(例外扱い)
        Field theUnsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        System.out.println("(3) sun.misc.Unsafe: " + theUnsafe.get(null).getClass().getName());

        // ④ java.base が java.lang パッケージを無名モジュールに open しているか
        Module base = String.class.getModule();
        System.out.println("(4) java.lang opened to us: " + base.isOpen("java.lang", EncapsulationDemo.class.getModule()));
    }
}
(1) InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @6b1274d2
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: false

どうしても必要な場合は、--add-opens で パッケージ単位で明示的に 開ける。Java 16 までの逃げ道だった --illegal-access=permit は、Java 17 では無視される。

java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo.java
(1) String.value opened: 5 bytes
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: true
java --illegal-access=permit EncapsulationDemo.java
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=permit; support was removed in 17.0
(1) InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @6b1274d2
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: false

古いライブラリ(Lombok・Mockito・一部の ORM やシリアライザなど)の古いバージョンは、内部 API に依存していることがある。Java 17 への移行で InaccessibleObjectException が出たら、まずライブラリを更新するのが正解である。--add-opens は、更新できない場合の一時的な回避策と考えたほうがよい。

sun.misc.Unsafe などの一部の内部 API は、jdk.unsupported モジュールで引き続き公開されている(③)。代替 API がまだないものを、移行期間として残しているためである。

その後:sun.misc.Unsafe も、代替となる VarHandle と FFM API がそろったことで制限が進んでいる。メモリアクセス用のメソッドは JDK 23 で削除予定の非推奨になり(JEP 471)、JDK 24 からは使うと警告が出る(JEP 498)。JDK 24 からは JNI を使ったときにも警告が出るようになり(JEP 472)、JDK 26 ではリフレクションで final フィールドを書き換えると警告が出るようになった(JEP 500)。

コンテキスト別のデシリアライズフィルター(JEP 415)

Java のデシリアライズ(ObjectInputStream)は、受け取ったバイト列に書かれたクラスを何でもインスタンス化してしまう。これを悪用する「デシリアライズ攻撃」を防ぐため、Java 9 でフィルター(JEP 290)が導入された。しかし、JVM 全体で 1 つのフィルターしか設定できず、「この通信ではこのクラスだけ」といった 場面ごとの制御 が難しかった。

Java 17 では フィルターファクトリ を登録できるようになった。ストリームが作られるたび、またストリームに個別のフィルターが設定されるたびにファクトリが呼ばれ、実際に使うフィルターを決める。

sequenceDiagram
    participant App as アプリ
    participant OIS as ObjectInputStream
    participant F as フィルターファクトリ
    App->>OIS: new ObjectInputStream(...)
    OIS->>F: apply(null, null)
    F-->>OIS: 基本ルール
    App->>OIS: setObjectInputFilter(許可リスト)
    OIS->>F: apply(基本ルール, 許可リスト)
    F-->>OIS: 合成したフィルター
    App->>OIS: readObject()
    Note over OIS: クラスごとに<br/>合成フィルターで判定
// JEP 415: Context-Specific Deserialization Filters
// Run: java DeserializationFilterDemo.java

import java.io.*;
import java.util.ArrayList;
import java.util.function.BinaryOperator;

public class DeserializationFilterDemo {

    record Order(String id, int amount) implements Serializable {}

    // 攻撃に使われうる「想定外のクラス」の例
    static class Gadget implements Serializable {}

    static byte[] serialize(Object o) throws IOException {
        var out = new ByteArrayOutputStream();
        try (var oos = new ObjectOutputStream(out)) {
            oos.writeObject(o);
        }
        return out.toByteArray();
    }

    static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            return ois.readObject();
        }
    }

    public static void main(String[] args) throws Exception {
        // JVM 全体の基本ルール:ネストの深さと配列の長さを制限する
        ObjectInputFilter base = ObjectInputFilter.Config.createFilter("maxdepth=10;maxarray=1000");

        // Java 17 の新機能:フィルターファクトリ。ストリームごとに「どのフィルターを使うか」を決める
        BinaryOperator<ObjectInputFilter> factory = (current, requested) -> {
            if (requested == null) {
                // ストリーム作成時:基本ルールを適用する
                return base;
            }
            // setObjectInputFilter() が呼ばれたとき:基本ルールと個別ルールを合成し、どちらも未判定なら拒否する
            return ObjectInputFilter.rejectUndecidedClass(ObjectInputFilter.merge(requested, current));
        };
        ObjectInputFilter.Config.setSerialFilterFactory(factory);

        // 許可リスト(DeserializationFilterDemo$Order と java.lang の一部だけ)
        ObjectInputFilter allowOrders = ObjectInputFilter.Config.createFilter(
                "DeserializationFilterDemo$Order;java.lang.String;java.lang.Number;java.lang.Integer");

        Object[] inputs = {new Order("A-1", 300), new Gadget(), new int[5000]};
        for (Object input : inputs) {
            byte[] bytes = serialize(input);
            try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
                ois.setObjectInputFilter(allowOrders);
                Object result = ois.readObject();
                System.out.println("ACCEPTED : " + result);
            } catch (InvalidClassException e) {
                System.out.println("REJECTED : " + input.getClass().getSimpleName() + " (" + e.getMessage() + ")");
            }
        }

        // setObjectInputFilter() を呼ばないストリームにも基本ルール(maxarray=1000)は効く
        System.out.println();
        for (Object input : new Object[] {new ArrayList<>(java.util.List.of(1, 2, 3)), new int[5000]}) {
            try {
                System.out.println("ACCEPTED : " + input.getClass().getSimpleName() + " -> " + deserialize(serialize(input)).getClass().getSimpleName());
            } catch (InvalidClassException e) {
                System.out.println("REJECTED : " + input.getClass().getSimpleName() + " (" + e.getMessage() + ")");
            }
        }
    }
}
ACCEPTED : Order[id=A-1, amount=300]
REJECTED : Gadget (filter status: REJECTED)
REJECTED : int[] (filter status: REJECTED)

ACCEPTED : ArrayList -> ArrayList
REJECTED : int[] (filter status: REJECTED)

許可リストにない Gadget は拒否され、許可リストを設定していないストリームでも、基本ルール(配列の長さが 1000 まで)で大きな配列が拒否されている。ライブラリが自分で setObjectInputFilter() を呼ばなくても、アプリ側でまとめて安全側に倒せるのがポイントである。

この機能は重要度が高いため、Java 8 と 11 の更新リリースにもバックポートされている。とはいえ、根本的な対策は 信頼できないデータを Java のシリアライズで受け取らない ことである。JSON などの形式を使えるなら、そちらを選ぶ。

インキュベーター API

インキュベーターは、プレビューよりさらに手前の「試験的な API」である。jdk.incubator.* パッケージに置かれ、--add-modules で明示的に追加しないと使えない。API は次のバージョンで大きく変わることがある。

Foreign Function & Memory API(JEP 412・インキュベーター)

JNI を使わずに、C のライブラリを呼び出し、ヒープ外のメモリを安全に扱う ための API である。Java 14 からの Foreign Memory Access API(JEP 370 など)と、Foreign Linker API(JEP 389)を 1 つにまとめたものが、Java 17 の JEP 412 である。

// JEP 412: Foreign Function & Memory API (incubator in Java 17)
// Run: java --add-modules jdk.incubator.foreign --enable-native-access=ALL-UNNAMED ForeignMemoryDemo.java

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;

public class ForeignMemoryDemo {

    public static void main(String[] args) throws Throwable {
        // ① C 標準ライブラリの strlen を JNI なしで呼ぶ
        MethodHandle strlen = CLinker.getInstance().downcallHandle(
                CLinker.systemLookup().lookup("strlen").orElseThrow(),
                MethodType.methodType(long.class, MemoryAddress.class),
                FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER));

        // ResourceScope を閉じると、その中で確保したネイティブメモリがまとめて解放される
        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            MemorySegment cString = CLinker.toCString("Hello, Java 17!", scope);
            long length = (long) strlen.invokeExact(cString.address());
            System.out.println("strlen(\"Hello, Java 17!\") = " + length);

            // ② ヒープ外(オフヒープ)に int を 5 個分確保して読み書きする
            MemorySegment ints = MemorySegment.allocateNative(5 * 4, scope);
            for (int i = 0; i < 5; i++) {
                MemoryAccess.setIntAtIndex(ints, i, i * i);
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 5; i++) {
                sb.append(MemoryAccess.getIntAtIndex(ints, i)).append(' ');
            }
            System.out.println("off-heap ints = " + sb.toString().trim() + " (" + ints.byteSize() + " bytes)");

            // ③ 範囲外アクセスは JVM のクラッシュではなく例外になる
            try {
                MemoryAccess.setIntAtIndex(ints, 5, 25);
            } catch (IndexOutOfBoundsException e) {
                System.out.println("index 5 -> " + e.getClass().getSimpleName());
            }
        }
        System.out.println("scope closed: native memory freed");
    }
}
WARNING: Using incubator modules: jdk.incubator.foreign
warning: using incubating module(s): jdk.incubator.foreign
1 warning
strlen("Hello, Java 17!") = 15
off-heap ints = 0 1 4 9 16 (20 bytes)
index 5 -> IndexOutOfBoundsException
scope closed: native memory freed

JNI なら、C のコードを書いてコンパイルし、共有ライブラリを用意する必要がある。FFM API では、Java のコードだけで strlen を呼び出せる。確保したメモリは ResourceScope を閉じたときに解放され、範囲外アクセスは JVM をクラッシュさせずに例外になる。

ネイティブコードの呼び出しは JVM の安全性を損なう可能性があるので、--enable-native-access で明示的に許可する必要がある。付け忘れると次のように失敗する。

Exception in thread "main" java.lang.IllegalCallerException: Illegal native access from: unnamed module @4f638935

その後:JDK 18 で 2nd インキュベーター、JDK 19〜21 でプレビューを経て、JDK 22 で正式化 された(JEP 454)。パッケージは java.lang.foreign に移り、API も大きく変わっている(ResourceScope → Arena、CLinker → Linker など)。上のコードは Java 17 専用なので、JDK 22 以降では書き直しが必要になる。また、正式版では --enable-native-access がなくても警告が出るだけで、実行は続く。

Vector API(JEP 414・2nd インキュベーター)

CPU の SIMD 命令を使ったベクトル演算を、Java から明示的に書くための API である。1 命令で複数の要素(レーン)をまとめて計算する。ここでは 2 つの配列の内積を、普通のループと Vector API で計算して比べた。

// JEP 414: Vector API (second incubator in Java 17)
// Run: java --add-modules jdk.incubator.vector VectorApiDemo.java

import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

public class VectorApiDemo {

    static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

    // 2 つの配列の内積(a[0]*b[0] + a[1]*b[1] + ...)を普通のループで計算する
    static float scalarDot(float[] a, float[] b) {
        float sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    // 同じ計算を Vector API で、SPECIES.length() 個ずつまとめて行う
    static float vectorDot(float[] a, float[] b) {
        var acc = FloatVector.zero(SPECIES);
        int i = 0;
        int upper = SPECIES.loopBound(a.length);
        for (; i < upper; i += SPECIES.length()) {
            var va = FloatVector.fromArray(SPECIES, a, i);
            var vb = FloatVector.fromArray(SPECIES, b, i);
            // acc += va * vb(積和演算)
            acc = va.fma(vb, acc);
        }
        float sum = acc.reduceLanes(VectorOperators.ADD);
        // 端数はスカラーで処理する
        for (; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        System.out.println("species = " + SPECIES + ", lanes per operation = " + SPECIES.length());

        float[] small = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        System.out.println("dot(small, small): scalar = " + scalarDot(small, small) + ", vector = " + vectorDot(small, small));

        // 100 万要素の内積を 500 回計算して時間を比べる
        float check = 0;
        float[] a = new float[1_000_000];
        float[] b = new float[1_000_000];
        for (int i = 0; i < a.length; i++) {
            a[i] = (i % 100) / 100f;
            b[i] = (i % 7) / 7f;
        }
        // 計測の前に、両方のメソッドを JIT コンパイルさせておく(ウォームアップ)
        float[] tiny = new float[1024];
        for (int r = 0; r < 20_000; r++) {
            check += scalarDot(tiny, tiny) + vectorDot(tiny, tiny);
        }
        for (int round = 1; round <= 3; round++) {
            long t0 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += scalarDot(a, b);
            long t1 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += vectorDot(a, b);
            long t2 = System.nanoTime();
            System.out.printf("round %d: scalar %4d ms | vector %4d ms%n", round, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000);
        }
        // 結果を使って、計算が最適化で消されないようにする
        if (check == 42) System.out.println();
    }
}
WARNING: Using incubator modules: jdk.incubator.vector
warning: using incubating module(s): jdk.incubator.vector
1 warning
species = Species[float, 4, S_128_BIT], lanes per operation = 4
dot(small, small): scalar = 385.0, vector = 385.0
round 1: scalar  575 ms | vector  196 ms
round 2: scalar  519 ms | vector  181 ms
round 3: scalar  560 ms | vector  231 ms

100 万要素の内積 × 500 回(実測、3 回の平均、目盛りの単位:s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 200millisecond
    section 普通のループ
    551 ms :crit, 0, 551
    section Vector API
    203 ms :active, 0, 203

この環境(Apple Silicon)では float を 4 つずつまとめて計算しており、普通のループより約 3 倍速い。

最初はウォームアップをせずに計測したところ、5 回中 4 回で Vector API のほうが遅く なった(普通のループ約 550 ms に対して 700〜1100 ms)。Vector API は、JIT コンパイラが FloatVector などのオブジェクトを消してベクトル命令に置き換えたときに初めて速くなる。その最適化が効く前のコードで計測すると、ただの「オブジェクトを大量に作るループ」になってしまう。Vector API の効果を測るときは、ウォームアップを入れ、できれば JMH を使う。

「JIT コンパイラが自動でベクトル化してくれるのでは」と思うかもしれない。しかし浮動小数点の足し算は、足す順番を変えると結果がわずかに変わるため、JIT は内積のような「足し込み」のループを勝手にベクトル化できない。Vector API を使えば、「順番が変わってもよい」ことをプログラマーが明示できる。

その後:Valhalla プロジェクト(値オブジェクト)の完成を待っているため、JDK 26 でも 11th インキュベーター(JEP 529)のままである。

削除・非推奨

Java 17 では、古い機能の整理も進んだ。まとめて 1 つのサンプルで確かめる。

// JEP 398 / 407 / 410 / 411: deprecations and removals in Java 17
// Run: java RemovalsDemo.java

import java.nio.file.Files;
import java.nio.file.Path;

public class RemovalsDemo {

    static void printDeprecation(String className) {
        try {
            Deprecated d = Class.forName(className).getAnnotation(Deprecated.class);
            if (d == null) {
                System.out.printf("%-34s not deprecated%n", className);
                return;
            }
            System.out.printf("%-34s @Deprecated(since=\"%s\", forRemoval=%s)%n", className, d.since(), d.forRemoval());
        } catch (ClassNotFoundException e) {
            System.out.printf("%-34s ClassNotFoundException%n", className);
        }
    }

    // 削除予定の API を使うと javac が [removal] 警告を出すので、ここでは抑止している
    @SuppressWarnings("removal")
    public static void main(String[] args) {
        // JEP 398:Applet API は「削除予定」になった
        printDeprecation("java.applet.Applet");

        // JEP 411:Security Manager も「削除予定」になった
        printDeprecation("java.lang.SecurityManager");

        // JEP 407:RMI Activation は削除された(java.rmi のほかの機能は残る)
        printDeprecation("java.rmi.activation.Activatable");
        printDeprecation("java.rmi.Remote");

        // JEP 410:実験的な AOT コンパイラ(jaotc)と Graal JIT が削除された
        Path bin = Path.of(System.getProperty("java.home"), "bin");
        System.out.printf("%-34s %s%n", "bin/jaotc exists", Files.exists(bin.resolve("jaotc")));
        System.out.printf("%-34s %s%n", "module jdk.internal.vm.compiler", ModuleLayer.boot().findModule("jdk.internal.vm.compiler").isPresent());

        // Security Manager を有効にしようとすると、警告が出る(標準エラー出力)
        System.out.println();
        System.out.flush();
        System.setSecurityManager(new SecurityManager());
        System.out.println("Security Manager installed: " + (System.getSecurityManager() != null));
    }
}
java.applet.Applet                 @Deprecated(since="9", forRemoval=true)
java.lang.SecurityManager          @Deprecated(since="17", forRemoval=true)
java.rmi.activation.Activatable    ClassNotFoundException
java.rmi.Remote                    not deprecated
bin/jaotc exists                   false
module jdk.internal.vm.compiler    false

WARNING: A terminally deprecated method in java.lang.System has been called
WARNING: System::setSecurityManager has been called by RemovalsDemo (file:/path/to/java17/RemovalsDemo.java)
WARNING: Please consider reporting this to the maintainers of RemovalsDemo
WARNING: System::setSecurityManager will be removed in a future release
Security Manager installed: true

Applet API の削除予定化(JEP 398)

Applet は、すでにどのブラウザでも動かない。Java 9 で非推奨になっていた java.applet パッケージなどが、Java 17 で 削除予定(forRemoval=true)になった。

その後:JDK 26 で削除 された(JEP 504)。

Security Manager の削除予定化(JEP 411)

Security Manager は、信頼できないコード(Applet など)を JVM の中で制限するための仕組みだった。しかし設定が難しく実際にはほとんど使われていない一方で、JDK の多くのコードに権限チェックの負担を強いていた。Java 17 で 削除予定 になり、System.setSecurityManager() を呼ぶと上のような警告が出る。

flowchart TB
    A["JDK 17<br/>削除予定・警告"] --> B["JDK 18<br/>既定で有効化を禁止<br/>(allow を指定すれば可)"]
    B --> C["JDK 24(JEP 486)<br/>恒久的に無効化"]
    style C fill:#fecaca,stroke:#b91c1c,color:#7f1d1d

その後:JDK 18 からは、-Djava.security.manager=allow を付けないと setSecurityManager() が UnsupportedOperationException を投げる。JDK 24 で恒久的に無効化 され(JEP 486)、有効にする方法はなくなった。

RMI Activation の削除(JEP 407)

RMI のオブジェクトを必要に応じて起動する仕組み(java.rmi.activation)が削除された。Java 15 で削除予定になっていた。RMI そのもの(java.rmi.Remote など)は残っている。

実験的な AOT / JIT コンパイラの削除(JEP 410)

Java 9 で追加された AOT コンパイラ(jaotc)と、Java で書かれた JIT コンパイラ Graal(実験的機能)が削除された。ほとんど使われておらず、保守の負担が大きかったためである。Graal を使いたい場合は GraalVM を使う。

その後:「起動を速くする」という目的は、Project Leyden が別の方法で引き継いでいる。JDK 24 でクラスのロードとリンクの結果をキャッシュする AOT キャッシュ(JEP 483)が入り、JDK 25 でメソッドのプロファイル(JEP 515)、JDK 26 ではどの GC でも使えるオブジェクトのキャッシュ(JEP 516)へと広がっている。

プラットフォーム

macOS/AArch64 ポート(JEP 391)

Apple Silicon(M1 以降)の Mac で、ネイティブに動く JDK が公式に提供されるようになった。それまでは Rosetta 2 で x64 版を動かす必要があった。本記事の検証環境も、このポートである。

java -XshowSettings:properties -version 2>&1 | grep -E 'os\.(arch|name)'
    os.arch = aarch64
    os.name = Mac OS X

macOS の新しい描画パイプライン(JEP 382)

Apple が OpenGL を非推奨にしたことを受けて、Swing / Java 2D の描画に Apple の Metal API を使うパイプラインが追加された。Java 17 では既定は OpenGL のままで、-Dsun.java2d.metal=true を付けると Metal を使う。

その後:JDK 19 で Metal が既定 になった。OpenGL のパイプラインも残っており、-Dsun.java2d.opengl=true で切り替えられる。

JEP 以外の追加 API:HexFormat と InstantSource

JEP にはなっていないが、Java 17 では便利な API もいくつか追加されている。

  • java.util.HexFormat:バイト列と 16 進文字列の相互変換。これまでは自作するか、外部ライブラリが必要だった。
  • java.time.InstantSource:「今の時刻」だけを返すインターフェース。Clock からタイムゾーンを取り除いたもので、Clock 自体もこれを実装している。
// Java 17 APIs outside the JEPs: HexFormat and InstantSource
// Run: java HexFormatDemo.java

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.util.HexFormat;

public class HexFormatDemo {

    // InstantSource:「今の時刻」だけを提供するインターフェース(タイムゾーンを持たない Clock)
    record Token(String value, Instant expiresAt) {
        boolean isExpired(InstantSource time) {
            return time.instant().isAfter(expiresAt);
        }
    }

    public static void main(String[] args) {
        // HexFormat:バイト列と 16 進文字列の相互変換(これまでは自作するか外部ライブラリが必要だった)
        byte[] bytes = "Java17".getBytes(StandardCharsets.UTF_8);
        HexFormat hex = HexFormat.of();
        String encoded = hex.formatHex(bytes);
        System.out.println("formatHex         = " + encoded);
        System.out.println("parseHex          = " + new String(hex.parseHex(encoded), StandardCharsets.UTF_8));
        System.out.println("MAC style         = " + HexFormat.ofDelimiter(":").withUpperCase().formatHex(bytes));
        System.out.println("toHexDigits(255)  = " + hex.toHexDigits((byte) 255));

        // テストでは時刻を固定した InstantSource を渡せる
        Instant t0 = Instant.parse("2026-08-01T00:00:00Z");
        Token token = new Token("abc", t0.plus(Duration.ofMinutes(10)));
        System.out.println();
        System.out.println("expired at +5 min  = " + token.isExpired(InstantSource.fixed(t0.plusSeconds(300))));
        System.out.println("expired at +15 min = " + token.isExpired(InstantSource.fixed(t0.plusSeconds(900))));
        System.out.println("Clock is an InstantSource = " + (Clock.systemUTC() instanceof InstantSource));
    }
}
formatHex         = 4a6176613137
parseHex          = Java17
MAC style         = 4A:61:76:61:31:37
toHexDigits(255)  = ff

expired at +5 min  = false
expired at +15 min = true
Clock is an InstantSource = true

Instant.now() を直接呼ぶ代わりに InstantSource を受け取るようにしておくと、テストで時刻を固定できる。

まとめ:Java 17 へ移行するときのチェックポイント

最後に、Java 11 から 17 への移行という観点で整理する。

区分機能判断
すぐ使えるrecord、テキストブロック、switch 式、instanceof のパターン、sealed クラス、HexFormat正式機能。新しく書くコードから積極的に使う
移行時に確認強いカプセル化(403)InaccessibleObjectException が出たら、まずライブラリを更新する
移行時に確認Security Manager・RMI Activation・AOT の削除や非推奨使っていないか検索する。使っていれば代替を検討する
置き換えを検討乱数 API(356)、デシリアライズフィルター(415)複数スレッドで共有している Random や、ObjectInputStream を使っている箇所を見直す
様子見switch のパターン(406)、FFM API、Vector APIJava 17 ではプレビュー・インキュベーター。本番で使うなら Java 21 以降で

Java 17 は、個々の JEP よりも Java 11 からの 6 年分の積み重ね に価値がある LTS である。record・sealed・パターンマッチングによって、データを型で表現し、型ごとに安全に処理を分けるスタイルが書けるようになった。一方で、強いカプセル化のように移行の障害になりうる変更もあるので、まず依存ライブラリを最新にしてから移行するのがよい。

参考資料

© 2020 - 2026 issyuu @Lakeheart Retreat
Powered by theme astro-koharu · Inspired by Shoka
読み込み中...