Java14やJava15で利用可能な新機能と変更点

久保田祐史氏(以下、久保田):簡単に自己紹介から始めたいと思います。LINEでソフトウェアエンジニアとして働いている久保田祐史と言います。現在はKafkaのプラットフォームを提供しているチーム内で働いています。私はよくJJUG CCCという、国内のJavaのカンファレンスでGCやJVMのセッションの他に、専らJavaの新機能について紹介することが多いのですが、今日も同じようにJava14やJava15で利用可能な新機能と変更点を紹介していきたいと思います。

今回は主に新機能を中心に話すので、非互換性については今回お話しません。注意事項として、本資料はjdk.java.netからダウンロードできるJava12.0.1およびJDK15のbuild32で確認しています。ただし、すべての確認はしていないので、もしこの資料をベースに自分のコードを修正したいとの場合があれば、まずは動作確認を行ってください。JDKの実装自体は、hg.openjdkに入っているjdk-updates/jdk14uまたはjdk/jdkで確認しています。

さて、それでは実際に見ていきたいと思います。Java14と15では実際にこのような数だけ変更がありました。それぞれの意味は、まずJava Enhancement Proposals、通称JEPは、新機能レベルで変更が行われました。そしてもう1つのCSRは、非互換性を伴う変更です。合計で16の新機能が入って、155の非互換性が入ってきました。

今回は非互換性については扱わないので、このJEPにどういう新機能が入ってきたかを中心に説明していきたいと思います。ただし、このJEPやCSRでの変更は、メジャーバージョンでのアップデート時にしか行われない変更なのですが、最近だけ例外的なものがあって、JDK8でバックポートというかたちで新機能レベルの追加が、マイナーバージョンのアップデートで行われることもあります。

つい先日、Flight RecorderがJava8に導入されたのも、このバックポートの1つです。いずれにしろ、バージョンのアップデートが半年ごとになりましたが、アップデートのパーツは今でも変わらず多い状況になっています。というわけで、今回はJava14と15のこの新機能のレベルについて説明していきたいと思います。さすがに30個もあるので、一つひとつ分類しながら説明していきたいと思います。

パターンマッチング

まずは言語レベルの変更とAPIレベルの変更。一部は先ほどのきしださんの説明と被るので、そこは簡単に説明しながら、次々と説明していきたいと思います。この言語レベルの変更は、Java12も15も分類上はこれが一番多い変更になっています。ではここからは一つひとつ説明をしていきたいと思います。

まずパターンマッチングについて。このパターンマッチングは、Java14からPreview機能として入りました。なおここで言うパターンマッチングは、正規表現ではなくオブジェクトの型、つまりクラスです。今までは表示されているように、instanceofで確認を行った場合、これが既にDoubleのクラスであることがわかっているのに、わざわざこのようにキャストする必要がありました。

そしてキャストした上で、この例に関連する処理をここで行うかたちでした。今後は、Afterで表示されている通り、キャストする必要がなく、今後は直通でdを使って処理できます。このように簡単な変更なんですが、意外に細かい修正で、これによってコードがスッキリして読みやすさやメンテナンスのしやすさが想像以上に改善されました。

将来的には、このinstanceofたちではなく、switch文でも使えるようになっていきたいという展望がありますが、現在のところはドラフト状態で、まだいつ入るか、その内容もこういうかたちなのかはまだ決まっていません。将来的に入る可能性はありますが、少なくとも今のところJava16ですら入っていない状況です。ちなみにJava15でSecond Previewになりましたが、今回のこのタイミングでは何の修正も入っていません。

Recordのサポート

次に、Java言語で評判が悪かった、「ただデータを格納したいだけなのにコード量が多すぎる」に対する回答として、Record型がサポートされました。基本的な使い方は、型の名前とそこに含めるメンバーを選定するだけです。通常のクラスの選定と異なるのは、このclassの替わりにrecordを選定すること。そして型の名前を選定したあとに、引数を続けて選定する必要があります。

そしてこのように書くことによって、自動的にこちらのようなコードが生成されます。特徴としてはまず、ここのデータの内容は変更が行われません。つまりfinalとして定義される。これが1つ目です。そしてこの値をどのように取得するかというと、getterのメソッドが自動的に生成されるので、このgetterメソッドを通じて取得します。

そしてこのgetterは変数名と同じ名前になります。このデータに対する入力値はコンストラクタで検証できます。Record型のクラスと同じように、インスタンス化されるときはコンストラクタが呼び出されるので、このコンストラクタ内で入力値のチェックや変換などの処理をすことが可能です。

今回の例ではコンストラクタを書いていないので、一般的な代入を行うだけのコンストラクタが生成されていますが、こちらにコンストラクタ内で入力値をチェックしたり変換したりするコードを書けば、ここに同じようなかたちで出力されます。また、これはRecordタイプですが、インターフェースも同じように実装できます。ただし継承は不可能です。

ちなみに新しい型が追加されているように見えますが、実際のところ、バイトコードを覗くと実体は、java.lang.Recordを拡張したクラスであることがわかります。このクラスの中で、抽象メソッド、toString()、equals()、そしてhashCode()が定義されているため、コンパイルしたときに、自動的にこれらのメソッドも生成されます。

これによって、今まで「ただデータを格納したいだけなのにコード量が多すぎる」という問題に対して、究極的にはこのコードだけで実装ができるかたちになりました。なお、これはJava14から導入されましたが、Java15ではこの基本的な使い方は変更されていません。一般的なクラスにおけるローカルクラスに、ローカルレコード追加などの機能の変更が組み込まれています。

どういうことかというと、Recordを生成して表示するアプリケーションとして考えるパターンとして、あるクラスの中に単純な変数を複数もつ、内部の値を扱う可能性があります。そしてそういった内部の値を、今の実装ではどのように扱っているかというと、内部のHelperクラスで扱っている実装があります。

つまり、このHelperクラスに対する処理内容に関しても、Recordから抽出して定義したことで、ローカルクラスのローカルレコードという概念を新しく追加されました。このようなAPIの基本的なところは変わっていないんですが、「もう少し柔軟に使いたい」というフィードバックがあったので、そのフィードバックを受けて修正が行われました。

これは今のところJava15ではSecond Previewなので、次のJava16で正規版に上がる可能性があります。

Text Blocksの機能

続いてText Blocks。Text Blocks自体はJava13から導入されたもので、これがSecond Previewになりました。そしてJava15でStandardになりました。Second Previewの段階で何が変わったかというと、line breakそしてspaceを意味するescape文が追加されました。

これはどういうものかというと、基本的にtextblockは、書かれた通りに出力しますが、それだけだと、例えばこれがものすごい長文が書かれていた場合、コード上ではとても見にくくなります。それをもう少し区切って見やすくしたいというときに、このような改行を加えることによって、コードを上は見やすく整形できるような改善が行われました。ただ単純に、このような変更ですね。そしてJava15で標準機能に昇格した際には、とくに大きな変更は行われていません。

Javaヒープ外でのメモリを扱うための新たな方法

続きまして、Javaアプリケーションで使用するメモリは、GCによってJavaヒープに管理されています。しかしながら、アプリケーションによってはJavaのヒープ外のメモリにアクセスしたいというケースもあります。これまでは、ByteBufferのallocateDirectメソッドを利用する方法、そしてJNIを利用する方法がありました。他に非公式ですが、Unsafeのメソッドを利用することによって実機に振という方法もありました。

しかしこれらの方法にはいくつか問題があります。まずByteBufferに関しては、1つのオブジェクトの条件が、IntegerではなくValueに限定されます。つまり1つのオブジェクトに対して最大でも2ギガでした。

そして、全体でMaxDirectMemoryで指定した値以下に指定する必要がありますが、メモリに割り当てる解放のタイミングは、自分でできるのではなくてGCによって行われるため、獲得するタイミングによっては、OutOfMemoryErrorが発生してしまうという問題がありました。

また、JNIに関してはC言語のようにわざわざcallを書く必要があり、メモリ管理をすべて自分で行う必要がありました。Javaを使ってGCで管理してもらっているのに、このようなところがデメリットになっていました。

そして最後のUnsafeクラスは、非公式であるため、将来的に無警告で削除される可能性がある上、名前の通りに容易にJVMをクラッシュさせるような、安全でない動作を引き起こすことがあるという問題を抱えていました。例えば管理されていないメモリを触るなどとか、そういったことをすると、簡単にクラッシュします。

というような問題がありましたので、Java14からJavaヒープ外でのメモリを扱うための新たな方法として、Java APIが導入されました。ここに書かれている例は、JEPで書かれていたメモリの逆演算子を行うサンプルとして次のようなコードを示しています。これ以外にもいろいろなAPIが用意されています。なので、もし興味があったらこちらのAPIのドキュメントも見て確認してみてください。

なお、このAPIはIncubatorと書かれていますが、これは今回APIが削除されたり変更される可能性があることを示し、標準モジュールはこのような特別なモジュールで提供されています。

このAPIに関しては、jdk.incubator.foreignに格納されています。ここにincubatorという文字が入っているのが特徴的なところです。またこのAPIはかなり広範囲でサポートされており、次のバージョンでAPIの利用方法が変わる可能性があります。現実的には、Java15ではSecond Incubatorとなりましたが、ここでもAPIの変更がかなり行われています。

例えばMapがメモリセグメントにサポート拡張されるなど、APIのリフレッシュが行われています。もし興味がある人は、APIのドキュメントを参照しながら使ってみて、OpenJDKコミュニティにフィードバックしてみてください。もしかしたら、あなたの貢献によってよりよくなる可能性があります。

Sealedクラス

続いてSealedクラス。ここからはJava15で追加された機能です。Java15では、新たに2種類のクラスが追加されました。そのうちの1つがSealedクラスです。これは誰がサブタイプになれるかを完全に制御可能なクラスです。これによって、今まではアクセス修飾子よりもクラスやインターフェースから、より明示的に実装を誰が担うか選定できるようになりました。

副次的な効果として、サブクラスを完全にコントロールできるので、パターンマッチングをより網羅的に行えるようになります。通常のクラス宣言と違うのは、まずclassの前にsealedを宣言すること。そしてpermitsのあとにどのクラスに拡張を許すかを宣言できます。今回は、パッケージ名も含めてFQAに記載していますが、同一パッケージ内であれば、もちろんクラス名だけの指定でも可能です。

特徴としては、Sealedタイプクラスの匿名サブクラスは禁止されています。また、とくに指定がない限り、Sealedクラスのabstorat、つまり抽象的なサブタイプはSealed内、通常のサブタイプはファイル内になります。各サブタイプの宣言時にnon-sealedと明記しておくことで、この制約を回避できます。

Hiddenクラス

続いてHiddenクラス。このクラスは、限定された方法以外で他のクラスやインターフェースからアクセスできない、他のクラスから完全に秘匿されていることを目的としています。現在の実装は、あるクラスのバイトコードからコンパイル時に作成されたものか、ランタイム時に作成されたものしか動きません。

このため、ある動的クラスが同じクラス階層にいる別のクラスから、名前を元にリンクしようとするたびに可視化されてしまうため、完全な取得を実現する方法がありませんでした。これを回避するために、フレームワークで実行時に生成され、他のクラスから取得されるリフレクションを返して間接的に使用でき、他のクラスから独立してアンロードできることを目的としたのが、このHiddenクラスというものです。

通常のクラスまたはインターフェースは、ClassLoader::defineClassを呼び出すことで生成されますが、このHiddenクラスは新しく定義されたこのメソッドによって生成できます。このメソッドは、byte配列を基にクラスを生成します。そしてリフレクティブアクセスを除くLookupオブジェクトを返します。

このLookupオブジェクトがHiddenクラスのクラスオブジェクトを取得する唯一の方法となり、他の方法からは取得できないことで、完全な取得を達成しました。その他の変更としては、まずJava13でレガシーなネットワークに関連するソケットAPIの再実装が行われました。これに基づいて周辺APIの再実装も行いました。

それ以外に不揮発性メモリにByteBufferが新しくサポートされました。最後にjava.rmi.activationパッケージに関連するクラスがすべて非推奨化されるという変更が行われました。以上がAPIに関連する修正です。

APIのそれ以外の変更

それ以外の変更についても説明していきたいと思います。まずJVMに関連して。ここのHelpful NullPointerExceptionに関しては、きしださんが説明していたので、省略したいと思います。

Java15では、Biased lockingが無効化されるようになりました。このBiased lockingは昔のJavaであればかなり効果が高かったんですが、現在、並列処理用のAPIが充実した昨今では、逆に性能が悪化しているということが認められたため、デフォルトで無効化されるようになりました。これによって、Java15以下からはBiased lockingがデフォルトで無効化されています。

もし有効にしたい場合はここを+にすることで有効にできます。

続いて、GCの変更について説明したいと思います。Java14の変更として、G1GCでNUMAを意識してGCが行えるようになりました。それ以外では、CMSGCがついにコード上から削除されるようになりました。これによって、警告モデルはCMSGCが選ばれることはありません。もしCMSGCを設定した上でJavaを起動した場合、デフォルトのGCが自動的に選択されて、実行されるようになります。

ZGCがmacOSおよびWindowsにも使えるようになりました。そして最後にある組み合わせのGCが非推奨化されました。このGCの組み合わせは、なかなか使えないタイプであって、このような組み合わせでのみ非推奨化が行われました。これ以外の組み合わせに関しては、とくに非推奨化は行われていません。

そしてJava15では、今はExperimentalな機能だった2種類のGCが、改めて標準機能になりました。まず、ZGCはアプリケーション停止時間を10ミリ秒以下にすることを目的としたGCで、ShenandoldGCはG1GCと同じようにリージョン単位でヒープを管理し、かつ並列コンパクションを採用したGCです。

これらは、Java15から標準機能になりますが、ロングタイムのサポートになるJava17から、本格的に使用し始めるプロジェクトも多いのではと思っています。

パッケージングツール

続いてツールに関して説明していきたいと思います。このツールに関しては、Java15では新しく追加はされていません。Java14でのみ追加が行われています。

まずパッケージングツールについて。このパッケージングツールは、平たく言うと、各OSに合わせたインストーラを作成するためのツールです。Windowsだけは新しく他に外部のアプリケーションを入れる必要がありますが、それ以外ではJDKをインストールするだけで作ることが可能です。このツールはインキュベーターなAPIに依存しているため、Incubatorとして記載されています。

今後、Javaアプリケーションを提供する流れとしては、まずはカスタムランタイムをjlinkで生成して、インストーラをこのjpackage、つまりこのツールで生成し、そしてこれをユーザーに提供していくという流れも、新しくできていくのではないかと想定しています。

では実際にどのように使っていくか説明したいと思います。これは単純に、まずinputで作ったjarを指定します。そしてmain-jarクラス、main-classを指定して、最後に名前を指定することによって、インストーラが生成されます。今回はmac上で実行したので、macに適用したインストーラが生成されています。.dmgファイルですね。このようなものが生成されています。

OSによってインストーラが生成されるため、例えばWindowsであれば.exeファイルが生成されます。これをユーザーに提供することによって、ユーザーはインストーラに基づいてインストールし、それを実行できるという環境が作れるようになりました。

custom runtime imageを作りたい場合は、今までと同じようにjdepsから依存しているモジュールを確認し、そしてjlinkで依存しているモジュールをadd-modulesで指定した上で、outputにカスタマイズされたイメージを生成、修飾するかを指定します。

そしてjpackageでは、ここで指定したいカスタムイメージが格納されているディレクトリを新しくここに、runtime-imageというかたちで指定することによって、custom runtime imageを元に同じようにインストーラを生成できます。

この2つのやり方で、何が一番違うのかと言うと、最終的なインストーラのサイズが一番違います。なので、なるべく小さいインストーラを提供したいという場合は、このようにcustom runtime imageを生成して、提供していくのが一般的になるかと思います。

JFR Event Streaming

最後にJFR Event Streamingに関して。これについては、私は簡単に説明します。もともとプロプライエタリ用であったオラクル社製のトラブルシューティングであるJava Flight Recorderが、Java11からOSS化され、つい最近例外中の例外に、Java8にもバックポートが行われました。

このJava Flight Recorderに、新しくStreaming機能が追加されました。これまではJFRで取得した情報をダンプしたファイル経由で読み取る必要があり、リアルタイムでモニタリングしにくいという点がありました。

この新機能によって、RecodingStreamクラスを通じて、同じJavaクラス内でメモリ経由で継続的に取得したり、もしくはリポジトリファイルを経由してJavaのプロセスを取得可能になりました。

それ以外の変更

最後に他の変更点について。まずJava15で新しい暗号の証明が追加されました。それがJava8で導入され、Java11で非推奨化されたJava EngineであるNashornというJSツールが削除されました。そして最後に、SolarisとSPARCのJVMのポートが、まずJava14で非推奨化されJava15で完全に削除されました。

以上でちょっと駆け足で説明してしまいましたが、Java14と15についての新機能の説明は以上となります。

どうもご清聴ありがとうございました。