Java9からJava14までの4つの細かい仕様変更

きしだなおき氏(以下、きしだ):よろしくお願いします。僕のセッションでは「Java9からJava14までをおさらいをする」という話をします。

自己紹介ですが、きしだと言います。LINE Fukuokaで働いていて、最近7年ぐらい放置していた洗濯機をやっと買い替えて、「文明って便利だな」と思っているところです。

今日の話ですが、Java9からJava14まで、けっこういろいろな変更が積み重なっていて、大きい言語仕様の変更などはけっこう話題になるので、みなさん知っているかもしれないのですが、小さい変更で取りこぼしているものがあるかもしれないので、それを拾っていこうと思っています。それを「言語の変更のまとめ」「VMの変更のまとめ」「ツールの変更のまとめ」「APIの変更のまとめ」として、話していきたいと思います。

Java9でのMilling Project Coin

はじめに「言語の変更」ですが、数としてはけっこうあって、まずはJava9でのMilling Project Coinについて。Project Coin自体は、Java7でのtry-with-resourcesやダイヤモンド演算子などといった細かい言語仕様の変更ですが、さらにprivateメソッドがinterfaceで使えるなど、もっと細かい変更が入ったのが、Java9のMilling Project Coinです。

あとは、Java10でvarが入ってローカル変数が型推論できるようになりました。Java12でSwitch Expression、Switchが式として扱えたり、Text Blocksで複数行の文字列が定義できたり、Recordsで名前付きタプルが定義できたり、あとはパターンマッチングの初歩的なものが入りました。

プレビューしてから正式版に「preview feature」

Java15でSealed Typesが入るのですが、ここで注意したいのがpreview featureで、varよりあとの機能、Switch Expression以降の機能については、preview featureとして入っています。Switch ExpressionやText Blocksについては、たぶんKUBOTAさんが取り扱うので、僕はそのpreview featureだけにしぼって、話をしたいと思います。

preview featureですが、varの場合は言語仕様の変更がけっこう直接入ったんですが、Switch Expressionからは、プレビューとしてまず一旦試用版が入って、それに対してフィードバックを行い、それで仕様が変更され安定したらスタンダードになる、という流れが入りました。

preview featureの機能を使うには、「--enable-preview」というスイッチが必要で、言語仕様の変更に伴って、API、例えばRecordsがわかりやすいんですけど、isRecords()というメソッドが入ったりします。

こういった言語仕様の変更、プレビュー機能についてのAPIの変更も、Java13のときはDeprecatedだったので単に警告が出るだけだったんですが、Java14からは--enable-previewがないと、そのAPIが使えなくなるという変更があったので、それも注意が必要です。

あとは、正式化まで3バージョン必要になります。これは別に「このようにしますよ」と定義されているわけではないんですけど、現実的には、まず一旦プレビューとして入って、そのフィードバックを得て、仕様変更が行われる。

例えばSwitch Expressionであれば、最初は値を出すためのyieldが「break 値」となっていたんですが、それがyieldに変更されたり、Text Blocksだったら改行のエスケープが入ったり、そういった小さい変更がセカンドプレビューとして入りました。それで仕様が安定したら、変更なしでスタンダードになります。つまり、少なくても3つのバージョンが必要と今はなっています。

VMの大きな変更は「モジュールシステムの導入」

次にVMの変更です。VMもけっこう変わっていて、とくに大きい変更は、これはVMだけの話ではなくてJavaの全体の変更になるんですけど、モジュールシステムが入っています。ただ現実的には、自分たちのプログラムをモジュール化することは、今はあまり行われていないと思っていて、ただJavaの内部でモジュール化されていることで、僕たちが間接的に恩恵に与っています。

あとはClass Data Sharing。この話はあとでします。あとはJava9のロギングで、JVMのログやGCのログなどが統一化されていますね。なので、Java8まででログの文字列を扱うようなプログラムを書いていた人は、Java9からはちょっと変更が必要になっています。

あとはガベージコレクタに、けっこう変更というか追加と削除が行われていて、大きいのはJava9でG1GCがデフォルトになっています。あとはGC Interfaceという、GCをいろいろ付け外しするための内部的なインタフェースが用意されていて、そのあと3つかな、ガベージコレクタが立て続けに入っていますね。

Java11で、EpsilonGCという何もしないGCですけど、ガベージをコレクトしないガベージコレクタが入っていますね。あとはZGC、Shenandoahもそうなんですが、大量のデータが扱えます。G1GCが入ったときも大量のデータと言っていたんですが、このときは32ギガバイトぐらいを大量と言っていたのですが、ZGCではテラバイト級まで扱えるようになっています。

あとはJava14で、CMS、Concurrent Mark & Sweepが削除されていますね。ZGC、Shenandoahに関しては、Java11、Java12にExperimentalで入っていて、Java15でプロダクションになっています。スタンダードではなくてプロダクションになっています。ZGCやShenandoahといったGCに関しては、Javaの標準ではなくてVMの実装なので、標準ではなくてプロダクションになったということですね。

Class Data Sharing

先ほど出てきたClass Data Sharingについてちょっと説明します。Class Data Sharingは、JVMがクラスを読み込んで内部表現になるんですけど、これをそのまま保存しておくと、クラスのロードの手間が省けて起動が速くできるというものです。Java10で、アプリケーションクラスのデータも使えるようになりました。

商用機能としては以前からアプリケーションのクラスも扱えていたんですが、Java9までのOpenJDKでは標準ライブラリのクラスデータだけが扱えていました。それがJava10でOpenJDKでもアプリケーションのクラスデータが扱えるようになっています。

Java12で、標準ライブラリについてのクラスデータがデフォルトで添付されるようになったので、何もしなくても起動が速くなったというのがあって、ちょっとその部分だけ紹介したいと思います。

ここに単にgetUptimeを表示するだけのクラスを用意していて、例えばこれをJava11で動かしてみますね。3回ぐらい動かして、だいたい110ぐらいになっています。これがJava12で動かすと……。これはOpenJ9なので、この話が通じなくて、(HotSpotで)こうすると75ぐらい。今までJava11だとWakeupするのに110ミリ秒ぐらい掛かっていたのが、75ミリ秒になっています。

これは単にJVMを起動するだけで速くなっているので、もう何もしなくても起動が速くなっていて、それだけでもJava12以降を使う意味があるなという感じですね。もちろん、自分で標準ライブラリのクラスデータを作ればいいんですけど、インストールの手間も省けるし、何よりCDSの仕組みを知らなくても速くなるのでけっこういいなと。だからもう、Java12以降は起動が速くなったと単純に考えてもいいんじゃないかなと思います。

ツールは「JShell」「Launch Single File Source Programs」が大きな変更

あとはツールの変更で、ツールもけっこう変更がありますね。個人的には一番大きいのがJShell。運用する人は、もちろんFlight Recorderが一番大きいんですけど、個人的にはJShellです。Flight Recorderの話は僕は扱いません。

JShellとLaunch Single File Source Programsは、いろいろ試すのにけっこう便利で、これがいろいろな人に恩恵がある機能だと思います。運用する人には、Flight RecorderでJVMの状況を保存して、あとからちゃんと「なんで落ちたのか」とかを監視できるものが入って、これが一番便利だと思います。

これが商用機能だったんですが、Java11でオープンソースになってJava Flight RecorderだったのがJDK Flight Recorderになって、Java14ではイベントストリーミングが入っています。

あと、インストーラ付きのパッケージングツールがJava14で入っています。ただこれはインキュベーターとして入っていて、試用機能の段階です。今回はJShellとSingle File Source Programsを紹介します。

JShellはREPLです。Read-Eval-Print Loopのことで、インタラクティブにJavaのコードの実行確認ができるものなのですが、見たほうが早くて、jshellと入力すると、14.0.2のJShellが起動していて、12+23とやると35と返す。これも立派なJavaのコードなので、こうやってJavaのコードが実行して、すぐに結果が見れるというのがわかりますね。

Tabを押すと補完されたり、もちろんこういったもの[IntStream.of(1,2,3).sum()]も実行されるわけですね。何か新しい機能を試すのにすごい便利で、Java8での機能を試したりしたときに、JShellがなくてとても不便だったりしました。

あとはLaunch Single File Source Programs。ソースファイルが1つのプログラムは、javacではなくjavaコマンドで直接実行が可能になりました。その場合にファイル名とクラス名が一致していなくてもよくて、.javaという拡張子も必要なくなっているので、そうするとshebang、頭に#!コマンド名を付けることで、Javaのソースファイルをスクリプトコマンドのように実行できるようになっています。

その様子だけちょっと見てみましょう。ここにHello.javaがあって、単なるHello Worldです。ファイル名を変えてみようか。これを単なるHelloにするか。そうすると今Helloファイルがjavacすることなく起動されたのがわかると思います。なのでJavaのプログラムはjavacをしないといけないというのは、もうjava11からは嘘になっていますね。

APIの変更のまとめ

次にAPIの変更で、これもけっこうあって、今ここに並べているのはJEPになっているものなんですが、例えばConvenience Factory Methods for Collections。これはofを使ってコレクションのインスタンスが作れるものです。

あとはIndy String Concatenation。これは、Java8までは+演算子による文字列の連結はStringBuilderに変換されていたんですが、コンパイル時にStringBuilderのappend()に変換されていて、もっと前はStringBufferに変換されていました。

そうすると、StringBufferに変換されていたものでコンパイルされたものは、新しいバージョンになってもStringBufferが使われる。さらに新しいアルゴリズムが出ても古いままとなるので、これを実行時に処理を選べるようにしたのが、Indy String Concatenationです。

同時に文字列の+の連結の場合、通常StringBuilderを使うときはどれだけ文字列が来るかわからないんですけど、+演算子で文字列を連結するときはいくつ連結するかがわかっているので、そうすると結果的に何バイトの文字列になるかがわかるので、それだけの領域をあらかじめ用意して頭から埋めていく処理になって、さらに効率がよくなっています。

Compact Stringは、今Javaでは内部的にUTF-16で文字を扱っていて、そうするとアルファベットにしろ漢字にしろ2バイトか4バイトを使うというようになっているんですが、ASCIIキャラクターだけの文字列の場合は1バイト1文字で扱う、漢字などが入っているとUTF-16で扱うというように、文字列の内容によって表示形式を切り替えるようになっていて、そこでメモリを節約するものが入っています。

あとはHTTP Client、HTTPアクセスが時代に合わなくなっていたので、Java9ではそれがインキュベーターとして入って、Java11でスタンダードになっています。あとはJava11でJava EE and CORBAが削除されたのですが、これはJava EEのコンテナ側にけっこう影響があって、GlassFishやPayara、JBossなど、その辺がけっこうJava11対応にがんばっていましたね。

ただ、我々はあまり気にすることがないです。普通にライブラリとして使えばいけたので。

あとは、Java13でその次のReimplement the Legacy Socketが入ったのと、3つ下のReimplement Datagram Socket。これはソケット通信のコードを書き換えたんですが、Project LoomというJavaでグリーンスレッド、Javaでスレッドを制御する、複数の処理をOSで切り替えるのではなくJavaで複数の処理を切り替えるというのをProject Loomとして開発中なんですが、それに対応できるようにAPIを変えたのがReimplement Socketです。なので、今の時点では直接的な影響はないですね。

あとForeign Memory Accessは、Javaヒープ、GCで扱うメモリじゃないところの領域を確保するものなんですが、今はUnsafeとして、標準ではないコードをみんな使っていて、それをどうにかして外したいとJVM開発者は思っていて、そのための一歩としてGCで扱わないメモリを扱えるAPIというのが、今Java14でのインキュベーターとして入っていますね。「Project Panama」という、パナマ運河のようにJVMとネイティブをつなぐプロジェクトの1つとして入っています。

次のやつが一番僕たちに便利な気がするのですが、Helpful NullPointerExceptions。NullPointerExceptionのメッセージが便利になりましたというのが入っています。

あとAPIの場合に、試用機能がインキュベーターとして入るようになったというのも大きいところで、HTTP Client、Foreign Memory Accessといった大きいモジュールに関しては、一度インキュベーターとして入って、それで仕様が固まったらスタンダードになるようになっています。

それ以外にもJEPになっていない変更がけっこういっぱいあって、それを紹介したいと思います。今回紹介するのは、Factory MethodsとNullPointerExceptionの例外の様子、あとはJEPになっていない変更をちょっとずつ紹介したいと思います。

Convenience Factory Methods for Collections、JEPになっていない変更

Convenience Factory Methods for Collectionsというのは、コレクションにofメソッドが生えて、いろいろインスタンスが作れるようになったというものです。jshellで起動して、例えばList.of("1","hello")とすると、1とhelloがあるリストができたり、あとはMap.of(“a”,”apple”,”b”,”banana”)とやるとaに対してapple、bに対してbananaというMapができたりとか、そういったものですね。

これはけっこう使っている人も多いんじゃないかなと思います。

次にJPEになっていないAPIの変更なんですが、いろいろ紹介しますね。Stringに関してはけっこう変更が入っていて、まずlinesは1行ごとにStreamで扱うみたいなものが入っています。あとはrepeatで文字列を繰り返とか。transformでメソッド適用を後置きで書けます。これはちょっとわかりにくいのでデモを見せます。あとはformatedというのが入っていますね。

デモをすると、linesは、こういう改行で区切られた文字列があって、それにlinesとやると1行ずつ扱えるメソッドですね。だからstr.lines().count()とやったら、行数が数えられるメソッドです。repeatは一番簡単で、repeat(3)とやると3回繰り返すものですね。

transformは、これだとわかりにくいですが、例えばsという文字列の変数があって、これをs.toLowerCase()として小文字にしましたと。これをこのMapに渡したいなと思ったときに、本当は先頭に戻って$2.get(s.toLowerCase())と書かないといけないんですけど、そのままtransformとやると、s.toLowerCase().transform($2::get)と書けるということですね。なので、文字列に対するメソッドの呼び出しを後ろに書けるというものです。

最近のプログラミング言語では、これは言語仕様であるんですけど、Javaはそういった言語仕様が今のところ入る予定はないので、メソッドで対応するという感じですね。これはいろいろなメソッドに適用してほしいんですが、まずはStringからという感じかなと思います。だから案外これが便利なときがあると思います。

Stringの話はこの辺にして。先ほどコレクションでofが入ったという話をしましたが、Java10でcopyOfが入っています。これにはリストやMapのコピーを作るメソッドが入っているんですが、ofにしろcopyOfにしろ、これらはUnmodifiableList/Map/Setになっています。これの内容がイミュータブルで追加・変更・削除ができない。そのことによって、マルチスレッド処理が安全に性能が出せることを狙っています。

ストリーム処理には、dropWhileとtakeWhileと、あとCollectorsに.toUnmodifiableList/Set/Mapが入っています。だからストリーム処理をしてMapにしたい、Setにしたいものをイミュータブルなものにするのが、Java10で入っています。あとはPredicateにnotが入っていて、メソッド参照を使うときに、条件を反転したいときに使えますね。

その他の細かい変更

I/Oですけど、FilesにreadStringとwriteStringが入っていて、ファイルから文字列の読み書きが1命令でできるようになっています。あとはInputStreamやReaderにtransferToが入っていて、InputStreamからOutputStreamに転送、ReaderからWriterに転送ということが1命令で書けるようになっています。

あとは令和ですね。Java8、11では表示やパースができるようになっています。Java8の場合はupdate 211と212で、Java11の場合は11.0.3で、フォーマットやパースができるようになっています。このときは文字列として扱えるだけで、APIの変更は表面上なかったものの、内部的にはもっていて、privateになっていたので我々は使えないという状態だったんですが、Java13でちゃんとAPIが入っています。

それだけちょっと見ましょうか。java.time.chrono.JapaneseDate.now()をやれば、ちゃんとReiwaと出ますね。こうなったのがJava8の場合は211だったりJava11の場合は11.0.3だったりします。ただ、JapaneseEraに今REIWAと入っているんですが、これが使えるようになったのがJava13からです。

あと例外メッセージが親切になった話だけしたい。「もう終わりの時間です」と言われているんですが、例外メッセージが親切になった話だけしたい。

まずArrayIndexOutOfBoundsExceptionの話ですけど、ちょっとJava10で動かしましょうか。例えば今はJava10でJShellを動かしていて、要素数0の配列を用意して0番目にアクセスする。そうすると、こうやってArrayIndexOutBoundsExceptionが出るんですが、0とだけ出ていますね。

慣れると意味がわかるんですが、慣れるまではこの数字が何を意味しているかがわからないということがありました。

ついでにNullPointerExceptionも見ておきましょう。new String[]{null}[0]はNullだけの要素のString配列の0番目なので、Nullが返りますね。これに.toUpperCase()とすると、NullPointerExceptionが出るんですが、NullPointerExceptionと言われるだけで、自分でどこかなと探さないといけなかったわけですね。

これがJava11、Java14でいろいろ変わっていて、ArrayIndexOutBoundsExceptionは自動的に変わっているんですけど、NullPointerExceptionはスイッチが必要で、JShellの場合は「-R-XX:+ShowCodeDetailsInExceptionMessages」とやって起動して、さっきの要素数0の配列を用意して0番目にアクセスします。

これをやるとIndex 0のout of bounds for length 0。両方0だからわかりにくいので、これを3にすると、要素数0の配列に対して3番目をアクセスするという意味で、Index 3をlength 0に対してと、ちゃんと教えてくれるようになりました。元の配列の長さも教えてくれるようになったのが、情報としては追加された部分で、それが人間にわかりやすいようなメッセージになったというものですね。

NullPointerExceptionは、String.toUpperCase()でNullPointerExceptionが発生すると、becauseでarrayの0番目がNullだからというインフォメーションが出るようになっています。

これが多分運用する場合でもメチャクチャ便利だと思うので、そのためだけにJava14を使ってもいいんじゃないかという気もしますね。

もう時間なのでまとめですけど、地味にいろいろな変更が入っているので、できる限り最新版、とくにJava8から11はギャップがあるのでがんばらないといけないのですが、既にJava11にしているのであれば、使えるならJava14を使うといいですね。あとの細かい話はKUBOTAさんがやってくれるので、それも楽しみにしてください。

この話は『みんなのJava』というのが出ていて、これにけっこうまとめているので、見てもらえるとうれしいです。ということで、僕の話は以上です。

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