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


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

2023/09/19

JEPでは語れないJava 21

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

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

Java 21はLTSのリリースが3年から2年に変わって、はじめてのLTSバージョンです。LTSかどうかはOpenJDK的には関係ないのですが、やっぱりこのバージョンを一区切りとする感じですね。

Java 21のJEPは以下の通り。JEPが15もあり、しかもPreviewもしくはIncubatorではないスタンダードJEPも多いのが、LTSという感じですね。。

  • 430: String Templates (Preview)
  • 431: Sequenced Collections
  • 439: Generational ZGC
  • 440: Record Patterns
  • 441: Pattern Matching for switch
  • 442: Foreign Function & Memory API (Third Preview)
  • 443: Unnamed Patterns and Variables (Preview)
  • 444: Virtual Threads
  • 445: Unnamed Classes and Instance Main Methods (Preview)
  • 446: Scoped Values (Preview)
  • 448: Vector API (Sixth Incubator)
  • 449: Deprecate the Windows 32-bit x86 Port for Removal
  • 451: Prepare to Disallow the Dynamic Loading of Agents
  • 452: Key Encapsulation Mechanism API
  • 453: Structured Concurrency (Preview)

やはり注目はJEP 444: Virtual Threadsでしょうね。すでに、Springなどのフレームワークが対応を発表するなど、Virtual Threadが普及するのも意外に早いかもしれません。とはいうものの、Springなどのフレームワークを使っている場合、ユーザーがVirtual Threadを意識することはないはずです。

Project Amber関連のJEPは5つ。スタンダードJEPはパターンマッチングのJEP 440とJEP 441です。JEP 443などが残っているので、パターンマッチングに関する機能はまだすべてではありません。しかし、switch式でパターンマッチングができるようになったのは大きいと思います。

JEP 430は汎用に使える小さなテンプレートエンジン、JEP 445はmainメソッドの簡略化です。

JEP 431はコレクションに関する機能です。これはAPIなので、java.utilパッケージで説明します。

JEP 439 Generational ZGCはZGCに世代別GCの機能を加えたものです。JEP 442 FFMはThird Previewなので、なかなかスタンダードにならないですが、どうやら次のJava 22でスタンダードになるようです(JEP 454)。FFMと同じProject PanamaのJEP 448 Vector APIもなかなかスタンダードにならないですね。

JEP 446 Scoped ValuesとJEP 453 Structured ConcurrencyはVirtual Threadと同じProject Loomで策定されているAPIです。できれば、これらがまとめてJava 21でスタンダードJEPになればよかったのですが、まぁしかたないです。

JEP 449とJEP 451は機能削減。というか、Windowsの32bit版なんてまだあったのかという感じですねw

JEP 452 Key Encapsulation Mechanism APIはセキュリティ関連のJEPです。

と、軽くJEPを説明したところで、APIの変更について紹介していきましょう。今回はAPIの変更がいっぱいあります。

例によってセキュリティ関連のAPIは省略します。また、java.compilerモジュール、java.desktopモジュールの変更は通常の開発では使用しない変更であるため省略します。

 

廃止になったAPI

Java 21では2クラス、1メソッドが廃止になりました。いずれも、頻繁に使われるAPIではないので、特に問題はないと思います。

クラス

  • java.lang.Compiler
  • javax.management.remote.rmi.RMIIIOPServerImpl

CompilerクラスもRMIIIOPServerImplクラスもJava 9からforRemoval=trueになっていたので、とうとう削除という感じです。

 

メソッド

  • java.lang.ThreadGroup.allowThreadSuspension(boolean)

「allowThreadSuspensionメソッドなんてあったの?」という感じですが、実際には何も処理を行わず、単にfalseを返すだけのメソッドになっています。このメソッドはJ2SE 1.2からDeprecatedになっており、逆になぜ今まで残していたのかが不思議なくらいですね。

 

廃止予定のAPI

Java 21で追加された廃止予定のAPIは、メソッドが2種類です。

 

メソッド

  • javax.management.remote.JMXConnector.getMBeanServerConnection(Subject delgationSubject)
  • javax.swing.plaf.synth.SynthLookAndFeel.load(URL url)

getMBeanServerConnectionメソッドはオーバーロードが2種類あり、引数のある方がforRemoval=trueになりました。Subjectを指定することはまずないと思うので、引数なしのgetMBeanServerConnectionメソッドを使用すれば大丈夫です。

SynthLookAndFeelクラスはSwingのLook&Feelの1つですが、Swingが使われることもないでしょうし、ましてやLook&FeelにSynth Look&Feelを使うことはまずないと思うので、問題ないと思います。

 

追加/変更されたAPI

Java 21で追加されたAPIの半分ぐらいはFFM APIですが、Preview JEPなので省略します。FFM API以外にもPreview APIが含まれていますが、それも省略します。

 

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

java.ioパッケージではAPIの追加はないのですが、変更がありました。

 

Consoleクラス

コンソールを扱うためのConsoleクラスですが、Java 21でsealedクラスになりました。

実をいうと、ConsoleクラスはJava 19まではfinalクラスでしたが、Java 20でfinalではなくなりました。これは、コンソールを扱うためにJLineライブラリを扱うためだったようです。JLineはJShellでも使われている、コンソールを扱うためのライブラリです。

しかし、他の用途でConsoleクラスの派生クラスを作成させるためではありません。そこで、sealedクラスにすることで、実質的にfinalクラスと同じになりました。

ちなみに、permitsされているクラスはProxyingConsoleです。

 

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

Virtual Threadの導入によりスレッド関連でAPIの追加が行われています。また、Java 21でサポートされるUnicodeのバージョンは15.0のままなのですが、CharacterクラスにもAPI追加が行われています。

 

Characterクラス

文字の判別メソッドが6種類追加されました。

  • static boolean isEmoji(int codePoint)
  • static boolean isEmojiPresentation(int codePoint)
  • static boolean isEmojiModifier(int codePoint)
  • static boolean isEmojiModifierBase(int codePoint)
  • static boolean isEmojiComponent(int codePoint)
  • static boolean isExtendedPictographic(int codePoint)

今までは絵文字かどうか調べるには、文字がUnicodeのどの面にあるかなど調べなくてはいけなかったのでめんどうくさかったのですが、これで簡単になりました。

なお、Java 21ではUnicodeのバージョンアップはなく、Unicode 15.0のままなので、UnicodeBlockクラスなどは変更ありません。

 

Mathクラス/StrictMathクラス

clampメソッドのオーバーロードが4種類追加されました。

  • static int clamp(long value, int min, int max)
  • static long clamp(long value, long min, long max)
  • static double clamp(double value, double min, double max)
  • static float clamp(float value, float min, float max)

clampメソッドは、valueをminとmaxの間に固定するためのメソッドです。valueがmaxより大きければmaxを返し、minより小さければminを返します。minとmaxの間であればvalueをそのまま返します。

jshell> Math.clamp(1000L, 100L, 200L)
$1 ==> 200

jshell> Math.clamp(1000L, 2000L, 3000L)
$2 ==> 2000

jshell> Math.clamp(1000L, 100L, 2000L)
$3 ==> 1000

jshell>

 

Stringクラス

indexOfメソッドのオーバーロードが4種と、スプリットに関するメソッドが追加されました。

  • int indexOf(int ch, int beginIndex, int endIndex)
  • int indexOf(String str, int beginIndex, int endIndex)
  • String[] splitWithDelimiters(String regex, int limit)

indexOfメソッドは引数が1つものと、beginIndexが指定できるものが提供されていましたが、それに加えてendIndexを指定できるようになりました。

splitWithDelimitersメソッドは、文字列を分割する時に、分割に使用した正規表現を含めて分割します。

jshell> var text = "ab:cd:e::fg"
text ==> "ab:cd:e::fg"

jshell> text.split(":", 100)
$2 ==> String[5] { "ab", "cd", "e", "", "fg" }

jshell> text.splitWithDelimiters(":", 100)
$3 ==> String[9] { "ab", ":", "cd", ":", "e", ":", "", ":", "fg" }

jshell>

これが不思議なのが、privateメソッドでsplit(String regex, int limit, boolean withDelimiters)があるということです。splitWithDelimitersメソッドも内部でこのsplitメソッドをコールしているだけです。

なので、このprivateメソッドをpublicにすれば、わざわざ長ったらしい名前のsplitWithDelimitersメソッドを追加する意味がないと思うんですよね。

 

StringBufferクラス/StringBuilderクラス

StringBufferクラスとStringBuilderクラスは、いずれもAbstractStringBuilderクラスの派生クラスです。AbstractStringBuilderクラスはAppendableインタフェースを実装しているので、StringBuffer/StringBuilderクラスもAppendableインタフェースを実装したクラスになります。

しかし、StringBuffer/StringBuilderクラスのimplements節には記述されていなかったので、Javadocの「すべての実装されたインタフェース」にはAppeendableインタフェースが記載されるものの、implements節の方には記載されていませんでした。そのためなのか、Java 21からimplements節にAppendableインタフェースが追記されました。なお、この変更による動作の変更はありません。

また、repeatメソッドのオーバーロードが2種類追加されました。

  • StringBuilder repeat(int codePoint, int count)
  • StringBuilder repeat(CharSequence cs, int count)

repeatメソッドは文字や文字列を指定した回数だけ繰りかえします。

jshell> var builder = new StringBuilder()
builder ==>

jshell> builder.repeat("a", 10)
$2 ==> aaaaaaaaaa

jshell> builder.toString()
$3 ==> "aaaaaaaaaa"
    
jshell>

とはいうものの、StringBuilderクラスを使う機会は減りましたね。今は、リテラル文字列の連結(たとえば、"a"+"b"のような文字列の連結)もStringBuilderクラスを使ってないですし。ましてや、StringBufferクラスはもうDeprecatedにしてもいいのではないかと思うぐらいですね。

 

Thread.Builderインタフェース

順番的にはThreadクラスの方が先ですが、その内部インタフェースとクラスを先に紹介します。

Thread.BuilderインタフェースはThreadFactoryインスタンスを作成したり、スレッドを作成するためのインタフェースです。もちろん、Virtual Threadの導入により追加されたインタフェースです。

Thread.Builderインタフェースはsealed interfaceで、Thread.Builder.ofPlatformクラス、Thread.Builder.ofVirtualクラスだけが派生クラスとして定義されています。これらの実装クラスは後述します。

Thread.Builderインタフェースで定義されているメソッドは以下のとおり。

  • Thread.Builder name(String name)
  • Thread.Builder name(String prefix, long start)
  • Thread.Builder inheritInheritableThreadLocals(boolean inherit)
  • Thread.Builder uncaughtExceptionHandler(Thread.UncaughtExceptionHandler ueh)
  • Thread unstarted(Runnable task)
  • Thread start(Runnable task)
  • ThreadFactory factory()

とはいうものの、このThread.Builderインタフェースを直接使うことは、普通の開発ではほとんどないはずです。通常はSpringなどのフレームワークが使うもので、そのユーザーはスレッドがどのように作成されるかは意識しないで大丈夫なはずです。

 

Thread.Builder.ofPlatformクラス

Thread.Builderインタフェースの実装クラスで、従来のOSのスレッドに対応したJavaのスレッドを生成します。Virtual Threadに対して、従来のスレッドPlatform Threadと呼ぶようになっています。

Thread.Builderインタフェースで定義された以外のメソッドとして、以下の5種類のメソッドが定義されています。

  • Thread.Builder.OfPlatform group(ThreadGroup group)
  • Thread.Builder.OfPlatform daemon()
  • Thread.Builder.OfPlatform daemon(boolean on)
  • Thread.Builder.OfPlatform priority(int priority)
  • Thread.Builder.OfPlatform stackSize(long stackSize)

いづれも、従来のThreadクラスで定義されていた同等のコンストラクタもしくはメソッドがあります。

 

Thread.Builder.ofVirtualクラス

Thread.Builderインタフェースの実装クラスで、Virtual Threadに対応しています。ofVirtualクラスはThread.Builderインタフェースで定義されたメソッド以外のメソッドはありません。

 

Threadクラス

Virtual Threadに対応したため、いろいろとAPIが追加されたように勘違いするかもしれませんが、実際にはほとんど変化がありません。

  • static Thread.Builder.OfPlatform ofPlatform()
  • static Thread.Builder.OfVirtual ofVirtual()
  • static Thread startVirtualThread(Runnable task)
  • boolean isVirtual()

ofPlatformメソッドは前述したThread.Builder.ofPlatformクラスのオブジェクトを生成するメソッドです。同様にofVirtualはThread.Builder.ofVirtualオブジェクトを生成します。

ただし、これらのメソッドを使うことはほとんどないはずです。パラレル処理を行うためのフレームワークやライブラリを作成するのであれば使いますが、それ以外の場合はフレームワークがやるはずです。

というか、今でもnew Thread(() -> {...});のような直接スレッドを生成するようなコードを書いていたら、かなりやばいです。後述するExecutorServiceインタフェースを使うようにしましょう。

isVirtualメソッドだけは使用するかもしれませんが、Virtual Threadと分かったからといって、処理を変える必要はないはずなので、通常は使わないと思います。

 

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

java.lang.constantパッケージはindyやcondyで使用するために必要なインタフェース/クラスを定義していますが、普通は使わないでしょうね。とりあえず、追加されたものだけ列挙しておきます。

 

ConstatntDescsクラス

定数が4種類追加されました。

  • DirectMethodHandleDesc BSM_CLASS_DATA
  • DirectMethodHandleDesc BSM_CLASS_DATA_AT
  • String CLASS_INIT_NAME
  • String INIT_NAME
  • MethodTypeDesc MTD_void

 

MethodTypeDescインタフェース

ファクトリメソッドが2種類追加されました。

  • MethodTypeDesc of(ClassDesc returnDesc)
  • MethodTypeDesc of(ClassDesc returnDesc, List<ClassDesc> paramDescs)

 

ModuleDescインタフェース

モジュール用のデスクプリターが追加されました。定義しているメソッドは以下の3種類です。

  • static ModuleDesc of(String name)
  • String name()
  • boolean equals(Object o)

 

PackageDescインタフェース

パッケージ用のデスクプリターが追加されました。定義しているメソッドは以下の5種類です。

  • static ModuleDesc of(String name)
  • static PackageDesc ofInternalName(String name)
  • String internalName()
  • String name()
  • boolean equals(Object o)

 

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

indy用のインタフェース/クラスなのでこのパッケージも使わないとは思いますが、メソッドのシグネチャが変更されているので、列挙だけしておきます。

MethodHandles.Lookupクラス

  • <T> Class<T> accessClass(Class<T> targetClass)
  • <T> Class<T> ensureInitialized(Class<T> targetClass)

いずれも<?>が<T>に変更になっています。

 

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

これもいつものバージョンアップに伴う定数の追加です。

ClassFileFOrmatVersion列挙型

Java 21用の定数が追加されました。それにしても、今まで定数の説明がThe version recognized by ... だったのが、すべてThe version introduced by ...に変更されたのは何か意味があるのでしょうか???

  • ClassFileFormatVersion RELEASE_21

 

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

java.utilパッケージの変更は多いのですが、JEP 431 Sequenced Collectionsによるものがほとんどです。そこで、JEP 431に関するインタフェースから説明を加えていきます。

JEP 431ではコレクションに順序を取り入れ、先頭と末尾へアクセスするメソッドを定義します。また、順序を逆順にするメソッドも定義しています。

これらのメソッドを定義しているインタフェースは3種類あります。

  • interface SequencedCollection<E> extends Collection<E>
  • interface SequencedSet<E> extends Set<E>, SequencedCollection<E>
  • interface SequencedMap<K,V> extends Map<K,V>

インタフェースの継承関係を表したのが、下図です。


リストにはSequencedCollectionインタフェース、セットにはSequencedCollectionの派生インタフェースであるSequencedSetインタフェースが使われます。マップにはSequencedMapインタフェースです。

 

SequencedCollectionインタフェース

SequencedCollectionインタフェースはCollectionインタフェースの派生インタフェースで、以下の7種類のメソッドを定義しています。

  • SequencedCollection<E> reversed()
  • void addFirst(E e)
  • void addLast(E e)
  • E getFirst()
  • E getLast()
  • E removeFirst()
  • E removeLast()

メソッドの動作はメソッド名から分かると思います。

リストはもともと順序があるので、特に違和感なく使えるはずです。

jshell> var list = List.of(0, 1, 2, 3)
list ==> [0, 1, 2, 3]

jshell> list.reversed()
$2 ==> [3, 2, 1, 0]

jshell> list.getFirst()
$3 ==> 0

jshell> list.getLast()
$4 ==> 3

jshell> list.addFirst(-1)
|  例外java.lang.UnsupportedOperationException
|        at ImmutableCollections.uoe (ImmutableCollections.java:142)
|        at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections.java:258)
|        at List.addFirst (List.java:796)
|        at (#5:1)
    
jshell>

reverseメソッドは逆順にしたリストを返すだけで、自分自身の順序は変更しません。

また、List.ofメソッドで作成されたリストはイミュータブルになるので、addFirst/addLast/removeFirst/removeLastメソッドをコールすると、上記のようにUnsupportedOperationException例外をスローします。

なお、reverse以外のメソッドはdefaultメソッドで実装されています。注意が必要なのは、addFirst/addLastメソッドのdefaultメソッドはUnsupportedOperationException例外をスローしていることです。このため、SequencedCollectionインタフェースの実装クラスの中にはaddFirst/addLastメソッドが使えないものがあります。

 

セット以外のインタフェースでは、Listインタフェース、Dequeインタフェース、BlockingDequeインタフェースがSequencedCollectionインタフェースの派生インタフェースになります。

なお、Queueインタフェースは両端にアクセスできないため、SequencedCollectionインタフェースの派生インタフェースとならないことに注意が必要です(実際にはQueueインタフェースのコンクリートクラスがSequencedCollectionインタフェースを実装していることも多いです)。

 

次にセットです。

セットは本来は順序はありませんが、SortedSetインタフェースおよびNavigableSetインタフェースの実装クラスだけは順序が存在します。また、その順序は実装クラスによって異なります。

SequencedSetインタフェースはSequencedCollectionインタフェースの派生インタフェースで、SortedSetインタフェースのスーパーインタフェースになります(NavigableSetインタフェースはSortedSetインタフェースの派生インタフェースです)。SequencedSetインタフェースはメソッドを1つだけ定義しています。

  • SequencedSet<E> reversed()

reversedメソッドの返り値の型がSequencedSetインタフェースになっただけですね。

SequencedSetインタフェースの派生インタフェースは前述したSortedSetインタフェースおよびNavigableSetインタフェースです。

これらのインタフェースの実装クラスにはTreeSetクラス、LinkedHashSetクラス、ConcurrentSkipListSetクラスがあります。LinkedHashSetクラスだけはSequencedSetインタフェースを直接実装しています。

なお、TreeSetクラスは前述したaddFirst/addLastメソッドのdefaultメソッドがそのまま使われています。つまり、addFirst/addLastメソッドを使用するとUnsupportedOperationException例外がスローされます。

 

最後がマップです。

マップもセットと同様に基本的には順序はありませんが、SortedMapインタフェースもしくはNavigableMapインタフェースの実装クラスは順序を持ちます。

SequencedMapインタフェースはSortedMapインタフェースのスーパーインタフェースになります(NavigableMapインタフェースはSortedMapインタフェースの派生インタフェースです)。

SequencedMapインタフェースが定義しているメソッドは以下の通り。

  • SequencedMap<K,V> reversed()
  • Map.Entry<K,V> firstEntry()
  • Map.Entry<K,V> lastEntry()
  • Map.Entry<K,V> pollFirstEntry()
  • Map.Entry<K,V> pollLastEntry()
  • V putFirst(K k, V v)
  • V putLast(K k, V v)
  • SequencedSet<K> sequencedKeySet()
  • SequencedCollection<V> sequencedValues()
  • SequencedSet<Map.Entry<K,V>> sequencedEntrySet()

SequencedMapインタフェースでは、アクセスする単位がキーと値がペアになったMap.Entryインタフェースとなります。SortedMapインタフェースがfirstKeyメソッドなどを定義しているのとは対照的です。

firstEntryメソッドとpollFirstEntryメソッドの違いは、firstEntryメソッドが単に最初のエントリーと取得するのに対し、pollFirstEntryメソッドは最初のエントリーを返して、そのエントリーをマップから削除するという点にあります。

lastEntryメソッドとpollLastEntryメソッドの違いも同じです。

jshell> var map = new TreeMap<String, String>(Map.of("a", "Alpha", "b", "Bravo", "c", "Charlie"))
map ==> {a=Alpha, b=Bravo, c=Charlie}

jshell> map.firstEntry()
$3 ==> a=Alpha

jshell> map
map ==> {a=Alpha, b=Bravo, c=Charlie}

jshell> map.pollFirstEntry()
$5 ==> a=Alpha

jshell> map
map ==> {b=Bravo, c=Charlie}
      
jshell>

SequencedMapインタフェースもreversedメソッド以外はdefaultメソッドで定義されています。SequencedMapインタフェースもSequencedCollectionインタフェースと同様にputFirst/putLastメソッドだけはUnsupportedOperationException例外をスローするようになっています。

SequencedMapインタフェースの実装クラスには、TreeMapクラス、LinkedHashMapクラス、ConcurrentSkipListMapクラスがあります。TreeMapクラスとConcurrentSkipListMapクラスはNavigableMapインタフェースを実装したクラスです。LinkedHashMapクラスだけはSequencedMapインタフェースを直接実装します。

なお、TreeSetクラスと同様にTreeMapメソッドもputFirst/putLastメソッドがdefaultメソッドのままになっています。このため、TreeMapクラスでputFirst/putLastメソッドを使用すると、UnsupportedOperationException例外をスローしてしまうことに注意が必要です。

 

Collectionsクラス

CollectionsクラスにはJEP 431関連のメソッドが4種類と、その他の1種類のメソッドが追加されました。

  • static void shuffle(List<?> list, RandomGenerator rnd)
  • static <E> SequencedSet<E> newSequencedSetFromMap(SequencedMap<E,Boolean> map)
  • static <T> SequencedCollection<T> unmodifiableSequencedCollection(SequencedCollection<? extends T> c)
  • static <T> SequencedSet<T> unmodifiableSequencedSet(SequencedSet<? extends T> s)
  • static <K,V> SequencedMap<K,V> unmodifiableSequencedMap(SequencedMap<? extends K,? extends V> m)

shuffleメソッドは、今まで引数がリストだけのオーバーロードと、リストとRandomクラスのオーバーロードが提供されていました。これに加えて、RandomGeneratorインタフェースのオーバーロードが追加されました。

これで、Randomクラス以外のSecureRandomクラスなども乱数として扱うことができるようになっています。

他の4種類のメソッドはSequencedXを生成するためのメソッドです。

unmodifiableSequencedXメソッドはSequencedXオブジェクトを変更不可にするためのメソッドで、今までのunmodifiableXメソッドと使い方は変わりません。

もう1つのnewSequencedSetFromMapメソッドはnewSetFromMapメソッドのSequencedSet版です。セットの実装にはマップが使われていますが、そのマップを指定してセットを作るメソッドです。あくまでも内部のマップを指定するためであって、マップからセットに変換するメソッドではないことに注意が必要です。

このため、引数のマップは空でなくてはいけません。

newSequencedSetFromMapメソッドも同じで、引数には空のSequencedMapオブジェクトを指定します。

 

Localeクラス

Localeクラスには2種類のメソッドが追加されました。

  • static Stream<Locale> availableLocales()
  • static String caseFoldLanguageTag(String languageTag)

availableLocalesメソッドは使用できるロケールの一覧をStreamオブジェクトで返すメソッドです。今までは配列が戻されるgetAvailableLocalesメソッドを使用していましたが、Stream APIを使用できるようになりました。

もう一方のcaseFoldLanguageTagメソッドは、IETF言語タグを比較する時などのためにCase Foldingするためのメソッドです。

たとえば、Scriptは先頭文字が大文字の略号が使用されるので、小文字で表記されていると大文字に変換されます。

jshell> var tag = "ja-kana-jp"
tag ==> "ja-kana-jp"

jshell> Locale.caseFoldLanguageTag(tag)
$2 ==> "ja-Kana-JP"
      
jshell>

variant以降のタグは大文字と小文字を区別しないため、小文字に統一されます。ただし、lvariantが使用されている場合はそれ以降のタグは変換されないままになります。

jshell> var tag1 = "ja-kana-jp-x-Java-Standard-Edition"
tag1 ==> "ja-kana-jp-x-Java-Standard-Edition"

jshell> Locale.caseFoldLanguageTag(tag1)
$2 ==> "ja-Kana-JP-x-java-standard-edition"

jshell> var tag2 = "ja-kana-jp-x-lvariant-Java-Standard-Edition"
tag2 ==> "ja-kana-jp-x-lvariant-Java-Standard-Edition"

jshell> Locale.caseFoldLanguageTag(tag2)
$4 ==> "ja-Kana-JP-x-lvariant-Java-Standard-Edition"
      
jshell>

 

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

java.util.concurrentパッケージの変更もJEP 431 Sequenced Collectionsによるものがほとんどです。これらは省略して、Virtual Threadに関連した変更を紹介します。

 

Executorsクラス

Virtual Threadに関するメソッドが2種類追加されました。

  • static ExecutorService newVirtualThreadPerTaskExecutor()
  • static ExecutorService newThreadPerTaskExecutor(ThreadFactory threadFactory)

スレッドを使いまわすスレッドプールには応答速度重視型とスループット重視型の2種類があります。一般的には、計算処理がメインであれば応答速度を重視し、Webシステムであればスループットを重視します。

スループットを重視した場合、タスクごとにスレッドを割り当てるThread per Task(もしくはThread per Request)のスレッドプールが用いられてきました。

しかし、従来のスレッドであればスレッド生成が重い処理であり、メモリも大量に使用してしまいます。そこで、導入されたのがVirtual Threadです。

Virtual Threadは従来のPlatform Thread上で動作しますが、ユーザーからは通常のスレッドと同じように動作します。

newVirtualPerTaskExecutorメソッドは、Virtual Threadに対応したThread per Taskタイプのスレッドプールを生成します。

もう一方のnewThreadPerTaskExecutorメソッドは引数にThreadFactoryオブジェクトを指定します。Virtual Threadに対応したThreadFactoryオブジェクトであればVirtual Threadを使用しますが、それ以外では従来のPlatform Threadになります。

 

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

正規表現を表すPatternクラスにメソッドが追加されました。

 

Patternクラス

Stringクラスで追加されたメソッドの大本のメソッドが追加されました。

  • String[] splitWithDelimiters(CharSequence input, int limit)

StringクラスのsplitWithDelimitersメソッドは内部で、PatternクラスのsplitWithDelimitersメソッドをコールしています。

 

また、APIの変更ではありませんが、正規表現で絵文字のプロパティが使えるようになりました。追加されたプロパティは以下の6種類です。

  • Emoji
  • Emoji_Presentation
  • Emoji_Modifier
  • Emoji_Modifier_Base
  • Emoji_Component
  • Extended_Pictographic

これらのプロパティは\p{IsX}の形式で使用することができます(Xがプロパティ名)。

jshell> Pattern.compile("\\p{IsEmoji}").matcher("😊").matches()
$1 ==> true
    
jshell>

 

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

java.net.httpモジュールはHTTP Client APIを提供しています。その中のHttpClientクラスに変更がありました。

 

HttpClientクラス

HttpClientクラスは今まで明示的にクローズすることをしませんでした。これに対し、Java 21ではAutoClosableになり、クローズを行えるようになります。また、これに伴い5種類のメソッドが追加されました。

  • void close()
  • void shutdown()
  • void shutdownNow()
  • boolean awaitTermination(Duration duration)
  • boolean isTerminated()

HttpClientクラスがAutoClosableインタフェースを実装するようになったため、closeメソッドが追加されています。

shutdownメソッドも通信をクローズしますが、sendメソッドやsendAsyncメソッドが完了するまで待ちます。これに対し、shutdownNowメソッドは完了を待たずにクローズします。

また、awaitTerminationメソッドは指定した時間までは、送受信の完了を待ちます。タイムアウトしてしまった場合はfalseを返します。

closeメソッドは基本的にはshutdownメソッドをコールしますが、完了しない場合awaitTerminationメソッドを1日のタイムアウトでコールします。awaitTerminationメソッドでInterruptedException例外がスローされた場合、shutdownNowメソッドをコールしているようです。

最後のisTerminatedメソッドは送受信が完了しているかどうかを調べるメソッドです。

 

 

といいうことで、Java 21のAPIの変更について紹介しました。APIの変更という観点からすると、JEP 431の変更が大きかったですね。とはいうものの、リスト以外で先頭や末尾にアクセスする場面というのは、ほとんどないような気がしますが...

また、JEP 444 Virtual Threadへの対応ですが、こちらはAPIの変更としては少ないです。逆にいうと、従来のPlatform Threadと同じように使えるということでもあります。

それ以外の変更は意外に少ないです。個人的にはHTTP ClientのAutoClosable対応がうれしいところですね。

 

さて、次のJava 22ではFFM APIが入るのかどうか?それによって、APIの変更度合いも違ってくるはずです。