2024/03/19

JEPでは語れないJava 22

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 22は、LTSであるJava 21の次のバージョンですが、意外と新機能盛りだくさんです。

Java 22のJEPは以下の12。しかも、スタンダードJEPが4もあります。

  • 423: Region Pinning for G1
  • 447: Statements before super(...) (Preview)
  • 454: Foreign Function & Memory API
  • 456: Unnamed Variables & Patterns
  • 457: Class-File API (Preview)
  • 458: Launch Multi-File Source-Code Programs
  • 459: String Templates (Second Preview)
  • 460: Vector API (Seventh Incubator)
  • 461: Stream Gatherers (Preview)
  • 462: Structured Concurrency (Second Preview)
  • 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
  • 464: Scoped Values (Second Preview)

注目すべきは、長らくIncubatorやPreviewだったJEP 454。FFMと省略して呼ぶことがおおいですが、Project Panamaのメインとなる機能です。

JNIの代わりに、ネイティブコードをコールしたり、ヒープ外のメモリにアクセスするためのAPIです。

モジュールはjava.baseで、パッケージはjava.lang.foreignになります。

APIなので、本来であれば本エントリーでも取り上げるのですが、ちょっと量が多いですし、差分を紹介してもしかたありません。そこで、別エントリーで使い方についてまとめて紹介する予定です。

ちなみに、同じくProject Panamaで仕様策定しているVector APIはまだIncubatorのままですが、次のバージョンで正式にリリースされるのではないかというのが、さくらばの予想です。

言語仕様の変更がJEP 447, 456, 459, 463と4種類もあります。JEP 456だけがスタンダードJEPで使用しない変数やパターンを _ (アンダーバー)で省略して記述できるというものです。

スタンダードJEPであるJEP 423はG1GCのアルゴリズム改良、JEP 458はjavacでコンパイルすることなく複数のJavaコードを実行できるというものです。この機能は、JEP 330の拡張ですね。

あらたにPreview JEPになったのが、JEP 457とJEP 461です。

JEP 457はバイトコードを扱うためのAPIです。今までバイトコード操作というと、ASMなどが使われていましたが、標準のAPIで可能になります。

JEP 461はStream APIの拡張です。今まで中間操作はストリームの流れてくる1データに対する処理に限定されていましたが、Gathereを使用するとかなり柔軟に中間操作を記述することができるようになります。

 

と、軽くJEPを説明したところで、APIの変更について紹介していきましょう。JEPは多いのですが、意外にもAPIの変更は少ないです。ほとんどがJEP 454とJEP 457に関する変更です。ただし、今回もPreviewやIncubatorの変更は省略するので、JEP 457に関連したAPI変更はStandard JEPになった時に紹介します。また、前述したようにJEP 454 FFMは別エントリーで紹介する予定です。

例によって、セキュリティ関連のAPIは省略します。本バージョンでも、java.baseモジュール以外にもAPIの変更はありますが、使用頻度が低いAPIであるため、解説を省略します。

 

廃止になったAPI

Java 22では1つのメソッドが廃止になりました。しかし、もともと使用しても例外をスローする実装になっているので、廃止されても問題はないはずです。

 

メソッド

  • java.lang.Thread.countStackFrames()

スタックフレームをカウントするメソッドですが、Java 21まではUnsupportedOperationException例外をスローする実装になっています。

 

廃止予定のAPI

Java 21で追加された廃止予定のAPIはありません。

 

追加/変更されたAPI

Java 22のjava.baseモジュールで追加されたAPIは約300なのですが、そのうちの200以上がJEP 457 Class-File APIで、約30がJEP 454 FFM APIです。8割ぐらいは、この2つのJEP由来の変更ということになります。本エントリーでは残りの2割を紹介していきます。。

 

java.base/java.ioパッケージ

java.ioパッケージのConsoleクラスで1つだけメソッドが追加されました。

 

Consoleクラス

Consoleオブジェクトで扱っているデバイスがターミナルかどうかを調べるメソッドが追加されました。

  • static boolean isTerminal()

ターミナルというのは標準入出力に対応したデバイス(POSIXでいうところのtty)です。ターミナルであればtrueが返ります。逆にいうと、JShellやIDEのコンソールだとfalseになります。

jshell> System.console().isTerminal()
$1 ==> false

jshell>

 

java.base/java.langパッケージ

Java 22では、Unicode 15.1に対応したのでそれに応じたブロックの追加が行われました。これ以外にClassクラスとStackWalker.Option列挙型に追加があります。

 

Character.UnicodeBlockクラス

Unicode 15.1で追加されたブロックの定数が追加されました。

  • static final Character.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_I

 

Classクラス

プリミティブ型に対応するClassオブジェクトを取得するメソッドが追加されました。

  • static Class<?> forPrimitiveName(String primitiveName)

引数にはプリミティブ型を表す文字列、たとえば"int"とか"double"を指定します。引数がnullの場合、NullPointerException例外がスローされます。

 

StackWalker.Option列挙型

StackWalkerクラスはスレッドごとに作成されるスタックフレームを操作するクラスです。Option列挙型はStackWalkerオブジェクト生成時に使用する列挙型ですが、定数が1つ追加されました。

  • StackWalker.Option DROP_METHOD_INFO

StackWalkerオブジェクトがスタックフレームを操作する時にメソッドの情報を扱わないように指定します。

 

java.base/java.lang.reflectパッケージ

いつものことですが、新しいリリースを表す定数が追加されています。

StackWalker.Option列挙型

Java 22に対応する定数の追加です。

  • ClassFileFormatVersion RELEASE_22

 

java.base/java.netパッケージ

IPv4/IPv6のアドレスを表すInet4Addressクラス/Inet6Addressクラスは直接生成することはできず、スーパークラスのファクトリメソッドを使用していました。これに対し、それぞれのクラスにファクトリメソッドが追加されました。

 

InetAddressクラス

"127.0.0.1"などに対応するInetAddressオブジェクトを生成するにはセグメントの配列を使用するgetByAddressメソッドか、ホスト名も使用できるgetByNameメソッドを使用してきました。これに対し、アドレスを文字列で指定するファクトリメソッドが追加されました。

  • static InetAddress ofLiteral(String ipAddressLiteral)

ofLiteralメソッドでは、引数の文字列をまずIPv4と仮定してパースを行います。失敗した場合、IPv6としてパースします。パースに失敗するとIllegalArgumentException例外がスローされます。

実際の処理はInet4AddressクラスおよびInet6Addressクラスに委譲します。

 

Inet4Addressクラス

Inet4Addressクラスにもアドレスを文字列で指定するファクトリメソッドが追加されました。

  • static Inet4Address ofLiteral(String ipv4AddressLiteral)

アドレスの表記は今まで使用してきたのと同じです。d.d.d.d形式だけでなく、d.d.dからd.d、そしてd形式もパース可能です。

jshell> Inet4Address.ofLiteral("127.0.0.1")
$1 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.1")
$2 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.257")
$3 ==> /127.0.1.1

jshell>

 

Inet6Addressクラス

Inet6Addressクラスも同様にファクトリメソッドが追加されました。

  • static Inet6Address ofLiteral(String ipv6AddressLiteral)

アドレスの表記も従来と同じで、::や::d.d.d.d形式なども使用できます。

jshell> Inet6Address.ofLiteral("::1")
$1 ==> /0:0:0:0:0:0:0:1

jshell>

 

java.base/java.nio.charsetパッケージ

UTF-8などの標準的な文字セットを定数に持つStandardCharsetsクラスに定数が追加されました。

 

StandardCharsetsクラス

StandardCharsetsクラスではUTF-8やUTF-16系の定数は定義されていましたが、UTF-32系がなかったので追加されました。

  • static final Charset UTF_32
  • static final Charset UTF_32BE
  • static final Charset UTF_32LE

 

java.base/java.nio.fileパッケージ

Pathインタフェースにデフォルトメソッドが追加されました。

 

Pathインタフェース

Pathインタフェースにresolveメソッドのオーバーロードが2種類追加されました。いずれもデフォルトメソッドです。

  • default Path resolve(String first, String... more)
  • default Path resolve(Path first, Path... more)

実際の動作はfirstに対しresolveを行い、得られたPathオブジェクトに対しmoreを順々にresolveしていきます。

実際のコードは以下のようになっています。

    default Path resolve(Path first, Path... more) {
        Path result = resolve(first);
        for (Path p : more) {
            result = result.resolve(p);
        }
        return result;
    }

 

java.base/java.textパッケージ

リストをフォーマットするクラスが追加されました。

 

ListFormatクラス

ListFormatクラスはリストのフォーマッタークラスです。なぜになって導入されたのか、いまいち謎です。

他のフォーマッターと同様にスタイルなどを指定する列挙型も導入されています。

  • enum ListFormat.Style { FULL, SHORT, NARROW }
  • enum ListFormat.Type { STANDARD, OR, UNIT }

ListFormatクラスの使い方は他のフォーマッタークラスと同じです。getInstanceメソッドでListFormatオブジェクトを生成し、フォーマットするのであればformatメソッド、パースをするのであればparseメソッドを使用します。

主なメソッドを以下に示します。

  • static ListFormat getInstance()
  • static ListFormat getInstance(Locale locale, ListFormat.Type type, ListFormat.Style style)
  • String format(Object obj)
  • String format(List<String> input)
  • List<String> parse(String source)
  • Object parseObject(String source)

Objectクラスを引数にするformatメソッドと、parseObjectメソッドはFormatクラスで定義されたメソッドです。

また、引数のないgetInstanceメソッドはデフォルトロケール、STANDARD、FULLとなります。

StyleとTypeによるフォーマットの違いはListFormatクラスのJavadocにまとめられているので、参考にしてください。

個人的には日本語ロケールだと、ちょっと使いものにならない気が...

jshell> import java.text.*

jshell> var format = ListFormat.getInstance()
format ==> ListFormat [locale: "日本語 (日本)", start: "{0}、{1}", ... }", three: "{0}、{1}、{2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$3 ==> "0、1、2、3"

jshell> format.format(List.of("a", "b", "c"))
$4 ==> "a、b、c"

jshell>

ここで示したようにデフォルトの日本語ロケールだと、リストの区切り文字に全角の"、"が使われます。それはちょっとなぁと思うわけです。

これに対し、たとえばUSロケールでSTANDARD/FULLだと次のようになります。

 jshell> var format = ListFormat.getInstance(Locale.US, ListFormat.Type.STANDARD, ListFormat.Style.FULL)
format ==> ListFormat [locale: "英語 (アメリカ合衆国)", start: "{0},  ... ree: "{0}, {1}, and {2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$6 ==> "0, 1, 2, and 3"

jshell>

英語的には最後の要素が", and "となるのは分かるのですが、これを使いたいことがあるのでしょうか。

結局、よく使うのはTypeをSTANDARDではなくUNITにし、StyleはFULLかSHORTのような気がします。

 jshell> var format = ListFormat.getInstance(Locale.of("c"), ListFormat.Type.UNIT, ListFormat.Style.FULL)
format ==> ListFormat [locale: "c", start: "{0}, {1}", middl ... , three: "{0}, {1}, {2}"]

jshell> format.format(List.of(0, 1, 2, 3))
$8 ==> "0, 1, 2, 3"

jshell>

parseメソッドは戻り値の型がList<String>となることに注意してください。

 

java.base/java.util.concurrentパッケージ

Fork/Join Framework関連でメソッドが追加されました。いずれも割り込みに関するメソッドです。

 

ForkJoinPoolクラス

割り込みがかからないタスク実行のメソッドが追加されています。

  • <T> List<Future<T>> invokeAllUninterruptibly(Collection<? extends Callable<T>> tasks)

複数のタスクをまとめて実行する時に使用するのがinvokeAllメソッドですが、それに割り込みがかからないようにしたのがinvokeAllUninterruptibly()メソッドです。

このメソッドではタスクをjoinする時に、quitelyJoinメソッドを使用しているため、割り込みがかからないようになっています。

 

ForkJoinTaskクラス

ForkJoinPoolクラスとは逆に、割り込みがかかるタスクのファクトリーメソッドが追加されました。

  • static <T> ForkJoinTask<T> adaptInterruptible(Callable<? extends T> callable)
  • static <T> ForkJoinTask<T> adaptInterruptible(Runnable runnable, T result)

今までのadoptメソッドでタスクを生成した場合、タスクに割り込みをかけることができませんでした。これに対し、adaptInterruptibleメソッドではタスクに対して割り込みを書けることができます。

戻り値の型はForkJoinTaskクラスですが、実際には派生クラスのInterruptibleTaskクラスのさらに派生クラスであるAdaptedInterruptibleCallableクラスが戻ります。

2種類のオーバーロードの違いは、引数の型が違うのでjoinした時の戻り値に違いがでるということです。Runnableインタフェースではタスクの戻り値がないので、adaptInterruptibleメソッドの第2引数のresultが返ります。

 

java.base/java.util.randomパッケージ

乱数値のストリームを生成するメソッドが追加されています。

 

RandomGeneratorクラス

RandomGeneratorクラスではdoubleの乱数値のストリームを生成するdoublesメソッドがあります。これの派生メソッドが追加されました。

  • default DoubleStream equiDoubles(double left, double right, boolean isLeftIncluded, boolean isRightIncluded)

doublesメソッドでは要素数を指定しますが、equiDoublesメソッドは無限ストリームになります。

引数は境界値で、その境界値を含むかどうかを第3, 4引数で指定します。

 

 

Java 22のAPI変更について紹介しましたが、やはり少ないですね。

また、これは便利だとか、使えそうというAPIの追加もないようです。

とはいうものの、FFMは外部ライブラリを使いたい人には有用ですし、Class-File APIもASMを使っていた人にはうれしいはず。といっても、これらを使う開発者はごくごくわずかだとは思います。

普通の開発者であれば、ストリームのGathererは便利に使えるはずです。GathererがStandard JEPになるまで、待ちましょう!

 

さて、次のJava 23では、長らくIncubatorだったVector APIが入るかどうかです。最近はVector APIのAPI変更もないようなのですが、一波乱あるのかどうか。ぜひ入ってほしいなぁ。

2024/03/17

Jfokus 2024 その2 セッション編

このエントリーをはてなブックマークに追加

前回に引き続き、Jfokusの参加記です。

www.javainthebox.com

 前回はJfokusに参加するまでの話ですが、本エントリーではさくらばがJfokusに参加して興味深かったセッションを紹介します。


Jfokusは3日間の会期中、1日目がチュートリアルとハンズオンが行われます。2, 3日目が通常のセッションです。


Java 21 Deep Dive - Better Language, Better Scalability, Better APIs, Better Tools

チュートリアルで聴講したのがこれ。おなじみのOracleのアドボケイトのNicolai ParlogとAna-Maria Mihalceanuのセッションです。

資料はこちら。

slides.nipafx.dev


Pattern Matching、Virtual Threads、String Templatesが前半で、後半はSequenced Collectinosなど細かな機能、最後にツール系を紹介していました。

まぁ、さくらばには、ほとんどが知っていることだったので、機能の再確認をしたという感じです。

適度にまとまっているので、機能のチェックをしたいのであれば、ちょうどいいと思います。


Java in 2024

Jfokusのキーノートセッションで、こちらもおなじみ、OracleのGeorges Saabです。


当初は、JavaのチーフアーキテクトのMark Reinholdが話す予定でした。しかし、Markの来訪がキャンセルになってしまって、急遽Georgesになりました。

さくらばはMarkのセッションを楽しみにしていたので、キャンセルと聞いてモチベーションダダ下がり。さらにVM Tech Summitもなくて、さらにモチベーションが下がる。

そのモチベーションの低さが会場の写真などをほとんど撮らなかったことにつながるわけです。

Georgesの話はいつも通りな感じですね。


Java Language Update

Devoxx BEでBrian Goetzが話したセッションのアップデート版。JfokusではOracleのViktor Klangが担当。


最近、ViktorさんとParさんが話すことが多いようなんですけど、そういう役割なんですかね。

Java 21だけでなく、Java 17ぐらいからのJava言語仕様の変化についてまとめたセッションです。


Enter the Parallel Universe of the Vector API

AzuleのSimon RitterのVector APIに関するセッション。


資料はこちら。

Enter The Parallel Universe of the Vector API


Vector APIについての分かりやすい解説。これを見ておけば、だいたい理解できるんじゃないかなぁ。資料だけだとちょっとつらいかもしれないですが。

Java 22でFFMが正式に導入されるので、Vector APIももうすぐですね。


Modern Java in Action

こちらもNicolai Parlogのセッション。


資料はこちら。

slides.nipafx.dev


GitHubをクロールしてするサンプルアプリケーションを古いスタイルから新しいスタイルに書き換えていくというライブコーディングのセッション。

なかなかおもしろいけど、早すぎて途中からついていけないのが...


これ以外にも、Ubertoの関数型のセッションや、ParさんのLeydenのセッション、Datadogのプロファイラー、AlinaとShaunのGraalVMなどを聴講しました。

それにしても、VM Tech Summitがなかったのがイタイ。

来年は、VM Tech Summitがあれば参加するつもりですが、ないのならばやめようかなぁと思うさくらばなのでした。

2024/03/16

Jfokus 2024 その1 準備編

このエントリーをはてなブックマークに追加

2月5から6日にかけて、スウェーデンのストックホルムでJavaのカンファレンスのJfokus 204が開催されました。今回、日本人の参加者は私を含めて3人しかいませんでした。

ぜひ、来年は日本からの参加者が増えるといいなぁということで、Jfokusの備忘録です。

なお、2月3, 4日にはベルギーのブリュッセルでFOSDEMというカンファレンスも開催されています。FOSDEMとJfokusの両方とも参加される方も多いのですが、さくらばはJfokusだけ参加しました。

今回はあまり写真を撮っていないので、会場などの写真はほぼないです。すみません。


Jfokus

Jfokusはスウェーデンのストックホルムで2007年から開催されているJavaのカンファレンスです。VM Tech SummitというVMに特化したイベントも一緒に行っているなど、ちょっとデープなカンファレンスになっています。

しかし、コロナ後はVM Tech Summitが開催されておらず、普通の大規模なJavaカンファレンスになっていました。今年は、事前にはVM Tech Summitもあると言われていたのですが、結局なかったらしいです。かなり残念。

www.jfokus.se

参加者は1,000人ぐらい?上述したようにFOSDEMから参加している方も多いようです。

スウェーデンでの開催ですが、セッションは英語です。スウェーデンはTOEICの国別ランキングで常に上位にいる国なので、どこでも英語でOKのようです。


チケット

JfokusはDevoxxのようにチケットの争奪戦になることはないですが、売り切れることもあるので早めに取得するのがいいと思います。また、Devoxxとは異なり、クレジットカードに対応しています。


ストックホルムへ

Jfokusに参加することが決まったら、まずやることは交通手段と宿泊の確保です。


空路

ストックホルムはアーランダ空港とスカブスタ空港がありますが、ほとんどがアーランダ空港になると思います。ただし、ライアンエアーなどの航空会社はスカブスタ空港をしていますが、まぁ使うことはないと思います。

現状、アーランダ空港への直行便はないので、どこかしらで乗り継ぎを行う必要があります。

スウェーデンはシェンゲン協定加盟国なので、フランクフルトやパリから乗り継ぐ場合は乗り継ぐ場所で入国審査を受ける必要があります。このため、乗り継ぎには余裕をみてスケジュールしたほうがいいです。

イギリスなどシェンゲン協定に加盟していない国はスウェーデンで入国審査を受けます。

今回、さくらばはロンドン ヒースロー空港で乗り継ぎで、アーランダ空港着でした。


ストックホルム市内へ

アーランダ空港からストックホルム市内へは、鉄道のアーランダエクスプレスを使うのが便利です。

ターミナル5に直結したアーランダ北駅と、ターミナル2, 3, 4から利用できるアーランダ南駅とストックホルム中央駅を結ぶ鉄道で、だいたい40分ぐらいでストックホルム中央駅に着きます。

チケットは空港でも購入できますが、事前にオンラインで購入する方がいいと思います。オンラインで購入すると、QRコードが発行されるので、それを駅でスキャンすればOKです。

列車内で検察にくることもあります。

www.arlandaexpress.com


宿泊

Jfokusの会場はストックホルム中央駅に直結したStockholm Waterfront Congress Centreという場所で開催されます。

なので、中央駅近辺でホテルを探すのがいいと思います。

一番いいのは会場と同じ建物にあるRadisson Blu Waterfront Hotelですが、周りのホテルに比べると宿泊費は高めです。なお、すぐそばに同じ系列のRadisson Blue Royal Vikingというホテルもあるので、お間違えなく。

Radisson Blue Waterfront Hotelは新しい建物なのですが、中央駅の近辺は古い建物が多いのでホテルも古いところが多い感じです。ちょっと離れると新しいところもあるので、会場まで近いという利便性をとるか、施設のよさをとるかのどちらかですね。

今回、さくらばはFreys Hotelに宿泊しましたが、ここもかなり古い建物でした。


気候

北欧と聞いたらやっぱり寒いと思いますよね。

今年のJfokus会期中は一番寒い日で最低気温-10度、最高気温-3度ぐらいでした。

Jfokusの前の週がかなり暖かくて、前々週は最低気温が-20度になるぐらいの寒さだったようです。

したがって、行くとしたら、最低気温が-20度になっても耐えられるぐらいの服装で!今年もそこまで寒いというわけではなかったですが、日本からの参加者の1人が寒さで風邪をひいてしまっていました。せっかくカンファレンスに参加するのですから、体調を崩さないように、万全の体制で挑むようにしましょう。

前週が暖かったということから、今年は街中はほとんど雪は残ってませんでした。昨年は雪も残っていて、道がアイスバーン化しているところもあったらしいので、靴もそれ用に用意したほうがいいと思います。

ストックホルムは湖に面した街ですが、最低気温が-10度にもなると湖も運河もカチカチに凍りますね。


Jfokus会場

Jfokusの会場は前述したようにWaterfront Congress Centreです。ストックホルム中央駅からほぼ直結してます。

ただ駅の裏側なので、ちょっと分かりにくいかもしれません。

古い建物が並ぶストックホルムの中心街の中で、ここだけは妙にモダンな建物になっています。

運河に面している建物ですが、運河の向かい側は市庁舎です。私は知らなかったのですが、魔女の宅急便に出てくる時計台の尖塔は、この市庁舎がモデルらしいです。

入り口にはクロークがあるので、上着はここで預けられます。会場は暖かく、上着は邪魔になるだけなので、預けた方がいいと思います。


食事

Jfokusは、朝食とランチが提供されます。

朝食にはスウェーデン名物でもあるカネルブッレ(シナモンロール)やカルダモンマブッレ(カルダモンロール)、オープンサンドのスモーブローなどが出されます。

昼は日によって異なりますが、スウェーデン料理のプレート。けっこうおいしいです。

また、会期2日目の夜は展示会場でレセプションがあり、軽食がでます。ただ、おなかがいっぱいになるほどではないですね。

気をつけなくてはいけないのが、レストランが意外と早い時間にしまってしまうこと。行くところが決まっているのであれば、予約してからいくのがいいようです。

逆に早朝からやっているカフェやイートイン併設のパン屋さんは多いんですけどね。


後編では、さくらばがJfokusで聴講したセッションの中から面白かったものについて紹介します。

2024/02/02

なぜあなたはラムダ式が苦手と感じるのか

このエントリーをはてなブックマークに追加

今年もブリ会議で講演してきました。例年、Javaの新しい機能などについて話すことが多かったのですが、今年はProject Lambdaから10年ということもあり、あらためてラムダ式についての話です。

2部構成で前半が初心者向けの「なぜあなたはラムダ式を苦手と感じるのか」というラムダ式を使う立場での話。後半はぐっと難易度があがって、「ラムダ式はどうやって動くのか」という内容です。

このエントリーはブリ会議で話した内容の前半部分の解説です。資料はこちら。


ラムダ式が導入されたのは2014年、Java 8の時です。今年でちょうど10年ですね。

10年もたったのですから、ラムダ式を当たり前のように使っている開発者も多いとは思いますが、いまだに苦手と感じている方がいらっしゃるのも事実だと思います。

ラムダ式が導入後にJavaを使い始めた方でも苦手という方がいるんですよね。

ラムダ式は、Javaで関数型プログラミングの考え方が導入された端緒です。ラムダ式と一緒に導入されたStream APIをはじめ、今では関数型プログラミングに関する機能がJavaではどんどん増えています。

たとえば、言語仕様では

  • switch式
  • Pattern
  • Sealed Class
  • パターンマッチング

など。PatternとSealed Classは、両方を組み合わせて代数的データ型 (Algebraic Data Type, ADT) を構成するのに使われます。

標準のAPIだと、次のようなAPIがあります。

  • Stream API
  • Flow (Reactive Stream)
  • HTTP Client

一番使われているのはもちろんStream APIですね。HTTP Clientは、Java 11で導入されましたが、Flowを使用して宣言的にHTTPのクライアントを記述します。

標準API以外でも、Spring WebFluxや、Oracle Helidon、Red HataのRed Hat Quarkusなど関数型プログラミングの考えを使うライブラリやフレームワークも増えています。

つまり、これからはJavaを使っていても関数型プログラミングからは逃れられなくなっているのです。

関数型プログラミングの考えを取り入れたプログラミングスタイル、つまり今までの手続き的な記述から宣言的な記述に変えていかなくてはなりません。

さらにいうと、今までのJavaはどうしても文で考えがちなのですが、そうではなく式を使って記述することを意識していきたいのです。


ラムダ式とは

さて、そのラムダ式ですが、端的にいえば関数です。名前はないので、無名関数ということができます。

無名関数といっても、メソッドと同じようなものです。Java Language Specificationのラムダ式の説明の一番はじめには、次のような記述があります。

A lambda expression is like a method: it provides a list of formal parameters and a body - an expression or block - expressed in terms of those parameters. (JLS 15.27)

つまり、メソッドが書ければラムダ式は書けるはずなのです。

もちろん、ラムダ式は通常のメソッドとは異なり、クラスに属していなかったり、ラムダ式の外側のクラスの状態にアクセスすることも制限されていたりするので、メソッドと同じというわけではありません。

しかし、それは今までの手続き的な記述にとらわれてしまっているからではないでしょうか。


手続き的な記述の欠点

手続き的な記述には読みにくいポイントや、バグを発生しやすくしてしまうポイントがあります。

代表的なポイントを以下に示しました。


1点目の複数の処理を一緒に書けてしまうというのは、処理を十分に分解せずにまとめて書きがちだということです。

残りの2点は変数に関することです。手続き的な記述だと、どうしてもy処理の途中経過を保持するなどの変数が使われます。途中経過を保持させるので、ミュータブルになってしまうわけです。

また、途中経過を保持させて後に、最終的な結果を得るためには別のスコープに入ることがあり、スコープが広くなってしまいがちです。

言葉で書いても分かりにくいと思うので、具体的なコードで見てみましょう。

たとえば、A組の生徒の平均値を算出することを考えてみます。

生徒の成績は次のRecordで保持しているとします(通常のクラスでもいいのですが、こういうデータを保持させるにはイミュータブルなRecordの方が適しています)。

    record StudentScore(
            String name,
            String className,
            int score) {}

StudentScoreでは生徒の名前と、クラス、成績を保持させています。

平均値を求めるメソッドを次に示します。

    double calcAverage(List<StudentScore> scores) {
        double sum = 0.0;
        int count = 0;
 
        for (int i = 0; i < scores.size(); i++) {
            var ss = scores.get(i);
            if (ss.className().equals("A")) {
                sum += ss.score();
                count++;
            }
        }

        sum /= count;

        return sum;
    }

一見、よさげに見えますが、何が問題なのでしょう。

まず、ローカル変数のsumとcountです。この2つの変数はいずれもループでの中間値を保持させるためにミュータブルになっています。

また、ループが終わった後の最終的な結果を求めるために、ループの外側でも変数を使用します。このため、変数のスコープがメソッド全体になってしまっています。

ミュータブルだと意図しない値の変更がされる可能性があります。このメソッドは行数が少ないからよいですが、行数が多いメソッドを複数人で編集していたりすると意図しない変更が起こりがちです。

スコープが広い変数だとなおさらです。

たとえば、このメソッドでは最後にsumをcountで割って平均値を求めていますが、変数sumはループの中では合計値を保持させています。合計値を保持させる変数であるのに、最後に合計値ではない値を代入しているわけです。

平均値を代入した後に、他の人が合計値だと思って変数sumを使ってしまうとバグが発生してしまいます。


そして、ループです。

ループというか、繰り返し処理ってやっぱり分かりにくいと思うんですよ。

慣れてしまえばパターンとして覚えてしまうので、パッと書けるとは思いますが、初心者には難しい。

ここでのたった7行のループで、ループの制御、値の取り出し、比較、合計処理、カウンターのインクリメントまでやっています。特にループカウンターを使ったループの制御は本来の平均を求める処理とは別個のものなので、一緒にしてしまうと分かりにくくなってしまいます。

ここでは普通のfor文で書きましたが、for-each (拡張for文)で書いたとしても本質的な難しさは変わりません。

ループの制御はコンテナ側に任せ、ループで扱うデータに対し何を行っていくのかを分解して考えることで分かりやすさ、読みやすさは格段に向上します。

これが宣言的に記述するということにつながります。

for文という文ではなく、式で処理を連ねていくわけです。


宣言的な記述

先ほどの平均値を求めるメソッドを宣言的に記述したのが以下のコードです。

    double calcAverage(List<StudentScore> scores) {
        final var ave = scores.stream()
                              .filter(ss -> ss.className().equals("A"))
                              .collect(Collectors.averagingDouble(ss -> ss.score()));
        
        return ave;
    }

この記述には、return以外は文が使われておらず、式で処理を記述しています。

そして、ローカル変数のaveはイミュータブルな変数になります。

Stream APIを使ったループはいわゆる内部イテレータになり、ループの制御はStream APIが行います。Stream APIを使う側は、データをどのように処理するかだけに集中し、それをラムダ式で記述します。

行っている処理自体は手続き的に記述しても、宣言的に記述しても同じです。

しかし、Stream APIを使うことで、条件、値の取り出しなどの処理をそれぞれ1つのラムダ式で記述することで、処理の流れが分かりやすくなります。

とはいっても、今まで手続き的な記述しかしてこなかった方には、宣言的な記述はとっつきにくい感じを受けてしまうのはしかたないと思います。

だからといって、今までの手続き的な記述に固執していたら、どんどん増えている宣言的スタイルのライブラリやフレームワークを使えなくなってしまいます。

いつかは宣言的な考え方をしなくてはいけないのであれば、今がそのチャンスです。


重要なのはシグネチャー

前述したように、ラムダ式は関数として扱うのが自然です。

しかし、関数型インタフェースがとかを考え始めてしまうと、なかなかとっつきにくくなります。

Javaにも関数型があればこんなことに悩む必要はないのですが、残念ながらJavaでは関数型はありません。しかたないので、関数型インタフェースなんてものを持ち出したわけです。

しかし、ラムダ式を関数と考えるのであれば、型はそれほど重要ではありません。

重要なのはシグネチャーです。つまり

  • 引数の個数と、その型
  • 戻り値の有無と、その型

が重要になります。引数や戻り値の型はジェネリクスの型パラメータで定義されるので、引数の個数や戻り値の有無から考えましょうということです。

たとえば、数値を保持しているリストをソートするには以下のように記述します(もっと簡単に書けますけど、説明のためこう書いてます)。

    List<Integer> nums = ...;

    var sortedNums = nums.stream()
                         .sorted((x1, x2) -> x1 - x2)
                         .toList();

sortedメソッドの引数のラムダ式で要素同士を比較して、ソートの並び順を決めています。

このラムダ式はComparator<S>インタフェースなのですが、実際にはBiFunction<S, S,  Integer>インタフェースだとしても、処理はまったく同じです。

つまり、sortedメソッドの引数にするラムダ式では2つの引数が渡されて、戻り値としてintで戻すということが重要になるわけです。


java.util.functionパッケージのインタフェース

ラムダ式は関数で、重要なのはシグネチャということですが、ではFunctionインタフェースなどのjava.util.functionパッケージで提供されている関数型インタフェースはどのように考えればよいのでしょう。

インタフェース自体の用途などは気にせずに、シグネチャーを区別するためのものと割り切ると理解しやすいです。

つまり、シグネチャーが

  • 引数なし、戻り値ありならば Supplier<T>
  • 引数が1つ、戻り値ありならば Function<T, R>
  • 引数が1つ、戻り値がbooleanならば Predicate<T>
  • 引数が1つ、戻り値はなしならば Consumer<T>

のように考えるわけです。

たとえば、Streamインタフェースのmapメソッドの引数はFunctionインタフェースですが、その場合は引数が1つで、戻り値ありのラムダ式を書けばいいのだなと分かるわけです。


ちなみに、ラムダ式を関数型インタフェースの匿名クラスの延長として考えてしまうのは危険です。それだと、いつまでたっても手続き的な考え方に執着してしまいます(実際に動作としても匿名クラスとラムダ式はまったく異なるのですが、それはブリ会議の後半のエントリーで説明します)。

そんな変なことを考えずに、ラムダ式は関数として考えましょう。そして、ラムダ式を書く時にはシグネチャーから処理を記述していきましょう。


ラムダ式を書く時のTips

ここまでラムダ式は関数として扱いましょうということを説明してきたわけですが、では実際にラムダ式を書く時にどうすればよいでしょう。

ラムダ式は単独で使うということはほぼなく、ほとんどがライブラリやフレームワークのメソッドの引数として使います。

ということはラムダ式を単体で考えるのではなく、ライブラリやフレームワークと合わせて一緒に考えればよいということです。

ちなみに、メソッドの引数や戻り値に関数を使用することを高階関数と呼びます。名前はどうでもいいのですが、メソッドの引数にラムダ式というのがラムダ式の主流の使い方ということです。

Project LambdaのスペックリードのBrian GoetzはJava Magazineのインタビューで次のように語っています。

Project Lambdaは単なる言語機能ではなく、ライブラリも対象としています。言語機能とライブラリが一体となって、Javaプログラミング・モデルを大幅にアップグレードします。 (Project Lambdaの展望, Java Magazine Oct. 2012)

ラムダ式を策定したProject Lambdaではラムダ式とStream APIを合わせて策定しています。

このようにラムダ式単独ではなく、ライブラリやフレームワークと一緒に使うことを前提にラムダ式を考えていきましょう。


次の処理は1つだけというのは、ラムダ式はなるべくシンプルにしましょうということです。ラムダ式のボディにいろいろ処理を書いてしまうというのは、処理を正しく分解できていないということにつながり、どうしてもラムダ式のボディが手続き的になってしまいます。

処理を分解して、1つのラムダ式には単純な処理を記述し、それを連ねていくというスタイルに変えていきましょう。


3つめはラムダ式で扱うデータをなるべく引数だけにするということです。

ラムダ式は、ラムダ式が定義されている外側のクラスのフィールドなどにもアクセスできますが、それは可読性の低下につながります。もし、外部のデータにアクセスするのであれば、定数などに限定しましょう。


4つめの処理結果を戻り値以外で戻さないというのは、3つめの外部のデータにアクセスしないにも通じます。関数なのですから、結果は戻り値だけです。


最後はラムダ式だけでなく、宣言的な記述全般にいえることですが、変数はイミュータブルにしましょう。


まとめ

さて、前半のまとめです。

なんども書いていますが、ラムダ式は関数としてあつかい、宣言的な記述を行うために役立てるのがお勧めです。

匿名クラスの延長として考えてしまうと、今までの手続き的な考えの枠内にとどまってしまい、宣言的な記述を書くことの妨げになってしまいます。

Javaは今までの手続き的な記述から、宣言的な記述にプログラミングスタイルが変わってきています。今後もこの流れは変わりません。

ですから、手続き的な考えでラムダ式を理解しようとすることはやめましょう。

宣言的な記述、文から式への移行を役立てるためにラムダ式が存在するのです。


最後に参考文献にあげた「なっとく! 関数型プログラミング」を紹介しておきます。

この本はJavaからScalaへの移行を解説した書籍です。

しかし、手続き的なJavaから、宣言的なJavaへ移行するにも役立つはずです。

トピックはだいたい1ページで収まっており、読みやすいのもいいです。

ただ、Kindleだと画像の解像度が低くて、ちょっと読みにくいのが欠点です。翔泳社さん、どうにかしてくれないですかねぇ。

www.amazon.co.jp


さて、ブリ会議で後半に解説したラムダ式がどのように動作するのかについては、次エントリーで紹介する予定です。