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 22で追加された廃止予定の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パッケージ

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

ClassFileFormatVersion列挙型

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式
  • Record
  • Sealed Class
  • パターンマッチング

など。Recordと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


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


2023/12/25

ヒープだけでコンパイル&クラスロード (Compiler API補遺)

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

本エントリーはJava Advent Calendarシリーズ2の最後のエントリーです。

qiita.com

ここまで、3回に渡ってString Templateについて紹介してきました。

www.javainthebox.com

www.javainthebox.com

www.javainthebox.com

また、動的にテンプレートを作るために使ったのがCompiler APIです。

www.javainthebox.com

動的にテンプレートを作れるようになったのはいいのですが、微妙に使いにくい点があります。

たとえば、DynamicTemplate.jarにクラスをまとめて実行してみると。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:21    <DIR>          .
2023/12/23  22:20             3,896 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
               2 個のファイル               3,956 バイト
               1 個のディレクトリ  378,476,552,192 バイトの空き領域

C:\sample>java --enable-preview -cp DynamicTemplate.jar TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Exception in thread "main" java.lang.ClassNotFoundException: Template
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
        at DynamicTemplateBuilder.createTemplate(DynamicTemplateBuilder.java:42)
        at TemplateTest.main(TemplateTest.java:13)

C:\sample>

といように、動的に作成したTemplateクラスをロードできないため、失敗しています。

この理由は簡単で、ワーキングディレクトリに生成されたTemplate.classファイルが出力されてしまっているからです。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:24    <DIR>          .
2023/12/23  22:20             3,896 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
2023/12/23  22:24               728 Template.class
               3 個のファイル               4,684 バイト
               1 個のディレクトリ  378,476,572,672 バイトの空き領域

C:\sample>

しかし、クラスパスはJARファイルしか指定していないため、クラスロードできなくなってしまいます。

これを動作させるためには、ワーキングディレクトリもクラスパスに追加すればOKです。

C:\sample>java --enable-preview -cp .;DynamicTemplate.jar TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Bob Dylan様
いつもお世話になっております

C:\sample>

このように実行はできるものの、クラスパスを毎回指定しなければいけないのは、ちょっとめんどうです。

そもそも自動生成したクラスなのですから、クラスファイルをファイルとして出力せずにヒープ内でどうにかしてくれないかなぁと思うわけです。

ところが、そんなことをしてくれる機能が実はすでにあるんです。

たとえば、実行すると単にSystem.outに"Hello, World!"を出力するHelloWorldクラスがあったとします。Java 11から、1つのJavaファイルであればコンパイルせずに直接実行できるようになっています。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:53    <DIR>          .
2023/12/23  22:53               123 HelloWorld.java
               1 個のファイル                 123 バイト
               1 個のディレクトリ  378,463,444,992 バイトの空き領域

C:\sample>java HelloWorld.java
Hello, World!

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:53    <DIR>          .
2023/12/23  22:53               123 HelloWorld.java
               1 個のファイル                 123 バイト
               1 個のディレクトリ  378,464,772,096 バイトの空き領域

C:\sample>

実行してみれば分かりますが、Javaファイルを直接実行した場合、HelloWorld.classファイルは出力されていません。

この機能はコンパイルした結果をヒープに保持し、クラスロードはヒープから行うということを行っているからです。

この機能を行っているのが、java.compilerモジュールのcom.sun.tools.javac.launcherパッケージのクラス群です。

なので、このパッケージにあるクラスを使えば、DynamicTemplateもクラスファイルを出力せずに実行することができるはず... なのですが、残念なことにcom.sun.tools.javac.launcherパッケージは外部に公開されていないパッケージなのです(module-info.javaでexport指定されていないのです)。

しかたないので、これらを参考に作ってみましょう。

ここで作成したファイルはGistにおいておきます。


Gist: 動的にString Templateのテンプレートを作成し、ヒープだけでコンパイル、クラスロードまで行う例


まずはJavaFileManagerインタフェースです。

通常はStandardJavaFileManagerインタフェースを使用しますが、部分的に機能を拡張するために提供されているのがForwardingJavaFileManagerクラスです。

ForwardingJavaFileManagerクラスは、コンストラクタでベースとなるJavaFileManagerオブジェクトを指定します。そして、このベースとなるJavaFileManagerオブジェクトに処理を委譲するようになっています。

機能を拡張したい部分だけメソッドをオーバーライドすればいいというわけです。

ここでは、バイトコードを出力するためのgetJavaFileForOutputメソッドをオーバーライドします。

    private Map<String, byte[]> classBytes;

    public Map<String, byte[]> getClassBytes() {
        return classBytes;
    }

    private class ClassOutputBuffer extends SimpleJavaFileObject {
        private final String name;

        ClassOutputBuffer(String name) {
            super(toURI(name), Kind.CLASS);
            this.name = name;
        }

        @Override
        public OutputStream openOutputStream() {
            return new FilterOutputStream(new ByteArrayOutputStream()) {
                @Override
                public void close() throws IOException {
                    out.close();
                    ByteArrayOutputStream bos = (ByteArrayOutputStream)out;
                    classBytes.put(name, bos.toByteArray());
                }
            };
        }
    }

    @Override
    public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
                                    String className,
                                    Kind kind,
                                    FileObject sibling) throws IOException {
        if (kind == Kind.CLASS) {
            return new ClassOutputBuffer(className);
        } else {
            return super.getJavaFileForOutput(location, className, kind, sibling);
        }
    }

出力用にSimpleJavaFileObjectクラスを派生させたClassOutputBufferクラスを用意して、それを出力用に使用します。

ClassOutputBufferクラスでは出力にByteArrayOutputStreamクラスを使用しているため、バイト配列に結果が出力されるわけです。このバイトコードはマップのclassBytesにクラス名と一緒に保持しておきます。

classBytes変数はgetClassBytesメソッドで取り出すことができます。


続いて、クラスローダーの方です。

バイト配列を利用してクラスロードするMemoryClassLoaderクラスを定義しました。このMemoryClassLoaderクラスはURLClassLoaderクラスのサブクラスになっています。

public final class MemoryClassLoader extends URLClassLoader {
    private final Map<String, byte[]> classBytes;

    public MemoryClassLoader(Map<String, byte[]> classBytes) {
        super(new URL[]{});
        this.classBytes = classBytes; 
    }

    @Override
    protected Class findClass(String className) throws ClassNotFoundException {
        byte[] buf = classBytes.get(className);
        if (buf != null) {
            classBytes.put(className, null);
            return defineClass(className, buf, 0, buf.length);
        } else {
            return super.findClass(className);
        }
    }
}

MemoryJavaFileManagerクラスで作られたバイトコードを保持したマップをコンストラクタで指定するようにしてあります。

findClassメソッドではマップのclassBytesにクラスがあれば、バイトコードであるバイト配列を取り出して、defineClassメソッドに渡すようにしています。

これで、ヒープからクラスロードができます。


最後にDynamicTemplateクラスの変更点を示します。

オレンジで示したところが、MemoryJavaFileManagerクラスを使用するところです。その前の行でStandardJavaFileManagerオブジェクトを取得しておき、そのオブジェクトをMemoryJavaFileManagerクラスのコンストラクタ引数に使用します。

これで、MemoryJavaFileManagerクラスで変更した部分以外の機能はStandardJavaFileManagerオブジェクトに委譲するようになります。

青字の部分がMemoryClassLoaderクラスに変更した部分です。先ほどのMemoryJavaFileMangerオブジェクトでバイトコードを保持させているマップをgetClassBytesメソッドで取得して、コンストラクタ引数にしています。

        // 仮想ファイルマネージャの取得
        try (StandardJavaFileManager fm
                = compiler.getStandardFileManager(null, null, null);
                var fileManager = new MemoryJavaFileManager(fm)) {
            
            
            // コンパイルするファイルの準備
            List<? extends JavaFileObject> fileobjs
                    = createJavaFileObjects(template, argName);

            // コンパイルタスクの生成
            JavaCompiler.CompilationTask task
                    = compiler.getTask(null,
                            fileManager,
                            null,
                            List.of("--release", "22", "--enable-preview"),
                            null,
                            fileobjs);

            // コンパイル
            if (task.call()) {
                // クラスのロード
//                ClassLoader loader = ClassLoader.getSystemClassLoader();
                ClassLoader loader = new MemoryClassLoader(fileManager.getClassBytes());
                Class<?> clss = loader.loadClass("Template");


では、実行してみましょう。

C:\sample>java --enable-preview -cp DynamicTemplate.jar TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Bob Dylan様
いつもお世話になっております

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:24    <DIR>          .
2023/12/23  23:18            16,248 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
               2 個のファイル              16,308 バイト
               1 個のディレクトリ  378,393,817,088 バイトの空き領域

C:\sample>

クラスパスにワーキングディレクトリーを指定せずに実行することができました。

また、Template.classも出力されていません!

これで、ソースが文字列、バイトコードはヒープに作成することができ。また、ヒープ上に保持してあるバイトコードを利用してクラスロードをすることもできました。


動的にコード生成をする場合、ヒープだけでバイトコードを保持し、クラスロードできるようになれば、クラスパスなどを気にすることがなくなり、使う上でのハードルは下がるはずです。

ここでは、String Templateを例にとりましたが、汎用に使えるテクニックなので、動的にコードを生成したい場合にはぜひご活用ください。


2023/12/22

String Templateによる文字列補間 その3 - 動的なテンプレート作成

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

本エントリーはJava Advent Calendarシリーズ2の22日目のエントリーです。

qiita.com

ここまで、String Templateの基本的な使い方と、カスタムテンプレートプロセッサーを紹介してきました。

www.javainthebox.com

www.javainthebox.com

String Templateは便利なので、チョコチョコと使っているのですが、1つだけ気にくわないところがあります。

それはテンプレートに文字列リテラルかテキストブロックしか使えないところです。

一般的なテンプレートエンジンであれば、外部のHTMLファイルなどをテンプレートとして使用することができます。

ところが、String Templateではそれができないのです!

STR.process("Hello, \{name}!");とか書ければいいんですけどね。


できないならば、作ってしまおうというのが本エントリーです。

JJUG CCCで使用した資料では32ページぐらいからの内容です。


テンプレート引数に文字列リテラルかテキストブロックしか使えないのであれば、任意の文字列を文字列リテラルにしてしまうクラスを実行中に作ってしまえばいいのです。

クラスを動的に作成するのは、現在のJavaでは当たり前に行われてます。ラムダ式をはじめ、いろいろなところでクラスの動的生成が使われています。

だったら、普通のアプリケーションでもクラスを動的に作成して、実行できるようにしておけばいろいろと拡張を考えることができるはず!

さて、実行中にテンプレート引数を持つクラスを作って、String Templateを実行するには道具として次の3種類のAPIが必要です。

  • Compiler API
  • クラスローダー
  • リフレクション、もしくはMethodHandle

Compiler APIは前回のエントリーで解説したAPIです。

それ以外の、クラスローダーやリフレクションは当たり前のように使われているAPIですよね。最近はリフレクションの代わりにMethodHandleクラスを使うことが多くなってきていますが、ここでは説明を省くためにリフレクションを使います。

手順はこんな感じ。

  1. 文字列(テキストブロック)でクラスのひな型を用意する
  2. テンプレート引数にする文字列をひな型に埋め込む
  3. クラスをコンパイル
  4. コンパイルしたクラスをクラスロード
  5. リフレクションを使って、クラスのメソッドを実行

前回、文字列でクラスを記述して、それをコンパイルする手法を紹介しましたが、ここで使うわけです。

今回使用したソースコードはGistにアップしてあるので、全体を見たい方はそちらを参照してください。

String Templateのテンプレートを動的に作成する方法


では、まず文字列でクラスを作成してみましょう。


動的にクラスを作成し、コンパイル

前回のCompiler APIの紹介で文字列をソースにする方法で使用したStringJavaFileObjectクラスを使用して、ソースのひな型を作ります。

これを行っているのが、createJavaFileObjectsメソッドです。

    private static List<? extends JavaFileObject> createJavaFileObjects(String template, String argName) {
        // Java のソースとなる文字列
        String templateSrc = STR."""
            public class Template {
                public static StringTemplate process(Object \{ argName }) {
                    return java.lang.StringTemplate.RAW.\"\"\"
                        \{ template }
                        \"\"\";
                }
            }
	    """ ;

        // 文字列をソースとする StringJavaFileObject オブジェクトを
        // 生成する
        JavaFileObject fileobj
                = new StringJavaFileObject("Template", templateSrc);

        return List.of(fileobj);
    }

createJavaFileObjectsメソッドの第1引数がテンプレートとなる文字列、第2引数がテンプレートに埋め込まれる変数の名前です。

ここでは、動的にTemplateクラスを作成して、Templateクラスのprocessメソッドをコールすると、StringTemplateオブジェクトができるようにします。

processメソッドの引数は動的に作成したテンプレートに埋め込む変数で、どのような型か指定することはできないので、Objectクラスにしてあります。

このTemplateクラスには2か所変数を埋め込んであり、それをString Templateで処理しています。1つがprocessメソッドの引数名、次がRAWで処理するテキストブロックです。

ここでRAWを使用しているのは、RAWであればこの後に任意のテンプレートプロセッサーでもう1度、文字列補間処理を行うことができるからです。

それにしても、StringTemplateオブジェクトを生成するために、String Templateを使うというちょっとメタ的な使い方なので、ちょっとこんがらがりますね。

このクラスの元となる文字列ができてしまえば、後はそれを使用してStringJavaFileObjectオブジェクトを生成し、リストにして返します。


さて、このcreateJavaFileObjectsメソッドをコールしている方です。

ソースをコンパイルするのは前回紹介した方法そのままです。

        // コンパイラの取得
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            return Optional.empty();
        }

        // 仮想ファイルマネージャの取得
        try (StandardJavaFileManager fileManager
                = compiler.getStandardFileManager(null, null, null)) {

            // コンパイルするファイルの準備
            List<? extends JavaFileObject> fileobjs
                    = createJavaFileObjects(template, argName);

            // コンパイルタスクの生成
            JavaCompiler.CompilationTask task
                    = compiler.getTask(null,
                            fileManager,
                            null,
                            List.of("--release", "22", "--enable-preview"),
                            null,
                            fileobjs);

            // コンパイル
            boolean result = task.call();


クラスロードと実行

クラスのコンパイルが成功したら、次にコンパイルしたクラスをロードし、リフレクションで実行します。

            if (result) {
                // クラスのロード
                ClassLoader loader = ClassLoader.getSystemClassLoader();
                Class<?> clss = loader.loadClass("Template");

                // Method オブジェクトを取得し、リフレクションで実行する
                Method method = clss.getMethod("process", Object.class);
                StringTemplate dynamicTemplate
                        = (StringTemplate) method.invoke(null, new Object[]{variable});

                return Optional.of(dynamicTemplate);
            } else {
                return Optional.empty();
            }

クラスロードにはシステムクラスローダを使用しているので、実行するときは動的に生成したクラスファイルがクラスパスに含まれるようにしてください。

クラスがロードできれば、そのprocessメソッドをClassクラスのgetMethodメソッドで探索します。getMethodメソッドの引数はメソッド名と、可変長引数で引数の型です。

processメソッドを探すので、第1引数は"process"、第2引数はprocessメソッドの引数の型であるObject.classを指定します。

processメソッドを表すMethodオブジェクトが取得できれば、後は実行するだけです。

processメソッドはstaticメソッドなので、invokeメソッドの第1引数はnullにしておきます。


コンパイル、クラスロード、リフレクションはスローされる例外がいろいろあり、失敗することもあるので戻り値はOptionalクラスにしてあります。

成功すればOptional.ofメソッドでStringTemplateオブジェクトを戻し、失敗したらOptional.empty()を戻すようにしました。


さて、動的にテンプレートを記述できるようになったので、試してみましょう。

ここではテンプレートをhello.tempというファイルに記述しました。

\{variable}様
いつもお世話になっております

メールの先頭によくありがちな定型文です。

このファイルを読み込んで、StringTemplateオブジェクトを生成します。

        // ファイルからテンプレートを読み込み
        String template = new String(Files.readAllBytes(Path.of("hello.temp")));
            
        // 動的テンプレート生成
        // 第1引数: テンプレート 
        // 第2引数: テンプレートに埋め込む変数名
        // 第3引数: テンプレートに埋め込む値
        Optional<StringTemplate> dynamicTemplate
            = DynamicTemplateBuilder.createTemplate(template, "variable", "Bob Dylan");
        
        // 任意のテンプレートプロセッサで処理
        dynamicTemplate.ifPresent(t -> {
            var result = STR.process(t);
            System.out.println(result);
        });

ここで、Files.readAllBytesメソッドでファイルを読んだ後に文字列に変換しているのは、改行も含んですべてを文字列にしたかったからです。

Files.readAllLinesメソッドだと行単位に分割されてしまうんですよね。

いつもお世話になっているBob Dylanさんを埋め込んでみました。

実行すると、次のように処理された文字列が表示されるはず。

C:\sample>java --enable-preview TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Bob Dylan様
いつもお世話になっております

ちゃんと、Bob Dylanが埋め込まれました!


試しに作ってみただけなので、テンプレートを作成するたびにクラス生成からコンパイルをしてしまうとか、生成するクラス名が決め打ちなのでStringTemplateオブジェクトを複数生成できないとか、埋め込む値が1つに限定されているなどありますが、できることは確認できました!!

もうちょっと手を入れれば、汎用的に使えるようになるかも。


2023/12/21

Compiler API

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

本エントリーはJava Advent Calendarの21日目のエントリーです。

qiita.com


もともとの予定ではString Template その 3として、ちょっとマニアックな使い方を紹介するつもりだったのですが、そこで使用するAPIのCompiler APIをまず紹介しておきます。


Compiler APIは名前の通り、コンパイラーを使うためのAPIで、Java 6で導入されました。Javaのコンパイラーといえばjavacです。これをプログラムの中から使うためのAPIになります。

ところで、プログラム中にソースコードをコンパイルすることがあるのでしょうか?

 古いところだと、JSPがそうです。

JSPはHTML中にJavaのコードを埋め込んだようになっていますが、サーブレットエンジンによってJavaのサーブレットに変換され、動的にコンパイルして実行されます。

最近だと(と言っても、すでに10年前ですが)、ラムダ式がそうです。ラムダ式は実行中にソースコードが生成され、コンパイルして実行されます。

この動作はJava 7で導入された新しいバイトコードのInvokeDynamic(通称 indy)によるものです。

他にもindyを使って動的にソースコード生成し、コンパイルしてから実行されるものとして、文字列の + 演算子などがあります。

以前は、+ 演算子を使用した文字列連結は、コンパイル時にStringBuilderクラスを使用するコードに置き換えられていましたが、今は違います(StringBufferクラスを使っていたのは、さらに前の話です)。

今はindyを使用して、動的にソースコードを生成、コンパイル、実行と進みます。

このように、アプリケーションの動作中にJavaのコードをコンパイルするために使われるのがCompiler APIです。


ところで、Compiler APIは前述したようにJava 6で導入されたのですが、JSPなどJava 6以前からあるものはどうやってコンパイルしていたのでしょう。

その答えは、javacを直接コールしていたです。

意外かもしれませんが、javacはJavaで書かれていて、そのメインクラスはcom.sun.tools.java.Mainクラスです。モジュールが導入される前はtools.jarに含まれていましたが、今はjdk.compilerモジュールに含まれています。

このMainクラスのmainメソッドをコールすればコンパイルできます。当然ですが、mainメソッドの引数は、javacの引数です。

たとえば、Hello.javaをコンパイルするのであれば、次のように記述します。

import com.sun.tools.javac.Main;
 
public class CompileTest {
  public static void main(String... args) {
    try {
      String[] javacArgs 
          = new String[] {"Hello.java"};
      Main.main(javacArgs );
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}

では、実行してみましょう。

C:\sample>dir /b
CompileTest.class
CompileTest.java
Hello.java

C:\sample>java CompileTest

C:\sample>dir /b
CompileTest.class
CompileTest.java
Hello.class
Hello.java

Hello.javaがコンパイルされて、Hello.classが生成されました。

このように、javacを呼び出すのと同じことをプログラム中でできました。

では、なぜCompiler APIが必要なのでしょうか?

理由はいくつかあります。

  • Mainクラスのパッケージがcom.sun.tools.javacであり、今後公開され続けるとは限らない
  • コンパイルエラーが扱いにくい
  • コンパイル対象がファイルに限定される

java.compilerモジュールではMainクラスが含まれているcom.sun.tools.javacパッケージはexportされています。ただし、パッケージがcom.sunで始まることから分かるように、このパッケージは内部で使用すること想定しているパッケージです。このため、今後このパッケージがexportされなくなることも考えなくてはいけません。

また、Eclipseなどが提供しているjavacコンパイラなどを使用することもできません。

それよりも、標準的なインタフェースを定義して、実装を隠す方が使いやすくなります。

その他の2つの理由はjavacが標準出力やファイルを対象としているため、しかたありません。とはいえ、特に最後の理由はソースコードをプログラム中で自動生成してコンパイルする場合にちょっとやっかいです。

ということで、Compiler APIが導入されたわけです。

ただ、いろいろと柔軟な使い方ができるようになった一方で、コード量が増えてしまうのはいかんともしがたいところではあります。


Compiler API

Compiler APIでは、ソースコードをコンパイルするために、下に示したインタフェースを使用します。

  • コンパイラ: JavaCompilerインタフェース
  • コンパイルタスク: JavaCompiler.CompilationTaskインタフェース
  • 仮想ファイルマネージャ: JavaFileManagerインタフェース
  • 抽象ソースファイル: JavaFileObjectインタフェース
  • エラー処理: DiagnosticListenerインタフェース

これらのインタフェースはいずれもjava.compilerモジュールのjavax.toolsパッケージで定義されています。

ソースコードのコンパイルは、以下のような流れになります。

  1. JavaCompilerオブジェクトの取得
  2. 必要に応じてコンパイルエラー処理コールバックを記述
  3. 仮想ファイルマネージャを生成し、コンパイル対象のソースファイルを取得
  4. ファイルマネージャ、抽象ソースファイル、エラー処理を指定してコンパイルタスクを生成
  5. コンパイル

ここでは、必要最低限の説明にとどめるため、コンパイルエラー処理は省略します。

では、順々に説明していきましょう。


JavaCompilerオブジェクトの取得

Javaのコンパイラを表すJavaCompilerインタフェースは、javax.tools.Toolインタフェースのサブインタフェースです。

Toolインタフェースはアプリケーション中から何らかの処理を行うための汎用インタフェースで、標準ではJavaコンパイラのJavaCompilerインタフェースと、Javadocを処理するDocletを実行するDocumentationToolインタフェースが提供されています。

実際にコンパイルを行うにはJavaCompilerインタフェースのgetTaskメソッドでCompilationTaskオブジェクトを生成し、CompilationTaskオブジェクトのcallメソッドをコールします。

さて、このJavaCompilationオブジェクトを取得するには、javax.tools.ToolProviderクラスのgetSystemJavaCompilerメソッドを利用します。

ただし、getSystemJavaCompilerメソッドで取得できるのはOpenJDKが提供しているjavacコンパイラです。

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    if (compiler == null) {
      // 例外処理
    }

コンパイラが取得できない場合、getSystemJavaCompilerメソッドはnullを返すので必要に応じて例外処理をしてください。


仮想ファイルマネージャ

続いて、仮想ファイルマネージャを生成し、ソースファイルを用意します。

仮想ファイルマネージャを表すJavaFileManagerインタフェースは汎用のインタフェースなので、物理ファイルを扱う場合はサブインタフェースのStandardJavaFileManagerインタフェースを使用します。

StandardJavaFileManagerオブジェクトはJavaCompilerオブジェクトから取得することができます。物理ファイル以外のソースファイルを使用する場合は、JavaFileManagerインタフェースを実装したクラスを用意します。

ここでは、まず標準的な物理ファイルをコンパイルすることを考えます。

StandardJavaFileManagerオブジェクトを取得するには、JavaCompilerインタフェースのgetStandardFileMangerメソッドを使用します。

getStandardFileManagerメソッドは第1引数がコンパイルエラー処理のコールバックであるDiagnosticListenerインタフェースです。エラー処理を行わないのであれば、nullを指定します。

第2引数がロケール、第3引数が文字セットになります。これらは引数がnullの場合、デフォルトの値が使用されます。

また、JavaFileManagerインタフェースはAutoClosableインタフェースを実装しているので、try-with-resources形式でクローズすることができます。


次に抽象化したソースファイルです。こちらはJavaFileObjectインタフェースを使用します。

通常はソースファイルは1つとは限りません。複数のソースファイルはリストではなくIterableインタフェースで表します。Iterableインタフェースが使われていたのは、そういう時代だったからだと思ってくださいw

このIterableオブジェクトはStandardJavaFileManagerインタフェースのgetJavaFileObjectsメソッドで取得で取得できます。getJavaFileObjectsメソッドは可変長引数なので、ソースファイルを列挙していきます。

    // 仮想ファイルマネージャの取得
    // 第1引数 コンパイルエラー処理コールバック
    // 第2引数 ロケール nullだとデフォルトロケール
    // 第3引数 文字セット nullだとデフォルト文字セット
    try (StandardJavaFileManager fileManager 
      = compiler.getStandardFileManager(null, null, null)) {

      // 抽象ソースファイルの取得
      Iterable<? extends JavaFileObject> files 
        = fileManager.getJavaFileObjects("Hello.java");


コンパイルタスク生成とコンパイル

コンパイルタスクはJavaCompilerインタフェースのgetTaskメソッドで生成します。

このメソッドは引数が6個もあるので、指定するのがちょっとめんどうですが、使わないものはnullを指定しておけば大丈夫です。

CompilationTaskオブジェクトを生成出来たら、callメソッドでコンパイルを行います。コンパイルが成功すればtrueが戻ります。

    // コンパイルタスクの生成
    // 第1引数 コンパイラメッセージの出力 nullの場合System.errが使われる
    // 第2引数 仮想ファイルマネージャー
    // 第3引数 コンパイルエラーのコールバック 指定しない場合はnull
    // 第4引数 コンパイラオプション 指定しない場合はnull
    // 第5引数 アノテーションプロセッサ 指定しない場合はnull
    // 第6引数 ソースファイル群
    JavaCompiler.CompilationTask task = compiler.getTask(
            null, fileManager, null, null, null, files);
    
    // コンパイル
    // 成功すればtrueが戻る
    var result = task.call();

上記のコードではHello.javaをコンパイルしているので、実行すればHello.classが出力されるはずです。


String Templateを含んだソースをコンパイル

コンパイルできるようになったので、String Templateを含んだソースをコンパイルしてみましょう。

Hello.javaを次のようにしたとします。

public class Hello {
  public void sayHello(String name) {
    System.out.println(STR."Hello, \{name}!");
  }
}

これで実行してみましょう。

C:\sample>java CompilerTest
Hello.java:3: エラー: 文字列テンプレートはプレビュー機能であり、デフォルトで無効になっています。
        System.out.println(STR."Hello, \{name}!");
                           ^
  (文字列テンプレートを有効にするには--enable-previewを使用します)
エラー1個

String TemplateはJava 22ではプレビュー機能なので、コンパイルが失敗してしまいました。

これを解決するのは簡単で、CompilationTaskオブジェクトを生成する時に、コンパイルオプションを指定するだけです。

    // コンパイルタスクの生成
    // 第1引数 コンパイラメッセージの出力 nullの場合System.errが使われる
    // 第2引数 仮想ファイルマネージャー
    // 第3引数 コンパイルエラーのコールバック 指定しない場合はnull
    // 第4引数 コンパイラオプション 指定しない場合はnull
    // 第5引数 アノテーションプロセッサ 指定しない場合はnull
    // 第6引数 ソースファイル群
    JavaCompiler.CompilationTask task = compiler.getTask(
            null, fileManager, null,
            List.of("--enable-preview", "--release", "22"),
            null, files);
    
    // コンパイル
    // 成功すればtrueが戻る
    var result = task.call();

--release 22は1つの意味を示すオプションですが、タスクを生成する時は別々に指定するようにします。

これでコンパイルできるはずです。


文字列をソース対象としてコンパイルする

ここまでは、ソースはファイルとして扱っていました。

これに対し、文字列をソースとして扱うこともできます。

ソースファイルを表すJavaFileObjectオブジェクトは仮想ファイルマネージャStandardJavaFileManagerオブジェクトから取得していましたが、文字列をソースとして扱う場合はJavaFileObjectオブジェクトを自分で生成しなくてはなりません。

JavaFileObjectインタフェースを実装したクラスとして、SimpleJavaFileObjectクラスが提供されています。このSimpleJavaFileObjectクラスでは、ソースコードを返すメソッドとしてgetCharContentというメソッドが定義されています。SimpleJavaFileObjectクラスの実装ではUnsupportedOperationException例外をスローするようになっているので、このメソッドをオーバーライドします。

この拡張に関してはJavaCompilerインタフェースのAPIドキュメントに書いてあるので、それをそのまま使います。

public class StringJavaFileObject extends SimpleJavaFileObject {
  private String content;

  public StringJavaFileObject(String className, String content) {
    super(URI.create("string:///"
                    + className.replace('.', '/')
                    + Kind.SOURCE.extension),
          Kind.SOURCE);

    this.content = content;
  }

  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return content;
  }
}

StringJavaFileObjectクラスを使用して、抽象ソースを生成します。

    String HELLOCLASS = """
        public class Hello {
            public void sayHello(String name) {
                System.out.println("Hello, " + name + "!");
            }
        }
        """;

    // 文字列を抽象ソースとして扱う
    Iterable<? extends JavaFileObject> files 
            = List.of(new StringJavaFileObject("Hello", HELLOCLASS));

他の部分は先ほどと同じで大丈夫です。

これで実行すれば、Hello.classが作成されるはずです。

思ったよりも簡単にできたと思いませんか。javacのMainクラスを直接使うよりも、Compiler APIを使うとこのような応用的な使い方が簡単にできるのです。


ということで、Compiler APIの簡単な紹介でした。次回はCompiler APIを使ったString Templateのちょっとマニアックな使い方を紹介します。


ここで使用したソースはGistにのせてあります。

Compiler APIサンプル その1

Compiler APIサンプル その2 文字列をソースにする

2023/12/12

String Templateによる文字列補間 その2 - カスタムテンプレートプロセッサー

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

本エントリーはJava Advent Calendarシリーズ2の12日目です。

 

本エントリーで紹介しているJEP 459は差し戻しになってしまったので、本エントリーの内容は使えなくなりました。また、新しい仕様が策定したら改めて紹介する予定です。

 

qiita.com


前回のエントリーで、String Templateの基本的な使い方を紹介しました。

www.javainthebox.com

基本的にはSTRを使って、\{}を使って式を埋め込めばOKです。ただ、STRの直後にピリオドで文字列という書き方は今までのJavaとは違うので、ちょっと違和感はあるかもしれません。

今日はその後半です。発表資料だとp15ぐらいからの内容です。


文字列補間はたいていのプログラミング言語で使うことができる当たり前の機能です。今までJavaになかったのが不思議なくらい。

しかし、後発だからこそ他にはあまりない機能も盛り込んでいます。それがテンプレートプロセッサーのカスタム化です。

つまり、STRと同じようなテンプレートプロセッサーを簡単に作ることができるのです。本エントリーでは、このテンプレートプロセッサーのカスタム化について説明していきます。


テンプレートプロセッサーの動作

カスタム化について説明する前に、テンプレートプロセッサーの動作を確認しておきましょう。

前回、テンプレートプロセッサーであるStringTemplate.Processorインタフェースのprocessメソッドで文字列補間を行うと紹介しました。

ところで、processメソッドの引数の型はStringTemplateインタフェースです。ということは、どこかでStringTemplateオブジェクトが生成されているはずです。

実をいうと、String Templateは、バイトコードだとinvokeDynamicで実行されます。最終的にはProcessor.processメソッドをコールしますが、その前の処理を含めてinvokeDynamicで処理されています。

実をいうとSTR、RAW、FMT、そしてカスタムテンプレートプロセッサーでInvokeDynamicの動作がすべて異なります。それらをすべて解説するのはちょっと深入りしすぎるので、共通的に行われている処理の手順だけを説明しておきます。

たとえば、次のString Templateの処理を考えてみます。

STR."Hello, \{name}!";

このテンプレート引数である "Hello, \{name}!" は、まず文字列の部分と値の部分にバラバラに分解されます。

"Hello, " name "!"

次に文字列だけを集めたリストfragmentsと、値の部分を集めたリストvaluesに集約されます。

List<String> fragments: "Hello, ", "!"

List<Object> values: name

そして、fragmentsとvaluesを使用してStringTemplateオブジェクトを生成します。

var template = StringTemplate.of(fragments, values);

最後に、生成したStringTemplateオブジェクトを引数にしてテンプレートプロセッサーのprocessメソッドをコールします。

STR.process(template);

重要なのはStringTemplateオブジェクトは文字列部分を集めたリストfragmentsと、値を集めたリストvaluesを持つということです。

テンプレートプロセッサーは、これら2つのリストを使用して文字列の補間を行います。

なお、テンプレート引数の先頭が\{}の場合、fragmentsの先頭は""になります。また、末端が\{}で終わった場合、fragmentsの末尾は""になります。つまり、fragmentsのサイズは必ずvaluesより1つ多くなります。


カスタムテンプレートプロセッサー

テンプレートプロセッサーを自作するには2つの方法があり、下記の2つのメソッドのいずれかを使用します。

  • static <T> Processor<T, RuntimeException> of(Function<? super StringTemplate, ? extends T> process)
  • R process(StringTemplate stringTemplate) throws E

ofメソッドはProcessorオブジェクトを生成するファクトリーメソッドです。引数には文字列補間を行うラムダ式を指定します。

2つめの方法は、Processorインタフェースを実装したクラスを作成して、processメソッドをオーバーライドします。ただし、Processorインタフェースは関数型インタフェースなので、こちらもラムダ式で記述できます。

これらのメソッドの使い分けは、例外を扱うかどうかです。ofメソッドの戻り値の型の2つめの型パラメータはRuntimeException例外に固定されています。

一方のprocessメソッドは型パラメータEが例外を示しており、任意の例外をスローすることができます。

つまり、例外を扱わないでおくにはofメソッド、例外を扱いたいのであればprocessメソッドを使います。

では、まずofメソッドを使う方法から試してみましょう。


ofメソッドを使用したカスタム化

ofメソッドの引数のラムダ式は引数はStringTemplateオブジェクトですが、戻り値は任意に決めることができます。

まずはSTRと同じ動作をする、つまり文字列を戻すテンプレートプロセッサーを作ってみましょう。

StringTemplateインタフェースはfragmentsを戻すfragmentsメソッド、valuesを戻すvaluesメソッドが定義されています。これらのメソッドを使用すれば、STRと同じことができるはずです。

コードはこんな感じです。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> {

        // fragments()とvalues()を使用して文字列補間

        return // 補間結果の文字列を戻す
    });

このラムダ式で文字列補間処理を行います。

STRと同じ処理であるのであれば、fragmentsとvaluesを1つづつ取り出し、文字列連結をすればよいことになります。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> {

        var fragments = st.fragments();
        var values = st.values();

        var builder = new StringBuilder(fragments.get(0));
        for (int i = 0; i < values.size(); i++) {
            builder.append(values.get(i));
            builder.append(fragments.get(i+1));
        }

        return builder.toString();
    });

個人的にはこういうミュータブルなリストを使ったコードはもう書きたくないのですが、Streamインタフェースにzipメソッドがないので、しかたありません。

ところで、ほとんどのテンプレートプロセッサーはfragmentsとvaluesを順々に連結する処理を行います。このため、StringTemplateインタフェースには、この処理を行うinterpolateメソッドが定義されています。

interpolateメソッドを使用して、上のコードを書き直してみましょう。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(StringTemplate::interpolate);

かなりスッキリしました。

これだとSTRとまったく同じなので、たとえばテンプレート引数の文字列の部分がアルファベットであれば、それをすべて大文字にしてみましょう。

これにはfragmentsを大文字に変換してから、interpolateメソッドをコールするようにします。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> StringTemplate.interpolate(
                st.fragments().stream().map(String::toUpperCase).toList(),
                st.values()));

簡単にできますね。


processメソッドのオーバーライドによるカスタム化

processメソッドをオーバーライドしたProcessorインタフェースの実装クラスを作るというと大げさな感じですが、実際にはofメソッドの引数で指定したラムダ式とほぼ同じです。

STRと同じ動作をさせるのであれば、interpolateメソッドを使用して次のように記述できます。

    StringTemplate.Processor<String, RuntimeException> proc
        = st -> st.interpolate();

さらにメソッド参照を使用すれば、さらに簡潔に記述できます。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate::interpolate;

これだけだとofメソッドの場合と同じなので、例外を扱えるようにしてみましょう。

STRは値がnullだった場合、nullと表記されます。

jshell> String name = null
name ==> null

jshell> STR."Hello, \{name}!"
$2 ==> "Hello, null!"

もちろん、これはこれでいいのですが、たとえば値がnullの場合、NullPointerException例外をスローするようにしてみましょう。

    StringTemplate.Processor<String, NullPointerException> proc
        = st -> {
            if (st.values().contains(null)) {
                throw new NullPointerException("Value is NULL");
            }
            return st.interpolate();
        };

NullPointerException例外をスローするので、Processorインタフェースの型パラメーターをRuntimeException例外からNullPointerException例外に変更してあります。

そして、ラムダ式の中でvaluesにnullが含まれるかどうかをチェックし、含まれていればNullPointerException例外をスロー、含まれていなければinterpolateメソッドをコールします。


また、カスタムテンプレートプロセッサーは文字列以外のオブジェクトを生成することも可能です。

たとえば、JSONを直接扱うこともできます。

JSON in Javaを使って、JSONObjectオブジェクトを生成するテンプレートプロセッサーは次のようになります。

    StringTemplate.Processor<JSONObject, JSONException> JSON
        = st -> new JSONObject(st.interpolate());

Processorインタフェースの第1型パラメータがJSONObject、第2型パラメータがJSONExceptionになっていることに注意してください。

後は、JSONObjectクラスのコンストラクタ引数に文字列補間の結果を渡すだけです。

使い方は今までのテンプレートプロセッサーと同じです。

    var name = "Sakuraba";
    var city = "Tokyo";
    
    JSONObject jsonObj = JSON."""
        {
            "name": "\{name}",
            "city": "\{city}"
        }
        """;

これまでJSONObjectオブジェクトを生成するために文字列を作るのがめんどうだったのが、簡単にできるようになりました。

カスタムテンプレートプロセッサーはいろいろと使い道がありそうですね。たとえば、以下のような使い方がありそうです。

  • SQL
  • JSON
  • HTML/CSS/XML
  • ログメッセージ
  • エラーメッセージ

定型的な文字列処理になるような部分は、テンプレートプロセッサーで担えそうです。


文字列補間の危険性

文字列補間は便利なのですが、その反面、危険性を伴うこともあります。

安易に文字列補間を行った結果、SQLインジェクションやクロスサイトスクリプティングなどの原因になってしまうことがあります。

たとえば、SQLインジェクションについて見てみましょう。

    String query = STR."SELECT * FROM Person p WHERE p.name = '\{name}'";

このテンプレートに対し、変数nameが次の値だったらどうでしょう。

    name = "Sakuraba' OR p.name <> 'Sakuraba'";

名前がSakurabaでもSakuraba以外でもOKになってしまい、結果登録されている全件がクエリー結果になってしまいます。

このような問題に対し、カスタムテンプレートプロセッサーで対応することもできます。

たとえば、次のような手法が考えられます。

  • 使用できない文字を置き換える
  • 使用できない文字が含まれていれば例外をスロー
  • 型で制限する

まず、使用できない文字を置き換えるテンプレートプロセッサーを作ってみましょう。

valuesの値に使用できない文字があれば、置き換える処理を加えればできそうです。

    StringTemplate.Processor<String, RuntimeException> PROC = st -> {
        List<String> replacedValues
            = st.values().stream()
                         .map(v -> v.toString().replace("'", "\\'"))
                         .toList();
        return StringTemplate.interpolate(st.fragments(), replacedValues);
    };

これで、先ほどのクエリーは次のように変換されました。

jshell> StringTemplate.Processor<String, RuntimeException> PROC = st -> {
   ...>             List<String> replacedValues
   ...>                 = st.values().stream()
   ...>                              .map(v -> v.toString().replace("'", "\\'"))
   ...>                              .toList();
   ...>             return StringTemplate.interpolate(st.fragments(), replacedValues);
   ...>     };
PROC ==> $Lambda/0x000002380c00a000@60c6f5b

jshell> var name = "Sakuraba' OR p.name <> 'Sakuraba'"
name ==> "Sakuraba' OR p.name <> 'Sakuraba'"

jshell> var query = PROC."SELECT * FROM Person p WHERE p.name = '\{name}'"
query ==> "SELECT * FROM Person p WHERE p.name = 'Sakuraba\ ... p.name <> \\'Sakuraba\\''"

jshell> System.out.println(query)
SELECT * FROM Person p WHERE p.name = 'Sakuraba\' OR p.name <> \'Sakuraba\''

ここではクオーテーション'が\'に置き換えられていることが分かります。

2番目の方法はそもそも置き換えられるような文字が入っていることがまちがっているということです。この場合は、例外をスローするようにします。

    StringTemplate.Processor<String, IllegalArgumentException> PROC = st -> {
        st.values().stream()
                   .map(v -> v.toString())
                   .filter(v -> v.contains("'"))
                   .findFirst()
                   .ifPresent(s -> {
                       throw new IllegalArgumentException(STR. "Illegal Text: \{s}" );
                   });

        return st.interpolate();
    };

ストリームでvaluesの中に使用できない文字が含まれていないか調べ、含まれていたらIllegalArgumentException例外をスローするようにしてあります。

使用できない文字が含まれていなければ、interpolateメソッドで文字列補間を行います。

ここまでの2種類の方法は、文字列補間の時にバリデーションをして対応する手法です。しかし、そもそもバリデーションはもっと早い段階、たとえばWebからの入力を受け取った時に行うこともできるはずです。

バリデーションを行って、適切なデータ型に変換し、カスタムテンプレートプロセッサーではそのデータ型に制限して処理を行うのが最後の方法です。

たとえば、name属性を持つPersonレコード( record Person(String name){} )があったとしましょう。カスタムテンプレートプロセッサーは、このPersonレコードだけを使用できるようにします。

    StringTemplate.Processor<String, IllegalArgumentException> PROC = st -> {
        var values = st.values().stream()
           .map(v -> {
               if (v instanceof Person(var name)) {
                   return name;
               } else {
                   throw new IllegalArgumentException(STR."Not Person class \{v.getClass()}: \{v}");
               }
           })
           .toList();

           return StringTemplate.interpolate(st.fragments(), values);
    };

型の比較には、Java 21で導入されたパターンマッチングを使用しています。


システムによってどのようにバリデーションを行うかの方針は異なると思いますが、その方針に応じてカスタムテンプレートプロセッサーを作成して、文字列補間の危険性を下げていくことができるはずです。


このように、いろいろと便利なString Templateなのですが、使いにくい点が1つだけあります。それはテンプレート引数が文字列リテラルもしくはテキストブロックしか使えないところです。

通常のテンプレートエンジンであれば、テンプレートをファイルから読み込むなど、任意の文字列をテンプレートにすることができます。

しかし、String Templateではそれができません。でも、できないと言われると、やりたくなりますよね。

そこで、次回は動的にテンプレート引数を作成する方法を紹介します。


2023/12/11

String Templateによる文字列補間 その1 - 基本的な使い方

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

本エントリーはJava Advent Calendarの11日目です。

 

本エントリーで紹介しているJEP 459は差し戻しになってしまったので、本エントリーの内容は使えなくなりました。また、新しい仕様が策定したら改めて紹介する予定です。

 

qiita.com


11月11日にJJUG CCC 2023 Fallが開催されました。

ccc2023fall.java-users.jp


今回、取り上げたのはString Templateです。Java 22では、Second PreviewのJEP 459が入る予定です。

openjdk.org

String Templateは文字列中に式を埋め込んで、文字列の補間を行う機能です。発表資料はこちら。


導入の背景

文字列と変数を組み合わせて、新たに文字列を作ることは、Javaを書いたことがある開発者であれば多々あると思います。

たとえば、Hello, World!のWorldの部分を変数nameで変更できるようにするには

  1. "Hello, " + name + "!"
  2. new StringBuilder("Hello ").append(name).append("!").toString()
  3. String.format("Hello, %s!");
  4. new Formatter().format("Hello, %s!", name).toString();
  5. MessageFormat.format("Hello, {0}!", name);

などの書き方があります。

たぶん、変数が少なければ1や2の方法が使われて、フォーマットをちゃんとしたい場合は3以降の方法になるのではないでしょうか。

ちなみに、1の方法はコンパイルするとかつては2に置き換えられましたが、現在のJavaではInvokeDynamicによって実行時に動的に処理が行われるようになっています。

さて、このようにいろいろと方法はあるのですが、どれも一長一短。

1や2の方法は変数が少なければいいのですが、多くなってくると書くのがめんどうくさい。

3はformatメソッドの内部で4と同じことをしているので、3と4は実質的には同じ手法です。

3からの5の手法は文字列に埋め込む場所(フォーマット記述子)と変数を別々に書かなくてはなりません。フォーマットを指定できるので、表現力は1や2に比べると高くなります。

しかし、変数が多くなると、変数とフォーマット記述子の組み合わせが分かりにくくなる欠点があります。

特に5のMessageFormatは変数の並び順とは異なる順番でも埋め込むことができるので、なおさら組み合わせを間違いやすくなります。ちなみに、3と4でも変数の並び順と埋め込む場所を別々に指定できますが、普通はやらないですね。

もっと簡単に、しかも間違いも起こしにくい方法があればいいですね。


こういう時に、今まで使われてきたのがテンプレートエンジンです。

古くはJSPやJSFなどがありますし、Spring FWではThymeleafがよく使われているようです。HTMLのレンダリングなどではテンプレートエンジンが使えるとは思いますが、テンプレートエンジンを使うほどではない文字列補間も多くあります。

そこで、簡単にしかも言語仕様として新たに導入されるのがSpring Templateです。


String Template

String Templateは文字列中に式を埋め込み、文字列補間を行う言語仕様とAPIです。

たとえば、先ほどのHelloの例だと

"Hello, " + name + "!";

を、String Templateを使うと次のように記述することができます。

STR."Hello \{name}!";

ここで、STRはテンプレートプロセッサーと呼ばれます。具体的には、java.lang.StringTemplate.Processorインタフェースです。このStringTemplate.Processorインタフェースが文字列補間の処理を行います。

標準で提供されているStringTemplate.ProcessorはSTRのほかにFMTとRAWがあります。これらについては後で説明します。

ピリオドの後の文字列リテラルがテンプレート引数と呼ばれます。ここでは文字列リテラルを使用しましたが、複数行の文字列を記述できるテキストブロックも使用することができます。

また、\{ }に式を埋め込むことができます。

定数の後にカッコもなく、いきなり文字列リテラルを書くなんて、変な感じがしますが、これが新しいString Templateの書き方になります。

String TemplateはまだPreviewなので、Java Language Specification (JLS)に正式に記述が追加されたわけではありませんが、JSLの変更分が提案されています。

docs.oracle.com

このリンクはJava 21のものですが、Java 22でもたぶん同じようになるはずです。

変更点はいろいろありますが、String TemplateのメインとなるのはJLS 15.8.6 Template Expressionsです。

JLS 15.8.6はJEP 459のSyntax and semanticsに書いてあることとほぼ同等なので、こちらを読めば大丈夫です。


StringTemplate.Processor

前述したようにテンプレートプロセッサーが文字列補間を行う主体です。インタフェースとしてはjava.lang.StringTemplate.Processorインタフェースです。

標準で提供されているテンプレートプロセッサーは次の3種類です。

  • java.lang.StringTemplate.STR
  • java.util.FormatProcessor.FMT
  • java.lang.StringTemplate.RAW

それぞれを簡単に説明していきます。


StringTemplate.STR

STRは最も基本的な文字列補間を行うテンプレートプロセッサーです。

STRはStringTemplateインタフェースの定数ですが、static importをしなくても使用することができます。

\{}に埋め込んだ式はプリミティブの場合はその値がそのまま使われ、参照型(オブジェクト)の場合toStringメソッドの結果が使われます。

文字列補間した結果は文字列になります。

jshell> var x = 10
x ==> 10

jshell> var y = 20
y ==> 20

jshell> STR."\{x} + \{y} = \{x+y}"
$3 ==> "10 + 20 = 30"

jshell> record Name(String first, String last) {}
|  次を作成しました: レコード Name

jshell> var name = new Name("Yuichi", "Sakuraba")
name ==> Name[first=Yuichi, last=Sakuraba]

jshell> STR."My name is \{name.first()} \{name.last()}."
$6 ==> "My name is Yuichi Sakuraba."

jshell> STR."""
   ...> {
   ...>   "firstName": "\{name.first()}",
   ...>   "lastName":  "\{name.last()}"
   ...> }
   ...> """
$7 ==> "{\n  \"firstName\": \"Yuichi\",\n  \"lastName\":  \"Sakuraba\"\n}\n"

jshell> System.out.println($7)
{
  "firstName": "Yuichi",
  "lastName":  "Sakuraba"
}
 

式であればよいので、\{}の中にswitch式なども記述できます。また、\{}は複数行になってもかまいません。

たとえば、下のようにも書くことができます。

jshell> import java.time.*

jshell> var today = LocalDate.now()
today ==> 2023-12-11

jshell> STR."""
   ...>    Today is \{today}, \{switch (today.getDayOfWeek()) {
   ...>       case SATURDAY, SUNDAY -> "Weekend";
   ...>       default -> "Weekday";
   ...> }}."""
$10 ==> "Today is 2023-12-11, Weekday."
 

書けることは書けますが、可読性がとても低下するので、こういう書き方はしない方がいいと思います。


FormatProcessor.FMT

FMTは文字列補間の結果が文字列になる点ではSTRと同じです。しかし、値の表示にjava.util.Formatterクラスと同じフォーマット記述子を使用することができます(厳密には完全に同じではないですが...)。

フォーマット記述子は\{}の直前に記述します。

なお、FMTはSTRは異なり、使用するにはstatic importが必要です。

jshell> import static java.util.FormatProcessor.FMT

jshell> var x = 10
x ==> 10

jshell> var y = 20.0
y ==> 20.0

jshell> FMT."%d\{x} / %.2f\{y} = %08.3f\{x/y}"
$5 ==> "10 / 20.00 = 0000.500"

jshell> FMT."Today is %tA\{LocalDate.now()}."
$6 ==> "Today is Mon."

最後の曜日のフォーマットで分かると思いますが、ロケールにja_JPが使用されていません。

FMTのロケールはLocal.ROOTになるので注意が必要です。

任意のロケールで使用する場合はFormatProcessorクラスのcreateメソッドでテンプレートプロセッサーを生成する必要があります。

jshell> var PROC = FormatProcessor.create(Locale.of("ja", "JP"))
PROC ==> java.util.FormatProcessor@2038ae61

jshell> PROC."Today is %tA\{LocalDate.now()}."
$10 ==> "Today is 月曜日."


StringTemplate.RAW

STRとFMTは処理の結果が文字列になりましたが、RAWだけは異なります。

RAWが返すのは、StringTemplateオブジェクトです。

ここまでテンプレートプロセッサーが何かということを触れずに来ましたが、テンプレートプロセッサーであるStringTemplate.Processorインタフェースはprocessメソッドだけ定義するインタフェースです(staticメソッドのofメソッドも定義していますが...)。

public interface StringTemplate {
        ...

    @FunctionalInterface
    public interface Processor<R, E extends Throwable> {
        R process(StringTemplate stringTemplate) throws E;

        static <T> Processor<T, RuntimeException> 
            of(Function<? super StringTemplate, ? extends T> process) { ... }

        ...
    }
}

このprocessメソッドが文字列補間を行うメソッドになり、その引数の型がStringTemplateインタフェースとなるわけです。

RAWで処理した結果はStringTemplateオブジェクトなので、他のテンプレートプロセッサーのprocessメソッドの引数にすることができます。

つまり、下に示すようなことができます。

jshell> var name = "Java"
name ==> "Java"

jshell> var st = RAW."Hello, \{name}!"
st ==> StringTemplate{ fragments = [ "Hello, ", "!" ], values = [Java] }

jshell> st instanceof StringTemplate
$15 ==> true

jshell> STR.process(st)
$16 ==> "Hello, Java!"

RAWで処理した結果をSTRの引数にしています。

このままだとあまり役に立つようには思えないかもしれませんが、テンプレートプロセッサーをカスタム化する場合に使えます。


長くなってしまったので、テンプレートプロセッサーのカスタム化は次のエントリーで説明することにします。

2023/12/01

Devoxx Belgium 2023 その2 セッション

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

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

なお、本エントリーはJava Advent Calendarの1日目のエントリーになります。

qiita.com


さて、後編ではDevoxxで、さくらばが聴講したセッションの中で面白かったセッションを紹介します。私が聴講するのはJava言語に関するセッションか、JVMに関するセッションばかりで、かなり偏っています。


なお、DevoxxのセッションはすべてYoutubeで公開されいます。

www.youtube.com

本エントリーでもセッション動画のリンクを貼っておきます。


Deep Dive

まずは、1日目と2日目に行われたDeep Diveのセッションです。


You. Me. Java 21

OracleのアドボケイターのNicolai ParlogによるJava 21を俯瞰的に説明するセッションです。

NicolaiはOracleのJava公式チャネルでいろいろと動画を公開していますけど、ちょっと釣り気味なところが鼻につくのですが、聞き取りやすい英語ですし、Java 21の概要を知るにはちょうどいいと思います。

ただ、途中で休憩があるものの3時間近いので、手っ取り早くJava 21について知りたいのであれば後述するBrian Goetzのキーノートセッションの方がいいと思います。


Value types and Pattern matching: bringing your data back on stage

OracleのアドボケイターのJosé Paumardと、Universite Gustave EiffelのRemi ForaxによるProject Valhallaの紹介セッション。

JoséはJava公式チャネルでJEP Cafeの動画に出ている人ですが、もともと大学の先生だったので、2人ともアカデミック系な人です。

ValhllaではValue Typeという新しい型を導入しようとしていますが、それについてのセッションです。ただし、Value Typeは正式に導入されるまで、まだまだ紆余曲折ありそうなので、今のところはイメージでとらえるのがいいと思います。


With Java 21, Your Code Runs Even Faster. But How is that Possible?

OracleでHotSpotグループに属しているPer Minborgのセッション。

Perさん、Oracleの開発者では数少ないブログを続けてくれている方です。ブログはよく読んでいたのですが、ご本人を見たのははじめて。

minborgsjavapot.blogspot.com


JVM的にパフォーマンスのいいコードはどのように書けばいいかという話。

なお、このセッションはDeep Diveの日でしたが、Tools-in-Actionというタイプの30分のセッションです。


Keynote

Keynoteは3日目の水曜日の午前中に行われました。

やっぱり注目はBrian Goetzですね。


Java 21

JavaのチーフアーキテクトであるBrian Goetzのキーノートセッション。

Brianが今のJavaの方向性を決めているといっても過言ではないです。

Java 21を俯瞰する内容なので、キャッチアップするにはちょうどいいかもしれません。とはいうものの、Brianはものすごく早口なので、追いつくだけでもたいへんですが。


Conference Session

3日目の午後からの通常のセッションがはじまります。

Deep Diveと異なり、セッションの時間は50分です。


Game of Loom 2: life and dead(lock) if a virtual thread

Red HatでQuarkusなどを担当されているMario FuscoのVirtual Threadのセッション。

QuarkusはすでにVirtual Threadに対応していますが、実際に担当されていた方の話はなかなか説得力があります。

1つのスレッドでデッドロックになってしまうなんて事象は、はじめてききましたw


Teaching old Streams new tricks

OracleのHot Spotグループに属しているViktor KlangのStreamのセッション。

JVMではなく、ライブラリに関するセッションで、現在提案されているStreamのGathere (JEP 461)などを紹介していました。


The Panama Dojo: Black-Belt Programming with Java 21 and the FFM API

再び、Per Minborg。

FFMはForeign Function & Memory APIのことです。Java 22でやーーーーーっと正式な機能として取り込まれる予定のFFMの紹介。

後半のサンプルは、イマイチついていけなかったのですが、FFMに関する情報がまだ少ないので、貴重です。


Java Language update

Brian Goetzはキーノート以外にも複数のセッションを担当されていたのですが、最後の最後のセッションがこちら。

まだJEPとして提案していないことなどを含めて、今検討中の機能について紹介してくれました。ここまで話してくれるのはなかなかないことかもしれません。

なお、この内容については11月22日に行われたJJUGナイトセミナーで伊藤さんが紹介してくれています。

資料はこちら。


動画がすべて公開されているので、わざわざ高いお金を払って海外までカンファレンスに参加するのはなんでなんででしょうね。

生の声を集中して聞けるのは、実際にカンファレンスに参加しているからだと思うのです。動画を見るのは受動的で集中力も落ちがちじゃないですか。少なくとも私はそうです。

どこに力を置いて話しているのかとか、何を強調したいのかなど、動画だとなかなか分からなくて...

しかもセッションがこれだけ多いと、動画を探すのも一苦労です。

時間があいたら普段は聞かないようなセッションを聴講できるのも、実際にカンファレンスに参加しているからですね。ここでは紹介しませんでしたけど、Nettyのセッションとか普段は絶対に聞かないと思うのですが、なかなかおもしろかったです。

といろいろ理由書きましたけど、やっぱりカンファレンスに参加するの楽しいんですよ。

日常とは異なる環境で技術にどっぷりつかった1週間なんて、なかなかないですから。


例年、JavaOneとDevoxxが近い日程で開催されるので、来年JavaOneが復活すればJavaOneに行ってしまうかもしれません。もし、行ってみたいという方がいらっしゃったら、ぜひお声がけください。

また、海外でなくても、日本国内、たとえばJJUGのカンファレンスに参加するのでも全然かまわないので、ぜひ実際にカンファレンスに足を運んでいただけれと思っています。