はじめに
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() を追加 |
| 382 | macOS の新しい描画パイプライン | 追加(既定は OpenGL) | JDK 19 で Metal が既定に |
| 391 | macOS/AArch64 ポート | 正式 | ― |
| 398 | Applet API の削除予定化 | 削除予定 | JDK 26 で削除 |
| 403 | JDK 内部の強いカプセル化 | 正式 | sun.misc.Unsafe のメモリアクセスも段階的に制限 |
| 406 | Pattern Matching for switch | プレビュー | JDK 21 で正式化 |
| 407 | RMI Activation の削除 | 削除 | ― |
| 409 | Sealed Classes | 正式 | ― |
| 410 | 実験的な AOT / JIT コンパイラの削除 | 削除 | AOT は Project Leyden として別の形で復活 |
| 411 | Security Manager の削除予定化 | 削除予定 | JDK 24 で恒久的に無効化 |
| 412 | Foreign Function & Memory API | インキュベーター | JDK 22 で正式化 |
| 414 | Vector API | 2nd インキュベーター | 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 12 | Collectors.teeing、String.indent / transform |
| Java 14 | switch 式(JEP 361)、わかりやすい NullPointerException(JEP 358) |
| Java 15 | テキストブロック(JEP 378)、ZGC・Shenandoah の正式化 |
| Java 16 | record(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 つある。
- 型パターン:
case Integer iのように、型の判定と変数への代入を同時に行う。if-else を連ねるより読みやすく、コンパイラが最適化しやすい。 case null:これまでの switch は、nullを渡すと必ず NullPointerException を投げた。case nullを書けば、nullも 1 つの場合として扱える。書かなければ、従来どおり例外になる。- 網羅性のチェック:
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.30000000000000004strictfp は付けても意味がなくなったので、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 ms8 スレッド × 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 freedJNI なら、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 ms100 万要素の内積 × 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: trueApplet 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 XmacOS の新しい描画パイプライン(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 = trueInstant.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 API | Java 17 ではプレビュー・インキュベーター。本番で使うなら Java 21 以降で |
Java 17 は、個々の JEP よりも Java 11 からの 6 年分の積み重ね に価値がある LTS である。record・sealed・パターンマッチングによって、データを型で表現し、型ごとに安全に処理を分けるスタイルが書けるようになった。一方で、強いカプセル化のように移行の障害になりうる変更もあるので、まず依存ライブラリを最新にしてから移行するのがよい。
参考資料
- JDK 17 プロジェクトページ
- JEP 409: Sealed Classes
- JEP 406: Pattern Matching for switch (Preview) / JEP 441: Pattern Matching for switch
- JEP 356: Enhanced Pseudo-Random Number Generators
- JEP 403: Strongly Encapsulate JDK Internals
- JEP 415: Context-Specific Deserialization Filters
- JEP 412: Foreign Function & Memory API (Incubator) / JEP 454: Foreign Function & Memory API
- JEP 414: Vector API (Second Incubator)
- JEP 411: Deprecate the Security Manager for Removal / JEP 486: Permanently Disable the Security Manager
- JEP 504: Remove the Applet API
- JEP 382: New macOS Rendering Pipeline
- Java 2D のシステムプロパティ(Oracle)