Java 14が2020/3/17にリリースされました。
Java SE 14 Platform JSR 389
ダウンロード
OpenJDKサイトからダウンロードできます。
https://jdk.java.net/14/
MacやLinuxでのインストールにはSDKMAN!をお勧めします
Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。
LTSではないのでAmazon Correttoではリリースされないようです。
アップデートは4月に14.0.1が、7月に14.0.2がリリースされることになります。
Oracle JDKは開発用途には利用できますが、商用利用にはJava SE Subscriptionを購入する必要があります。
JEP
大きめの変更はJEPでまとまっています。
https://openjdk.java.net/projects/jdk/14/
今回は、16ものJEPが取り込まれました。影響が大きいものも多いです。
305: Pattern Matching for instanceof (Preview)
343: Packaging Tool (Incubator)
345: NUMA-Aware Memory Allocation for G1
349: JFR Event Streaming
352: Non-Volatile Mapped Byte Buffers
358: Helpful NullPointerExceptions
359: Records (Preview)
361: Switch Expressions (Standard)
362: Deprecate the Solaris and SPARC Ports
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination
367: Remove the Pack200 Tools and API
368: Text Blocks (Second Preview)
370: Foreign-Memory Access API (Incubator)
分野ごとにまとめていきます。
言語仕様
言語仕様にかかわる変更としては次のようなものがあります。
359: Records (Preview)
305: Pattern Matching for instanceof (Preview)
368: Text Blocks (Second Preview)
361: Switch Expressions (Standard)
359: Records (Preview)
データ保持用のクラスとしてrecordがpreview機能として入りました。
Java 15でフィードバックを反映した改善版がpreviewされてJava 16で正式化という流れになりそうです。
recordとして定義します。
record Foo(int x, int y) {}
他のクラスは継承できません。
record レコード名(状態) { 定義 }
これは次のようなクラスになります。
class Foo extends Record {
// 状態がprivate finalとして定義
private final int x;
private final int y;
// 追加のインスタンスフィールドは定義できない
// 状態をフィールドに設定するコンストラクタが定義される
public Foo(int x, int y) {
this.x = x;
this.y = y;
}
// 状態と同じ名前のメソッドが定義される
public int x() {
return x;
}
public int y() {
return y;
}
// 状態を反映するhashCodeが定義される
public int hashCode() { ... }
// 状態を比較するequals が定義される
public boolean equals() { ... }
// 状態を表示するtoStringが定義される
public String toString() { ... }
}
取得用メソッドは定義されますが、値設定用のメソッドは定義されません。つまり、イミュータブルなオブジェクトとなります。また、get/setではないことからJava Beanとの互換性もありません。
hashCode()
メソッドやequals()
メソッドなどは実際にはinvokeDynamicで実行時に実装コードが生成されます。
次のように、状態の検査や正規化にコンストラクタが定義できます。
record Range(int lo, int hi) {
public Range {
if (lo > hi) throw IllegalArgumentException();
// 設定されなかったフィールドはあとで設定される
}
}
staticではないinner classの中でrecordを定義することはできません。
次のようなコードをコンパイルするとrecord declarations not allowed in inner classes
というエラーになります。
public class NestedRecord {
class Foo {
record Bar(int x){}
}
}
次のようなコードはコンパイルできます。
public class NestedRecord {
static class Foo {
record Bar(int x){}
}
}
APIの拡張
recordはjava.lang.Record
を継承したクラスになります。
次のようにRecordを継承するコードを書こうとするとrecords cannot directly extend java.lang.Record
というエラーになります。
class Foo extends Record {
}
Class
クラスにはレコード関連のメソッドが追加されています。
isRecord
メソッドで型がrecordかどうか判定できます。またgetRecordComponents
でrecordで定義されたコンポーネントを取得することができます。ただし、値の取得はRecordComponent経由では行えないので、Field経由で行うようです。
jshell> Foo.class.isRecord()
$9 ==> true
jshell> Foo.class.getRecordComponents()
$10 ==> RecordComponent[2] { int x, int y }
jshell> String.class.isRecord()
$11 ==> false
jshell> String.class.getRecordComponents()
$12 ==> null
型名としてのrecord
の制限
record
という名前を型(クラス・インタフェース・レコード型など)につけることは制限されています。
--enable-preview
を付けた状態ではエラーになります。
$ jshell --enable-preview
| JShellへようこそ -- バージョン14-ea
| 概要については、次を入力してください: /help intro
jshell> class record{}
| エラー:
| ここでは'record'は許可されません
| リリース13から'record'は制限された型名であり、型の宣言に使用できません
| class record{}
| ^
jshell> String record=""
record ==> ""
jshell> record Rec(int record){}
jshell> Rec record=new Rec(3)
record ==> Rec[record=3]
ちなみにこの「リリース13から」というのは誤りで、ただしくは14からです。
http://mail.openjdk.java.net/pipermail/amber-dev/2020-February/005623.html
enumと違いキーワードではないので、変数名やフィールド、recordのコンポーネント名にはrecordを使えます。
--enable-preview
を付けない状態では、警告が表示されます。
$ jshell
| JShellへようこそ -- バージョン14-ea
| 概要については、次を入力してください: /help intro
jshell> class record{}
| 警告:
| 'record'は将来のリリースで制限された型名になる可能性があり、型の宣言での使用、または配列の要素タイプとしての使用はできなくなる可能性があります
| class record{}
| ^
| 次を作成しました: クラス record
今後の改善
当初はSealed Typesと同じJEPでしたが分離されました。
クラスの継承を限られたクラスに制限できる機能です。Java 15でpreviewに入ると思われます。
JEP 360: Sealed Types (Preview)
Preview 2での改善点はこちらで提案されています。
https://mail.openjdk.java.net/pipermail/amber-spec-experts/2020-January/001913.html
-
java.util.Record
はクラスですが、Valhallaのinline classはinterfaceしか継承できないので、これをinterfaceにしたほうがいいかどうか。 - 必須メンバーへの可視性(private recordのメンバがpublicなのはおかしい?)
- 現状ではstaticではないinner classではrecordを入れ子にすることができないけど対応したい
- abstract record
- パターンマッチでのdeconstructionへの対応
- recordコンポーネントへの
@Deprecated
参考
Stephen Colebourneさんからのフィードバック
https://mail.openjdk.java.net/pipermail/amber-dev/2019-November/005271.html
305: Pattern Matching for instanceof (Preview)
パターンマッチングです。
まずはinstanceofを使ったパターンマッチが14にプレビューとして入ります。
http://openjdk.java.net/jeps/305
値 instanceof パターン
で、値をマッチさせることができます。
パターンは、定数か変数定義です。変数定義の場合には、型が一致していた場合にtrueになりその変数に値が割り当てられます。
if (x instanceof Integer i) {
// can use i here
}
switchで使えるようになれば便利ですが、これは別のJEPで定義されていて、Java 15に持ち越されています。
JEP draft: Pattern matching for switch (Preview)
また、recordを分解する機能(deconstruction)やその入れ子が導入されてPreview 2としてJava 15に入るようです。
http://openjdk.java.net/jeps/8235186
妥当なスケジュールとしては
- 14 Pattern Matching for instanceof(Preview)
- 15 Pattern Matching for instanceof(Preview 2)
Pattern matching for a switch (Preview) - 16 Pattern Matching for instanceof(Standard)
Pattern matching for a switch (Preview 2) - 17LTS Pattern matching for a switch(Standard)
となるか
- 14 Pattern Matching for instanceof(Preview)
- 15 Pattern Matching for instanceof(Preview 2)
Pattern matching for a switch (Preview) - 16 Pattern Matching for instanceof(Preview 3)
Pattern matching for a switch (Preview 2) - 17LTS Pattern Matching for instanceof(Standard)
Pattern matching for a switch(Standard)
となるかですが、17LTSにはパターンマッチングの機能がひととおり標準機能として入る目途がついてきました。
368: Text Blocks (Second Preview)
改行などを含んだ文字列を定義できます。"""
で囲みます。
JDK13にPreviewとして入ったものにJDK14では改行のエスケープなど少し仕様変更が入りました。JDK15でstandardになる予定。
// same as "You can write\ntwo line string.\n"
var str = """
You can write
two line string.
""";
開始の"""
のあとには文字列を続けれません。また、インデントは"""
や内部の文字列で一番浅いところが基準になります。
var str = """
..You can write
..two line string.
""";
var str = """
.. You can write
.. two line string.
""";
改行をエスケープすることもできます。
var str = """
You can write \
two line string, \
but this is single.
""";
これは"You can write two line string, but this is single."
になります。
行末のスペースは削除されます。
そこで、行末にスペースが必要なときは\s
を入れてスペースが必要なことを示します。
var str = """
test\s
test \s
""";
これは"test_\ntest__\n"
になります。(Qiitaでは複数スペースをいれてもスペースひとつになってしまう)
文字列への変数の埋め込みはできません。その代わりにformatted
メソッドがインスタンスメソッドとして用意されて、次のように書けるようになりました。
var str = """
こんにちは、%sさん。
今日はいい天気ですね。
""".formatted("きしだ");
361: Switch Expressions (Standard)
いままでステートメントであったswitch
を式として使えるようになります。Java 12でプレビューとして導入され、Java 13で仕様変更、そしてJava 14で正式機能として導入されます。
https://openjdk.java.net/jeps/361
switch
はステートメントでしたが、多くのswitchで同一の変数に値を割り当てたりすべてのcaseでreturnしたりといった使いかたがされていたため、効率的に書けるように式としても使えるようになります。
つまり、こう。
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
yield
で値を返すということもできます。
int result = switch (s) {
case "Foo":
yield 1;
case "Bar":
yield 2;
default:
System.out.println("Neither Foo nor Bar, hmmm...");
yield 3;
}
基本的な形はyield
で値を返すものです。
case LABEL: yield expression;
そのシンタックスシュガーとしてラムダっぽく書けるということのようです。
case LABEL -> expression;
また、case
に複数の値を指定できるようにもなります。
switch (day) {
case MONDAY, FRIDAY, SUNDAY:
numLetters = 6;
break;
...
};
このようなcase
の拡張は、既存のswitch
ステートメントでも有効です。
JVM
JVMの挙動変更としては、NullPointerException
での詳細メッセージがあります。
358: Helpful NullPointerExceptions
358: Helpful NullPointerExceptions
JavaプログラマのみなさんはNullPointerException
が大好きだと思います。
しかし、メッセージがそっけないという悩みがありました。
Java 14では、NullPointerException
について詳細なメッセージを表示できるようになりました。
例えば次のようなコードがあるとします。
public class Sample {
static String s;
public static void main(String... args) {
s.length();
}
}
Java 14でも普通に実行すると以前のバージョンと同様に特別なメッセージは表示されません。
$ java Sample.java
Exception in thread "main" java.lang.NullPointerException
at Sample.main(mysample.java:4)
-XX:+ShowCodeDetailsInExceptionMessages
オプションをつけて実行すると次のように何に対して何を呼び出したときにエラーになったかということが表示されます。
$ java -XX:+ShowCodeDetailsInExceptionMessages Sample.java
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "Sample.s" is null
at Sample.main(mysample.java:4)
このメッセージ構築はメッセージ取得時に行われるので、パフォーマンスへの影響はほとんどないようです。
JShellの場合は-R-XX:+ShowCodeDetailsInExceptionMessages
をつけます。
$ jshell -R-XX:+ShowCodeDetailsInExceptionMessages
| JShellへようこそ -- バージョン14-ea
| 概要については、次を入力してください: /help intro
jshell> String[] strs = {null}
strs ==> String[1] { null }
jshell> strs[0].toUpperCase()
| 例外java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "REPL.$JShell$11.strs[0]" is null
| at (#2:1)
API
APIに関する変更は次の3つのJEPです。
349: JFR Event Streaming
370: Foreign-Memory Access API (Incubator)
352: Non-Volatile Mapped Byte Buffers
この他にCompactNumberFormat
での複数形対応とStrictMathでの便利メソッドの追加がありました。
また、あとでツールの項で取り上げるPackagingツール関連のAPIがIncubatorとして追加されたのと、Pac200関連のAPIが削除されています。
APIの相違はこちらでまとまっています。
DRAFT: API Differences Between Java SE 13 (build 33) & Java SE 14 (build 36)
349: JFR Event Streaming
Flight Recorderはメトリクス収集ツールで、障害発生時の解析に役立ちますが、ダンプファイルの解析が必要で、モニタリング用途には不便でした。
JFR Event Streamingでは、イベント登録ができるなど、モニタリングに使いやすい機能が追加されています。
JEPのサンプルではイベントの登録の例が載っていますが、これにGCログの出力をつけくわえてみました。
import java.io.IOException;
import java.time.Duration;
import jdk.jfr.consumer.RecordingStream;
public class JFRStreamTest {
public static void main(String[] args) throws IOException {
try (var rs = new RecordingStream()) {
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
rs.onEvent("jdk.CPULoad", event -> {
System.out.println(event.getFloat("machineTotal"));
});
rs.onEvent("jdk.JavaMonitorEnter", event -> {
System.out.println(event.getClass("monitorClass"));
});
rs.onEvent("jdk.GarbageCollection", System.out::println);
rs.start();
}
}
}
-XX:StartFilghtRecording
をつけて実行すると、Flight Recorderが起動して、定期的にメトリクスが表示されます。
$ java -XX:StartFlightRecording JFRStreamTest.java
Started recording 1. No limit specified, using maxsize=250MB as default.
Use jcmd 79660 JFR.dump name=1 filename=FILEPATH to copy recording data to file.
[1.715s][warning][os] Unable to resolve PDH index: (230)
[1.716s][warning][os] Please check the registry if this performance object/counter is disabled
{
classLoader = null name = "jdk/jfr/internal/PlatformRecorder"
package = {
name = "jdk/jfr/internal"
module = {
name = "jdk.jfr"
version = "14-ea"
location = "jrt:/jdk.jfr"
classLoader = null }
exported = true
}
modifiers = 49
}
jdk.GarbageCollection {
startTime = 13:51:48.973
duration = 12.5 ms
gcId = 1
name = "G1New"
cause = "G1 Evacuation Pause"
sumOfPauses = 12.5 ms
longestPause = 12.5 ms
}
JDK Mission Control(JMC)で見ることができます。
JFRを動かしておくと、イベントブラウザにイベントが記録されているのがわかります。
370: Foreign-Memory Access API (Incubator)
ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。
ということで、ヒープ外のメモリを直接扱うAPIが導入されたわけです。
次のようなコードになります。
VarHandle intHandle = MemoryHandles.varHandle(int.class);
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAddress base = segment.baseAddress();
for (int i = 0 ; i < 25 ; i++) {
intHandle.set(base.offset(i * 4), i);
}
}
352: Non-Volatile Mapped Byte Buffers
ByteBufferを不揮発メモリに対応します。
jdk.nio.mapmode
といモジュールが導入されて、同名のパッケージにExtendedMapMode
クラスが用意され、新しいMapModeとして
READ_ONLY_SYNC
READ_WRITE_SYNC
が追加されます。
var fc = FileChannel.open(path);
fc.map(ExtendedMapMode.READ_WRITE_SYNC, 0, 1024);
などとする感じです。デバイスが不揮発メモリではない場合はUnsupportedOperationExceptionが投げられます。
また、BufferPoolMXBeanに永続MappedByteBufferの統計情報がmapped - 'non-volatile memory'
という名前で取れるようになるようです。
@PreviewFeature
String.formattedのようなプレビュー機能のために導入APIには、13ではDeprecatedがついていました。
14ではinternalなアノテーションとして@PreviewFeature
が導入されて、プレビュー機能用のAPIであることが明示されました。
13では--enable-preview
がなくても使えましたが、14では--enable-preview
の指定がないときにこれらのAPIを使うと
formatted(java.lang.Object...)はプレビュー機能の一部であるAPIです
のようなエラーが出ます。
シリアライズ関連メソッド・フィールドを@Serialで明示
シリアライズできるクラスには関連フィールドやメソッドを定義しますが、シグネチャが決まっているもののコンパイル時にチェックされず、正しく定義されているかどうか確認できませんでした。
private static final long serialVersionUID
https://download.java.net/java/GA/jdk14/docs/api/java.base/java/io/Serial.html
https://bugs.openjdk.java.net/browse/JDK-8217698
CompactNumberFormatの複数形対応
JEPになってない変更のひとつ。
ドイツ語とかイタリア語の場合に100万と200万でMillionかMillionenみたいな変形があって、それに対応したということらしい。
jshell> import java.text.*
jshell> var cnf = CompactNumberFormat.getCompactNumberInstance(Locale.GERMAN, NumberFormat.Style.LONG)
cnf ==> java.text.CompactNumberFormat@5088aaba
jshell> cnf.format(1_000_000)
$3 ==> "1 Million"
jshell> cnf.format(2_000_000)
$4 ==> "2 Millionen"
UnicodeのLanguage Plural Rulesに従って独自のルールを設定することができるコンストラクタも用意されています。
StrictMathへの便利メソッド追加
StrictMathに便利メソッドが追加されています。メソッド参照が使いやすくなることを狙ったんでしょうか。
- incrementExact(int)
- incrementExact(long)
- decrementExact(int)
- decrementExact(long)
- negateExact(int)
- negateExact(long)
インクリメント、デクリメント、符号反転ですが、オーバーフローしたときに
エラーがでます。
jshell> StrictMath.incrementExact(3)
$5 ==> 4
jshell> StrictMath.incrementExact(Integer.MAX_VALUE)
| 例外java.lang.ArithmeticException: integer overflow
| at Math.incrementExact (Math.java:968)
| at StrictMath.incrementExact (StrictMath.java:849)
| at (#4:1)
jshell> StrictMath.negateExact(Integer.MAX_VALUE)
$6 ==> -2147483647
jshell> StrictMath.negateExact(Integer.MIN_VALUE)
| 例外java.lang.ArithmeticException: integer overflow
| at Math.negateExact (Math.java:1044)
| at StrictMath.negateExact (StrictMath.java:909)
| at (#7:1)
通常の演算子ではオーバーフローがおきます。
jshell> Integer.MAX_VALUE+1
$8 ==> -2147483648
jshell> Integer.MIN_VALUE
$9 ==> -2147483648
jshell> -Integer.MIN_VALUE
$10 ==> -2147483648
ツール
ツールの変更としては、jpackageの追加が大きいですね。
343: Packaging Tool (Incubator)
367: Remove the Pack200 Tools and API
343: Packaging Tool (Incubator)
Javaアプリケーションのインストーラを作るツールが入りました。
関連APIも追加されていますが、Incubatorモジュールになっています。
Windowsではlight.exeとcandle.exeが必要なのでhttps://wixtoolset.org からダウンロードしてPATHに追加する必要があります。
試しに次のアプリケーションのインストーラを作ってみます。
import javax.swing.*;
public class App {
public static void main(String[] args) {
var f = new JFrame("My App");
var t = new JTextArea();
f.add(t);
var b = new JButton("Hello");
b.addActionListener(al -> t.append("Hello!\n"));
f.add("North", b);
f.setSize(500, 400);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
}
アプリケーションはJarファイルになっている必要があります。
$ javac App.java
$ mkdir target
$ jar cf target/app.jar App.class
$ jpackage --name myapp --input target --main-jar app.jar --main-class App
そうするとWindowsの場合はmyapp.exeという名前で45MBのインストーラが作成されます。
モジュラーJARの場合は自動的に必要なモジュールのみのJavaランタイムがインストールされるようになりますが、ここではモジュール対応しないJarファイルを作ったので、jlinkを使って最低限のJavaランタイムを作ると小さなインストーラが作れるようになります。
$ jdeps --list-deps App.class
java.base
java.desktop
$ jlink --add-modules java.base,java.desktop --output jre
$ jpackage --name myapp --input target --main-jar app.jar --main-class App --runtime-image jre
そうすると27MBまでインストーラのサイズが削減されました。
インストール後のアプリケーションサイズも124MBから74MBに減ります。
Windowsの場合は--win-menu
をつけてWindowsメニューにアプリが追加されるようにするほうがいいでしょう。
367: Remove the Pack200 Tools and API
Pack200はJarファイルを効率的に圧縮するツールでしたが、主なユースケースであるAppletが使われなくなり、役目を終わったということで削除されます。
java.util.jar
配下の関連APIも削除されます。
GC
GCに関する変更のJEPは5つありました。
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination
345: NUMA-Aware Memory Allocation for G1
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
Concurrent Mark & Sweep GCがソースコードから削除されます。
Java 9のJEP 291でDeprecatedになっていましたが、メンテナンスを引き継ぐような人も現れなかったので削除するということです。
そうすることで、GCのメンテナンスコストが下がり、他のGCの開発が速くなることが期待されています。
364: ZGC on macOS / 365: ZGC on Windows
いままではLinuxしかサポートしてなかったZGCがmacOSとWindowsをサポートするようになりました。
当初は、ZGCは本番のLinuxサーバーでだけ使われて、macOSやWindowsは開発用途に使うだけだという想定でしたが、IDEをZGCで動かしたいなどの要望もあったことからmacOSやWindowsもサポートすることになったようです。
WindowsはWindows 10 ver 1803以降に対応しています。
ZGCは仮想アドレスを使って物理メモリを複数のアドレスでアクセスする仕組みを使っていますが、Windowsでは1803からページングファイルメモリがサポートされたということでWindowsでもZGCが動かせるようになったようです。
366: Deprecate the ParallelScavenge + SerialOld GC Combination
Young領域のGCをPrallelでOld領域のGCをSerialでという、ほとんど使われてない割にメンテナンスが大変な組み合わせが非推奨になりました。
345: NUMA-Aware Memory Allocation for G1
最近は各コアからのメモリアクセスが均等というわけではないNUMA(Non-Uniform Memory Access)アーキテクチャが広まっています。
パラレルGCではNUMAに対応していましたが、G1は対応していませんでした。
このJEPによってG1もNUMAに対応しています。ただし対応OSはLinuxのみです。
JDK
JDK自体に関する変更というのは、リリース形態やビルド方針などに関するものです。
Java 14ではSolarisやSPARCへの対応がDeprecatedになりました。
362: Deprecate the Solaris and SPARC Ports
362: Deprecate the Solaris and SPARC Ports
Solaris/SPARCやSolaris/x64、Linux/SPARCへの対応を外していくというものです。
CMSの削除と同様に、もし、いっぱい需要があるということを示せれば、取り下げられます。
また、これはオープンソースとしてのOpenJDKの開発の話で、Oracle JDKなど商用ディストリビューションでの対応継続は、それぞれに続く可能性があります。Solaris/SPARCを導入してるところだとJavaのライセンス代もそれほど問題にならなそうだし。