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の引数にしています。

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


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

0 件のコメント: