X

A blog about Oracle Technology Network Japan

Java 13のswitch式と再実装されたSocket APIの内側

Guest Author

段階的変更により、将来のメリットが今回のリリースに含まれるように

著者:Raoul-Gabriel Urma、Richard Warburton

2019年10月16日

時がたつのは早いものです。予定どおり、2019年9月にJDK 13がリリースされました。Java 13では、開発者の興味を引くアップデートが主に3つ行われています。

  • switch式(プレビュー機能)を新しいyield文により改善
  • 言語プレビュー機能として、複数行文字列リテラル(テキスト・ブロック)を導入
  • Socket APIの実装を最新化

本記事では、switch式とSocket APIに注目します。テキスト・ブロックについては、本号の別の記事で特集しています。

switch式(プレビュー機能)

以前の記事で、JDK 12のプレビュー機能として導入されたswitch式を紹介しました。switch式はJDK 13でもプレビュー機能のままですが、これが意味するのは、プレビュー・モードを卒業するまでは今後のリリースで変更される可能性があるということです。さらに、機能を開放する必要があることも表しています。switch式を有効にするためには、次のコマンドを実行する必要があります(Example.javaというファイルがあるものとします)。

javac --enable-preview --release 13 Example.java
java --enable-preview Example

switchの新機能に入る前に、簡単にJDK 12をおさらいしておきます。JDK 12では、式形式のswitchが導入されています。この形式のswitchを使うことで、以前の記事で紹介したいくつかのメリットを享受できます。たとえば、次のようなものです。

  • フォールスルー:switchの新しい構文ではフォールスルーが発生しません。これにより、バグが発生する可能性を減らすことができます。
  • 複合形式:新しいswitch式では、1つの分岐条件で複数のcaseラベルを扱うことができます。これにより、コードの冗長性を減らすことができます。
  • 網羅性:新しいswitch式を使った場合、可能な列挙値すべてに対して、対応するswitchラベルが存在することをコンパイラが確認してくれます。これにより、バグが発生する可能性がまた少なくなります。
  • 式形式:次の例で示すように、switchを式形式で使えることから、コードが短くなるとともに、コードの意図をより明確に表現できるようになります。次の例では、列挙型をswitchで条件分岐して適切に文字列を返しています。
var log = switch (event) {
    case PLAY -> "User has triggered the play button";
    case STOP -> "User needs a break";
    default -> "No event to log";
};

この例からわかるように、アロー構文を使ったワンライナーで簡単な式を返すことができます。しかし、分岐のコードが複数行にまたがる複数の文(ブロック式)の場合、どのように値を返すかはわかりやすくはありませんでした。return文は、呼び出されたメソッド自体から戻ることを示すため、使うことはできません。JDK 12では、switch式自体から値を返す場合、break文を使いました。これは、次に示す例のコードの最終行のように、値付きのキーワードをオーバーロードしているため、少々不格好です。 

var log = switch (event) {
    case PLAY -> "User has triggered the play button";
    case STOP -> "User needs a break";
    default -> {
        String message = event.toString();
        LocalDateTime now = LocalDateTime.now();
        break "Unknown event " + message + " logged on " + now;
    }
};

上記のコードはJDK 12のプレビュー・モードではコンパイルできますが、JDK 13では動作しなくなります。

JDK 13では、この動作を実現するためにyield文が導入されています(コードの最終行をご覧ください)。 

var log = switch (event) {
    case PLAY -> "User has triggered the play button";
    case STOP -> "User needs a break";
    default -> {
        String message = event.toString();
        LocalDateTime now = LocalDateTime.now();
        yield "Unknown event " + message + " logged on " + now;
    }
};

それでは、break、return、yieldはどう違うのでしょうか。return文は呼出し元に制御を戻し、yieldはもっとも内側にあるswitchに制御を戻すと考えることができます。breakキーワードは、switchから抜け出すためのものです。なお、yieldの導入により、Javaのキーワード管理と予約済み識別子に関連して、拡大した議論が始まっていることに注目してください。returnbreakはキーワードですが、yieldは現在、JDK 10で導入されたvarのような限定識別子として提案されています。この2つの違いは小さなものですが、この違いが重要になる場合もあります。breakを変数名として使うことはできません。その理由は、breakがキーワードであるからです。しかし、varは限定識別子であるため、変数名として使うことができます。

switch式へのyield文の導入は小さな変更ですが、言語にyield文が導入されることによって、将来的な可能性が開かれることになります。というのは、yield文を他の機能に使用できる可能性もあるからです。たとえば、他の多くのプログラミング言語では、コルーチンやジェネレータなどの高度な構造を実現する方法として、yieldという単語が採用されています(たとえば、PythonやJavaScript、Scalaでサポートされています)。一方、現在のJavaで文としてのyieldがサポートされているのはswitch式のみであることを覚えておくことは重要です。 yieldという名前が付いた別の機能(Thread.yieldなど)や、他のプログラミング言語の機能とは混同しないでください。

Socket API

多くの場合、OpenJDKなどの実績ある長命のソフトウェア・プロジェクトは、内部に多くの古いコードを抱えています。こういったコードは、メンテナンスと改善が必要であるにもかかわらず、長期間存在しています。最近のJavaリリースでは、古いコードベースを改善して新機能を追加するために、多くの作業が行われています。Java 13もその例外ではありません。Java 13では、古いSocket APIが完全に再実装されています。その対象になった機能と、理由について確認します。

Java標準には、低レベルTCPネットワーク構成を実装する複数のAPIが含まれています。このAPIを大きく分類すると、「New IO」(NIO)サブシステムと、古いI/Oシステムに分かれます。導入されてからすでに15年が経過したNIOは、この2つのうち新しい方の実装で、非同期APIと同期APIの両方が含まれています(現在、一からTCPを記述している方は、古いI/Oシステムではなく、NIOや高レベルライブラリ(Nettyなど)を使うべきです)。

従来のI/Oライブラリは、古いAPIであるにもかかわらず、現在もJavaエコシステム全体で広く使われています。少しGitHubを検索しただけでも、11,046件のコミットがあり、古いSocketクラスは100万回を超えて参照されていることがわかります。そのため、今後のバージョンのJavaでもこのAPIのメンテナンスがやはり必要であることは明らかです。それでは、Java 13でまったく新しく実装し直す必要があったのはなぜでしょうか。この問いに答えるためには、JDK開発の全体像を理解する必要があります。

現在、OpenJDKで行われている大規模なプロジェクトの1つに、Project Loomがあります。このプロジェクトは、JVMによるFiberの実装です。Fiberは、オペレーティング・システムのスレッドとの対応関係が1対1ではない軽量スレッドです。実際には、1つのオペレーティング・システムのスレッドで、数百や、場合によっては数千のFiberが動作している可能性もあります。Fiberでは、スレッドの動作をブロックしないように考慮されています。Fiberでロックの使用やスリープが必要になった場合、ベースとなるスケジューラはそのスレッドに別のFiberをスワップ・インし、動作を継続させます。

Javaのソケット・オブジェクトはスレッドセーフな設計になっているため、内部的にロック(つまり、同期ブロック)を使ってI/O操作の競合状態を回避します。この実装の大部分は古いCコードです。このコードでは、I/Oバッファとしてスレッド・スタックが使われ、ネイティブ・ロックも使われています。 Project Loomでは、ネイティブCのロックではなく、Java 5のロック(つまり、java.util.concurrent.lockパッケージ)を使ってFiberを効率的に切り替える仕組みがサポートされる予定です。そのため、JDKのすべてのブロッキング・コードをJava 5のロックに移行する必要があります。つまり、Project Loomとの両立性を高めるため、古いSocket APIの再実装が必要だったのです。

ただし、書き換えの動機はそれだけではありませんでした。驚くべきことではありませんが、古いSocket APIには、膨大な問題を抱えた、古くてメンテナンス性の低いコードが数多く含まれています。ネイティブ・ロックとネイティブ・バックエンドのコードには、前述のLoomとの連携に関する問題のほか、メンテナンス上の問題もありました。さらに、JDKに古いSocket APIとNIO APIの両方が存在したため、JDK開発者は2つの異なる実装をメンテナンスする必要がありました。1つの実装を使うことができれば、それに伴ってメンテナンスの負荷も低くなるでしょう。

一言で言えば、Java 13では、NIOと同じベースのインフラストラクチャと実装を使って、古いSocket API用の新しいバックエンドを実装しています。Java 11リリースの一環として、NIOはJava 5のロックを使うように移行されました。そのため、すでにFiberとの親和性があります。今回の変更にはその既存作業が活用され、その結果、Java 13の古いSocket APIにもFiberとの親和性が生まれています。また、JDKで2つのI/Oバックエンドをメンテナンスする必要もなくなっています。

移行の負荷を軽減するため、jdk.net.usePlainSocketImplシステム・プロパティをtrueに設定することで、古い実装を有効化できるようになっています。

APIは変更されていないことから、動作の互換性を別にすれば、この変更に関連する主要なリスクはパフォーマンスです。 OpenJDKプロジェクトでは、変更のパフォーマンスを評価するために使われる一連のマイクロベンチマークがメンテナンスされています。このベンチマークをUbuntu Linux 18.04 / AMD Ryzen 7 1700マシンで実行し、Java 8のSocket API実装と、書き換えられたJava 13のSocket API実装のパフォーマンスを比較してみました。 

図1図2は、それぞれタイムアウトがある場合とない場合で、java.net.Socket APIを使ってTCPのパケットを読み書きするSocketReadWrite.echoベンチマークのパフォーマンスを示したものです。このベンチマークを、1バイトから128,000バイトのサイズのメッセージに対して実行しました。このベンチマークでは、Socketオブジェクト自体のtimeoutプロパティを有効および無効にしています。

 

図1:タイムアウトがある場合のパフォーマンス

 

図2:タイムアウトがない場合のパフォーマンス

さらに、I/O Streams APIを使ってパフォーマンスをテストするSocketStreaming.testSocketInputStreamReadベンチマークも実行しました。このベンチマークでは、Java 8とJava 13の両方の実装で44ミリ秒/opが達成されました。

以上のベンチマークから、パフォーマンス面での差異に気づくJavaプロジェクトが多いとは考えにくいことがわかります。差は極めて小さく、測定ツールの誤差の範囲内です。なお、この分析の注意事項として、このベンチマークはSocket APIが使われるすべてのシナリオを代表したものではないことを付記しておきます。そのため、パフォーマンスの問題を発見した場合は、バグとして報告する方がよいでしょう。

まとめ

Java 13では、いくつかの変更や改善がJava開発者に提供されています。そのいずれもが、調べてみるだけの価値があるものです。複数行文字列が導入され、yieldキーワードの追加によってswitch式が改善されています。これらは、Java SEプラットフォームへの継続的な注力や改善を実証するプレビュー機能です。

さらに、古いSocket APIの書き換えは、Fiberといった将来の機能のリリースへの対応に向けて内部的に行われている継続的な作業の一例です。Java言語は、Javaらしいスタイルと大きなライブラリ・エコシステムを維持しつつ、積極的に改善を繰り返そうという気概にあふれています。そのため、Javaは今後も特に人気の高いプログラミング言語であり続けることでしょう。

Java Magazine 日本版Vol.47の他の記事

Javaにテキスト・ブロックが登場
言語の内側:シールド型
TeaVMを使ってブラウザでJavaを動かす
ツールをよく知る
クイズに挑戦:1次元配列(中級者向け)
クイズに挑戦:カスタム例外(上級者向け)
クイズに挑戦:ロケールの読取りと設定(上級者向け)
クイズに挑戦:関数型インタフェース(上級者向け)


Raoul-Gabriel Urma

Raoul-Gabriel Urma(@raoulUK):イギリスのデータ・サイエンティストや開発者の学習コミュニティをリードするCambridge SparkのCEO/共同創業者。若いプログラマーや学生のコミュニティであるCambridge Coding Academyの会長/共同創設者でもある。ベストセラーとなったプログラミング関連書籍『Java 8 in Action』(Manning Publications、2015年)の共著者として執筆に携わった。ケンブリッジ大学でコンピュータ・サイエンスの博士号を取得している。

 

Richard Warburton

Richard Warburton(@richardwarburto):Java Championであり、ソフトウェア・エンジニアの傍ら、講師や著述も行う。ベストセラーとなった『Java 8 Lambdas』(O'Reilly Media、2014年)の著者であり、Iteratr LearningとPluralsightで開発者の学習に貢献し、数々の講演やトレーニング・コースを実施している。ウォーリック大学で博士号を取得している。


※本記事は、Raoul-Gabriel Urma、Richard Warburtonによる”Inside Java 13’s switch Expressions and Reimplemented Socket API“を翻訳したものです。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.