Java 17が2021/9/14にリリースされました。
機能的に多くのプログラマに関係ありそうな変更はApple Siliconに対応したくらいですが、LTSであるということが大きいと思います。
詳細はこちら
Oracle Releases Java 17
Java SE 17 Platform JSR 392
JDK 17 GA Release
APIドキュメントはこちら
Overview (Java SE 17)
今回はLTSなので、前回LTSであるJava 11からの差分もまとめられています。
JEPの差分はこちら。
https://openjdk.java.net/projects/jdk/17/jeps-since-jdk-11
APIの差分はこちら。
https://docs.oracle.com/en/java/javase/17/docs/api/new-list.html
MacやLinuxでのインストールにはSDKMAN!をお勧めします
Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。(まだ用意されていないものもあります)
- Oracle JDK ※対外サーバーの運用には有償ライセンスが必要です
- Adoptium
- Azul Zulu
- Liberica JDK
- Amazon Corretto 17
- Microsoft Build of OpenJDK
アップデートは10月に17.0.1が、翌年1月に17.0.2がリリースされることになります。
Oracle JDKも「Oracle No-Fee Terms and Conditions License」で無償での商用利用ができるようになっています。ただし対外サーバーの運用には有償ライセンスが必要です
次のLTSは2年後の21になる模様。
https://mreinhold.org/blog/forward-even-faster
JEP
大きめの変更はJEPでまとまっています。
https://openjdk.java.net/projects/jdk/17/
今回は14個のJEPが取り込まれました。すでにプレビューなどで出ていたものが3つあり、新たに取り込まれたものは11です。非推奨・削除が4つあるので、新機能としては7つですね。
JEP 306: Restore Always-Strict Floating-Point Semantics
JEP 356: Enhanced Pseudo-Random Number Generators
JEP 382: New macOS Rendering Pipeline
JEP 391: macOS/AArch64 Port
JEP 398: Deprecate the Applet API for Removal
JEP 403: Strongly Encapsulate JDK Internals
JEP 406: Pattern Matching for switch (Preview)
JEP 407: Remove RMI Activation
JEP 409: Sealed Classes
JEP 410: Remove the Experimental AOT and JIT Compiler
JEP 411: Deprecate the Security Manager for Removal
JEP 412: Foreign Function & Memory API (Incubator)
JEP 414: Vector API (Second Incubator)
JEP 415: Context-Specific Deserialization Filters
言語機能
言語仕様にかかわる変更としては次のようなものがあります。
JEP 306: Restore Always-Strict Floating-Point Semantics
JEP 406: Pattern Matching for switch (Preview)
JEP 409: Sealed Classes
JEP 306: Restore Always-Strict Floating-Point Semantics
常に厳密な浮動小数点演算をする。strictfpをつけるのと同じ動作になる。
もともと厳密な浮動小数点演算は負荷が高いからCPU任せをデフォルトにして厳密に演算したい場合はstrictfpをつけるというふうにJava 1.2でなったけど、もうSSE2で対応できて負荷もなくなったので常にstrictfpでやろう、ということらしい。
JEP 406: Pattern Matching for switch (Preview)
Switch文・式でパターンマッチングが使えます。
Object o;
System.out.println(switch (o) {
case String s -> " %s ".formatted(s);
case Integer i -> "%,d".formatted(i);
default -> o.toString();
}
ガード節もあります。
Object o;
System.out.println(switch (o) {
case String s && s.length() >= 5 -> s.toUpperCase();
case String s -> " %s ".formatted(s);
case Integer i -> "%,d".formatted(i);
default -> o.toString();
}
switchにnullを渡すとjavacが落ちます。
public class def {
public static void main(String[] args) {
switch (null) {
default:
}
}
}
6月に報告されてそのままリリースされてますね。
https://bugs.openjdk.java.net/browse/JDK-8269113
JEP 409: Sealed Classes
Sealedクラスは、継承できるクラスを限定する機能です。仕様の名前は「クラス」 となってますが、interfaceでも使えます。
クラス名のあとに permits
で継承クラスを指定していくという感じになります。
public abstract sealed class Shape
permits Circle, Rectangle, Square {...}
そうするとShapeクラスを継承できるのはCircle、Rectangle、Squareに限定されます。Circle、Rectangle、Squareは必ず定義する必要があります。
これらのクラスは、モジュール化しているのであれば同じモジュール内、モジュール化していないのであれば同じパッケージ内である必要があります。
また、permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できます。
permitsするクラスは定義のときにsealedかnon-sealedかfinalをつける必要があります。recordは暗黙的にfinalになるのでなにもつける必要はありません。
現状ではほとんど使いどころがないです。
例えば、Shapeの継承先は確定しているので、次のコードはコンパイル通ってほしいところですが、エラーになります。
String getName(Shape s) {
if (s instanceof Circle) {
return "円";
} else if (s instanceof Rectangle) {
return "四角";
} else if (s instanceof Square) {
return "正方形";
}
}
まだプレビューですがPattern matching for switchでswitch式/文でパターンマッチを使うときには、次のようにdefaultなしで書ける予定です。
String getName(Shape s) {
switch (s) {
case Circle c -> return "円";
case Rectangle r -> return "四角";
case Square q -> return "正方形";
}
}
もちろん、ここでswitch式を使うこともできますね。
String getName(Shape s) {
return switch (s) {
case Circle c -> "円";
case Rectangle r -> "四角";
case Square q -> "正方形";
}
}
ということで、現時点でのSealedクラスは今後の拡張のための下準備というところです。
背景にある考え方は、次の記事で言語設計者であるBrian Goetzが解説しています。
Java 注目の機能:Sealed クラス
API
APIの変更に関するJEPは次のようなものがあります。
JEP 290: Filter Incoming Serialization Data
JEP 356: Enhanced Pseudo-Random Number Generators
JEP 382: New macOS Rendering Pipeline
JEP 398: Deprecate the Applet API for Removal
JEP 403: Strongly Encapsulate JDK Internals
JEP 407: Remove RMI Activation
JEP 411: Deprecate the Security Manager for Removal
JEP 412: Foreign Function & Memory API (Incubator)
JEP 414: Vector API (Second Incubator)
JEP 415: Context-Specific Deserialization Filters
APIの変更はこちらにまとまっています。
http://cr.openjdk.java.net/~iris/se/17/build/latest/diffsFrom16%2b36/
JEP 356: Enhanced Pseudo-Random Number Generators
疑似乱数の改善
java.util.randomパッケージが作られて乱数発生まわりがまとめられています。
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html
JEP 382: New macOS Rendering Pipeline
レンダリングにMetalフレームワークを使う
JEP 398: Deprecate the Applet API for Removal
Applet使わんやろってことでDeprecatedに
JEP 403: Strongly Encapsulate JDK Internals
デフォルトになっていた。強制になる。
JEP 396: Strongly Encapsulate JDK Internals by Default
sun.misc.Unsafeなど以外の内部APIの隠蔽をデフォルトにします。
内部APIから標準APIへの移行がやりやすいようにします。
Java 9で内部APIを隠蔽する仕組みが導入されていてJava 16でデフォルト有効になりましたが、今回は--illegal-accessが削除されて強制になりました。
--add-opens
で利用するクラスを明示する必要があります。
制限される内部APIの一覧はこちらです。
https://cr.openjdk.java.net/~mr/jigsaw/jdk8-packages-denied-by-default
jshell> A.class.getClassLoader().loadClass("com.sun.beans.finder.ClassFinder").getMethod("findClass", String.class).invoke("test")
| 例外java.lang.IllegalAccessException: class REPL.$JShell$15 cannot access class com.sun.beans.finder.ClassFinder (in module java.desktop) because
module java.desktop does not export com.sun.beans.finder to unnamed module @77556fd
| at Reflection.newIllegalAccessException (Reflection.java:385)
| at AccessibleObject.checkAccess (AccessibleObject.java:687)
| at Method.invoke (Method.java:559)
| at (#5:1)
JEP 407: Remove RMI Activation
Java 1.2で導入されたRMI Activationですが、時代遅れってことでJava 15でDeprecatedになり、今回削除されました。
※ RMI自体は消えません
rmidというデーモンにRMIサービスを登録して、必要に応じてリモートオブジェクトを起動するという仕組みでした。Java 8からは必須ではなくなっています。rmidも削除されました。
JEP 411: Deprecate the Security Manager for Removal
アプレットを動かすときのサンドボックスなど、リモートからやってきたコードを安全に動かす仕組みをもってたけど、サーバサイドでは使われてないしアプレットなくなったらいらんやろってことでdeprecated
JEP 412: Foreign Function & Memory API (Incubator)
変更点
ネイティブライブラリの呼び出しを行う
外部メモリのアクセスにはForeign Memory Access APIを使う
JNIの代替
MemorySegmentとMemoryAddressの明確な分離
MemoryAccessインタフェースの導入。VarHandle APIの利用を最小限にする
共有セグメントのサポート
Cleanerつきセグメント
ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。
ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入されたわけです。そしてJava 15でセカンドインキュベータになっています。
次のようなコードになります。
import jdk.incubator.foreign.*;
import java.nio.ByteOrder;
VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
for (int i = 0 ; i < 25 ; i++) {
intHandle.set(seg, i * 4, i);
}
}
コンパイル、実行では--add-modules jdk.incubator.foreign
を付けてモジュールを読み込む必要があります。
パッケージもモジュール名と同様、jdk.incubator.foreign
になっています。
MemoryHandlesからwithOffsetとwithStrideが消えましたね。
jshell> import jdk.incubator.foreign.*
jshell> MemoryHandles.
asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates(
filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle(
Java 15ではMemoryHandlesからVarHandleを得るメソッドが拡充してました。
jshell> MemoryHandles.
asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates(
filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle(
withOffset( withStride(
Java 14では次の3つのメソッドしかありませんでした。
jshell> MemoryHandles.
class varHandle( withOffset( withStride(
Java 16ではMemoryAccessというユーティリティが用意されて、VarHandleを使わずにシンプルにかけるようになっています。
import jdk.incubator.foreign.*;
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
for (int i = 0 ; i < 25 ; i++) {
MemoryAccess.setIntAtOffset(i * 4, i);
}
}
JEP 414: Vector API (Second Incubator)
AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。
Project Panamaのひとつとして開発されていました。
Java 16でインキュベータとして導入されました。
使うためには実行時やコンパイル時に--add-modules jdk.incubator.vector
をつける必要があります。
import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
void vectorComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8
VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる
// a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....]
// FloatVector va, vb, vc;
FloatVector va = FloatVector.fromArray(SPECIES, a, i, m);
FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m);
FloatVector vc = va.mul(va).
add(vb.mul(vb)).
neg();
vc.intoArray(c, i, m);
}
}
利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。
型 | bit幅 | Vector |
---|---|---|
byte | 8 | ByteVector |
short | 16 | ShortVector |
int | 32 | IntVector |
long | 64 | LongVector |
float | 32 | FloatVector |
double | 64 | DoubleVector |
ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。
jshell> FloatVector.SP
SPECIES_128 SPECIES_256 SPECIES_512 SPECIES_64 SPECIES_MAX SPECIES_PREFERRED
MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。
jshell> FloatVector.SPECIES_PREFERRED
$11 ==> Species[float, 8, S_256_BIT]
ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。
実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。
Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。
jshell> va.
abs() add( addIndex( bitSize() blend(
broadcast( byteSize() compare( div( elementSize()
elementType() eq( equals( fma( getClass()
hashCode() intoArray( intoByteArray( intoByteBuffer( lane(
lanewise( length() lt( max( min(
mul( neg() notify() notifyAll() pow(
rearrange( reduceLanes( reduceLanesToLong( reinterpretAsBytes() reinterpretShape(
selectFrom( shape() slice( sqrt() sub(
test( toArray() toDoubleArray() toIntArray() toLongArray()
toShuffle() toString() unslice( viewAsFloatingLanes() viewAsIntegralLanes()
wait( withLane(
ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。
JEP 415: Context-Specific Deserialization Filters
デシリアライズのフィルターをプログラミング可能にする。
Hex formetter / parser
16進数のフォーマット、パース
C:\Users\naoki>jshell
| JShellへようこそ -- バージョン17
| 概要については、次を入力してください: /help intro
jshell> HexFormat.of().toHexDigits(12345678)
$1 ==> "00bc614e"
jshell> HexFormat.of().withUpperCase().toHexDigits(12345678)
$2 ==> "00BC614E"
jshell> HexFormat.of().fromHexDigits("00bc614e")
$3 ==> 12345678
jshell>
http://cr.openjdk.java.net/~rriggs/hex-formatter/java.base/java/util/Hex.html
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-August/068375.html
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-August/068256.html
JVM
JVMに関する変更はGraal削除の1件です。
JEP 410: Remove the Experimental AOT and JIT Compiler
JEP 410: Remove the Experimental AOT and JIT Compiler
Java 16ではOracle OpenJDKなどでビルドに含まれていなかったGraalですが、だれも文句いわなかったということで正式に削除されました。
https://bugs.openjdk.java.net/browse/JDK-8255616
ZGCの変更
JEPになってませんが、ZGCも改善されています。
https://malloc.se/blog/zgc-jdk17
OpenJDK
OpenJDKに関する変更は、Apple Silicon対応の1件です。
JEP 391: macOS/AArch64 Port
JEP 391: macOS/AArch64 Port
M1などApple Siliconに対応しました。