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のカンファレンスに参加するのでも全然かまわないので、ぜひ実際にカンファレンスに足を運んでいただけれと思っています。


2023/11/30

Devoxx Belgium 2023 その1 参加方法、行き方など

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

10月2日から6日にかけて開催されたJavaのカンファレンスであるDevoxx Belgiumに参加してきました。

今後 Devoxxに参加してみたいという方に向けて、Devoxxへの参加方法などをまとめておきます。


Devoxxとは

Devoxxはヨーロッパを中心に開催されているJavaのカンファレンスです。

devoxx.be

もともとはベルギーで始まり、そこからフランスやイギリスなどでも開催されるようになっています。

その中でも、ベルギーのDevoxxは規模が一番大きく、約3,000人の参加者がいます。この3,000人は会場の広さによるものですが、会場を変えるつもりはないらしいので、ずっと3,000人のままです。

JavaOneがシュリンク状態なので、現状では世界最大のJavaカンファレンスです。

スピーカーだけでも200人以上、初心者向けから上級者向け、またJavaだけでなくチームビルディングなどのセッションもあります。なお、セッションはすべて英語です。


今回のDevoxxベルギーは10月2日から6日まで開催されました。

ベルギーといっても会場は首都のブリュッセルではなく、アントワープになります。

アントワープというのは英語読みで、オランダ語だとアントウェルペンです。

ベルギーは南側がフランス語圏、北側がオランダ語圏、西側にちょっとだけドイツ語圏があります。アントワープはベルギー北部で、オランダ語圏になります。

今年は、Devoxxベルギーがちょうど20回目の開催です。コロナの時に開催しなかった期間がありますが、長く続いているカンファレンスです。


Devoxxベルギーは会期が5日間で、月曜から金曜まで。

1, 2日目はDeep Diveという3時間のチュートリアル的なセッションです。

3日目から5日目までが通常のセッションです。こういうカンファレンスはだいたい最終日は早く終わるのですが、Devoxxも最終日はほぼ半日です。


チケット

Devoxxは人気のあるカンファレンスですが、参加者数が3,000人と限られているため、チケットは争奪戦になります。

例年、2回に分けてチケットが販売されますが、今年はちょっと違っていました。

チケットの種類と価格

チケットは3種類あります。価格は今年の価格で、もちろんユーロです。

  • Deep Dive 400
  • Conference €650
  • Combi €975

Deep Diveは前半2日間のDeep Diveだけに使えるチケットです。

次のConferenceが3日目からの一般のセッション、CombiがDeep DiveとConferenceの両方を聴講できるチケットです。

日本から行くのであれば、Deep Diveだけ聞くというのはちょっと考えられないので、Combiがほとんどだと思います。実際、今回日本から参加した4人ともCombiでした。

チケット販売

前述したようにチケットは争奪戦になります。

チケットの販売開始日時はDevoxxのWebサイトに発表されますが、今年は1回目が8月16日、2回目が9月2日となっていました。

ところが、8月16日のチケットがものの数分で売り切れ。

あまりにも早く売り切れてしまったということで、2回目に販売する予定だったチケットもあらかた放出してしまいました。

そして、残ったチケットはなんと抽選制ということに。

来年がどうなるかは分かりませんが、2回あるからといっても、1回目で購入すべきです。しかも、販売開始時間と同時に購入しないと、なかなか厳しいです。


さて、チケットを購入できた場合、料金を払わなければなりません。

なんとDevoxxはクレジットカードに対応していません。使えるのは銀行振り込みのみ。

海外送金になるので、なかなか大変です。

銀行から振り込む場合、事前に登録していないと送金できないところが多いです。

そこで、さくらばは海外送金サービスのWiseを使用しました。

wise.com


と書きましたが、実をいうとチケットを購入した時はそんなことを全然知らず、あたふたしてしまいましたww

Wiseも西川さんに教えてもらって、やっと送金できたわけです。

来年参加する人は、ぜひ事前にこれらを用意してからチケット争奪戦に参加してください。


というか、Devoxxがクレジットカードに対応してくれればいいんですけどね。


アントウェルペンへ

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

交通手段として考えられるのは次の2手段です。

  • 空路
  • 空路 + 鉄道


空路

アントウェルペンにも空港がありますが、ほとんどの便がベルギーの首都であるブリュッセル国際空港行きです。ブリュッセル空港からアントワープは鉄道で40分ほど。

現在、日本からブリュッセルへの直行便を飛ばしているのはANAの成田 - ブリュッセル便だけです。

しかし、毎日ではなく、水曜と土曜だけです。

Devoxxに行く場合は、土曜日に日本を出てブリュッセル着、次の土曜日にブリュッセルを出て、日曜に帰国になると思います。


これ以外は、すべて乗り継ぎ。

ヨーロッパだとフランクフルトやパリ (CDG)、ロンドン (ヒースロー) などで乗り継ぐことが多いと思います。他にも、ドバイやアブダビなんて選択肢もあります。

ご自分の使う飛行機会社で決めるか、料金の安さで決めるかはお好みで。


空路 + 鉄道

ヨーロッパは鉄道が発達しているので、陸路でアントウェルペンに行く手段もあります。

フランクフルトやパリで乗り継ぐ場合、乗り継ぎの待ち時間などを考えると鉄道の方が早かったりしますね。

今回、さくらばは飛行機はパリまで、そしてパリからアントワープまで鉄道を使いました。

実際には、パリに2泊してからアントウェルペン入りしました。Devoxxの日程よりかなり早くついたのは、Devoxxに時差ボケで挑みたくないからです。決して、パリでスイーツを食べまくるためではありませんww


さて、鉄道の主な経路としては、以下のような路線があります。

  • アムステルダム - アントウェルペン (ユーロスター) 1時間45分
  • パリ - アントウェルペン (ユーロスター) 2時間30分
  • ロンドン - ブリュッセル南 - アントウェルペン (ユーロスター) 3時間20分
  • フランクフルト - ブリュッセル北 - アントウェルペン (ドイツ鉄道) 4時間

パリからだと、ちょうど東京と大阪ぐらいの感覚ですね。

とはいうものの、新幹線ほどの運行本数があるわけではないのは注意が必要です。

いずれもネットからチケットを購入できます。本数が少ないので、チケットはなるべく早く買っておいた方がよいですね。


ちなみに、パリやアムステルダムからアントウェルペンに行く列車はThalys (タリス)の列車でした。

ところが、今年の10月1日からThalyisはEurostarに統合されています。

さくらばはThalysで予約していたのですが、10月1日に移動のためパリ北駅についたらThalysがない!!!

まさか、この日からEurostarになっていたとはまったく知らず、どうしようかと思いましたよ。

_DSC9010

ちなみに、フランスの駅は撮影禁止です。この後、駅員さんに撮らないでと注意されました😰


宿泊

空港や他の都市からの鉄道はアントウェルペン中央駅 (Antwerpen Centraal) に止まります。

アントウェルペンは中央駅のある区域と、そこから西へ歩いて30分ぐらいのところに大聖堂のある旧市街があります。そして、Devoxxの会場であるKinepolisは中央駅から北に歩いて1時間ぐらいのところ。

ところが、会場のKinepolisのそばにはホテルがほとんどありません。

ホテルが一番多いのは中央駅のそば。

旧市街にもホテルはありますが、Kinepolisに行くにはちょっと遠いです。ただ、後述しますが、レストランなどが多いのが旧市街。食べるところに近いところで選ぶというのであれば、旧市街もあるかもしれません。


とはいうものの、やはり選びやすいのは中央駅の近くですね。

今回、さくらばはCitybox Antwerpというホテルに宿泊しました。

かなり省人化されており、チェックインもチェックアウトも自動チェックイン機で行い、部屋の清掃もお願いしないとしてくれません。

また、レストランはなく、朝ごはんからランチまで空いているカフェのみ。

こういうこともあって、宿泊費は平日は1泊1万強。ただし、金曜、土曜は値段がかなり上がっているので、税金と合わせると平均で1泊1.5万ぐらいでした。

一緒に行った杉山さん、伊藤さんはB&B Hotel Antwerp Centrumというホテルだったそうです。ここもだいたい1泊1万円ぐらいのようです。

他にも、このレンジのホテルが結構あるようです。


Devoxx会場までの交通手段

前述したように中央駅とDevoxx会場であるKinepolisは歩くと1時間近くかかります。

普通の人はとてもじゃないですけど、歩く距離ではないですね。

会場までの交通手段として考えられるのは以下の3種類です。

  • トラム (or バス)
  • シェアサイクル
  • 電動キックボード

ほとんどの参加者はトラムを使っているようです。

トラムでだいたい20分ほどです。

下の写真がトラムです。

_DSC9512


トラムやバスはすべてDe Lijn (フランデレン交通公社)が運航しています。

チケットはDe Lijnのアプリで購入可能。

ちょうど10枚で€17.00の回数券があるので、Devoxxにはこれがちょうどいいと思います。

チケットは1枚ずつアクティベイトして、アクティベイトのあと1時間有効です。さくらばはこれを知らずに1枚無駄にアクティベイトしてしまいました😢

チケットをアクティベイトするとQRコードが表示されます。しかし、これをスキャンする装置はトラム内にはありません。

たまに車掌が検札に来るらしいので、その時に見せるようです。

とはいうものの、私は1度も検札に遭遇したことないので、タダで乗っている人も多いんじゃないかなぁ。


次の選択肢がシェアサイクルです。

ベルギーやオランダは自転車大国。歩道よりも自転車専用路の方が幅が広く立派ですw

アントウェルペンで使い勝手がいいのがVelo Antwerpenというシェアサイクル。街中のいたる所にあり、すぐに使用できるようです。

料金は1日で€5、1週間乗り放題で€12です。

_DSC1832


また、電動キックボードはLimeのものが主流のようです。Limeは乗り捨てできるので、便利ですね。

下の写真はKinepolisの近くに停められたキックボード。緑と白のキックボードがLimeです。こんな感じで、いっぱい使われています。

_DSC1865


ちなみに、さくらばは行きは歩き、帰りはみなと食事に行くためトラムという感じでした。


Devoxx会場

Devoxxの会場のKinepolisは映画館です。いわゆるシネマコンプレックスです。

そこを貸し切って、イベントの会場にしています。

こちらがシネコンとしての正面です。

_DSC0351

街の中心部から離れているので、車で来るようなところ。そのため、シネコンの正面は車寄せになっています。

しかし、こちらからは入りません。

裏側に回ると、下の写真のようにKinepolis Event Centerという入り口があります。そちらが、Devoxxの入り口です。

_DSC9527

とはいっても、中に入れば映画館そのままです。

_DSC9665

セッションの資料は映画のスクリーンに投影されるので、広く、コントラストも高く、とても見やすいです。

_DSC9532

また、映画館なので席は傾斜がつけられており、後ろの方でも前の人が邪魔で見えないなんてことはありません。

シートも立派なので、とても楽。ただ、このシートがよすぎて、時差ボケの状態だと眠くなってしまうのが難点ですw

_DSC9595


食事

Devoxxは、朝食とランチが提供されるので、会期中は夕食だけどこかで食べることを考えないといけません。

朝食はクロワッサンや甘いペストリーなど。

_DSC0713 _DSC0699


ランチはランチボックスとサンドウィッチが交互に出ていました。

_DSC1151


また、飲み物は飲み放題。コーラやLiptonの甘い紅茶などが常においてありました。

_DSC9672


問題は夕食です。

ところが、前述したように会場のKinepolis近辺には食べるところはほとんどありません。

まぁ、Kinepolis内にBurger Kingとかありましたけど...

レストランが多いのは旧市街。観光客が多いところなので、レストランも多いです。

とはいえ、人も多いので、満席で入れないところも多いです。また、コロナのせいもあってスタッフが足りてなく、席が空いていても入れないこともあります。

ほとんどの店はネットで当日でも予約できるので、予約していくのがお勧めです。

また、ほとんどの店でクレジットカードを使えますが、たまに現金しか使えない店があるので現金は持っておいた方がよいです。


さて、せっかくベルギーに行くのであれば、ベルギー料理を楽しみたいですね。

ベルギーの名物といえば

  • ムール貝
  • フリッツ
  • クロケット
  • ワッフル

とかでしょうか。

ベルギーといえば、ムール貝のワイン蒸し。バケツのような容器で大量に出てくるので、少人数の場合は注意が必要です。

_DSC9955


フリッツはフライドポテトです。フレンチフライと呼ばれるぐらいなので、フランスと思われがちですが、実際はベルギー発祥。

街中にはフリッツ専門店もあります。

こちらもレストランで頼むと大量に出てきますw

_DSC0554


クロケットはコロッケのこと。ベルギーというよりオランダの名物かもしれませんが、アントウェルペンはオランダ語圏ということもあり、どこでもクロケットがおいてあります。

特にエビのクロケットが多いですね。

下の写真はエビのクロケットとチーズクロケットのミックスです。

_DSC0002


ワッフルはベルギー名物かもしれないですけど、食べるのであればブリュッセルの方がいいかも。

というより、日本のワッフルの方がおいしいですよ。


後はビールですかね。でも、私は下戸なので、よくわからないです。

ベルギーの人は、家で夕食を食べてからビールを飲むところに繰り出して、ビールだけ延々と飲むらしいです。

たしかに、テラス席でビール飲んでいる人はテーブルにビールしか置いてないんですよね。


さて、参考までに今回行ったところを載せておきます。セッションが終わった後に予約していて空いているところ優先なので、ここがお勧めというわけではないところにご注意ください。


De Sinjoor

旧市街の大聖堂の裏にあるベルギー料理の店。上にあるムール貝はここで食べました。

_DSC9937

De Zeven Schaken

ここも旧市街にあるベルギー料理店。フリッツはここで食べました。

アントウェルペンは日本よりも寒かったので、シチューです。確かビーフシチュー。

_DSC0582

Amadeus Antwerpen 2

スペアリブの食べ放題が売りの店。店内の雰囲気もいいです。

_DSC1551


Bife Churrasco

遅くまでやっているアルゼンチンスタイルのステーキの店。

_DSC2282


't Hofke

小路を入り込んだところにある隠れ家的なレストラン。有名なところらしいです。

_DSC2782


Birtrot L'ilot

ここは旧市街ではなく、ちょっと離れたところで、基本はフレンチです。

ここはおいしかったけど、それなりに高いです。

_DSC3059 _DSC3102


Bizie Lizie

ここも旧市街からはちょっと離れてますが、比較的洗練されたベルギー料理でした。

ウナギのグリーンソース。これもベルギー料理として有名なんだそうです。

_DSC4006 _DSC4029


カフェ

Devoxx会期中はいいのですが、それ以外の日に朝ごはんを食べようとするとカフェが便利ですね。


Le Pain Quotidien

日本にも進出しているベーカリーカフェのル・パン・コティディアンは、ベルギーが本拠です。

アントウェルペンにも何店舗かあり、朝からやってます。

_DSC1673 _DSC1727


Koffie Dealers

パンダのマークのこじんまりしたカフェです。ベルギーはエスプレッソがメインですが、ここはドリップコーヒーも出してます。

食べものはマフィンとかの軽いものだけでした。

_DSC2355 _DSC2312


Caffe Mundi

旧市街の中にある人気のあるカフェ。ロースタリーもあるくらい、コーヒーにこだわっているカフェでした。

_DSC3543 _DSC3587


Georges Espressobar

Kinepolisに行く途中にある、こじんまりしたカフェ。

クレジットカードが使えずに、現金かQRコード決済のみなのが、残念。

_DSC2392 _DSC2408


Bakker Aldo

ここはカフェではなくパン屋さん。ただし、テラス席か2階のワインバーで購入したパンを食べることができます。ワインバーで食べるときはワンドリンク制です。

_DSC3683


Lints

昔からあるベーカリーカフェらしいです。

年配の地元客が多かったですし、中心部からはちょっと離れているので、地元密着型の店なんだと思います。

_DSC0126 _DSC0075


Prandeo

街の中心部からDevoxx会場に行く途中にあるカフェ。

_DSC0250 _DSC0268


Le Royal Cafe

アントウェルペン中央駅にあるカフェ。

かつては王族の待合室に利用されていた場所らしく、内装は豪華。でも、シートはボロボロだし、メニューも古ぼけているし、イマイチ歴史や場所の優位性を活かせていない感じです。

まぁ、写真を撮るにはいいとは思いますよw

_DSC3253 _DSC3330


Popoff

旧市街にある、週末の夜だけやっているカフェレストラン。

レストランで食べた後に、ちょっとスイーツとかいう時にいいかもしれません。ここも現金のみ。

_DSC3174


さて、後半ではさくらばが聴講したセッションを紹介していきます。