本記事はプレビュー公開時のものです。正式リリース版については以下を参照してください。
※JDK23 にて本JEPは取り下げとなりました。
はじめに
JEP 430 String Templates がプレビュー公開されました。 ようやく Java でも文字列補完(string interpolation)が使えるようになる予定です。
JavaScriptでは ${x} plus ${y} equals ${x + y}
のように書く文字列補完ですが、JEP 430 では、単なる文字列補完を超えた、String Templates として提案されています。
String Templates では、埋め込み式のエスケープやバリデート、テンプレートからオブジェクトを生成といったことが可能です。 プレビュー段階の JEP ではありますが、どのようになるのかを予習しておきましょう。
文字列テンプレート
文字列補完の無い Java で、式の結果を含む文字列を構築するには、以下のように書く必要があります。
String s = x + " plus " + y + " equals " + (x + y);
バインド変数として埋め込む場合は以下のようになります。
String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y); String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y);
いずれの場合でも、可読性に難があります。
文字列テンプレートを使えば以下のように書くことができます。
int x = 10, y = 20; String s = STR."\{x} plus \{y} equals \{x + y}";
以下のように、より複雑な式を埋め込むこともできます(式の中で、"
をエスケープ無しで使えることに注意してください)。
File file = new File(filePath); String msg = STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";
式は複数行に跨ぐことも可能です。
String time = STR."The time is \{ // The java.time.format package is very useful DateTimeFormatter .ofPattern("HH:mm:ss") .format(LocalTime.now()) } right now";
さらに、Java 15 で導入されたテキストブロックも同じように利用できます。
String title = "My Web Page"; String text = "Hello, world"; String html = STR.""" <html> <head> <title>\{title}</title> </head> <body> <p>\{text}</p> </body> </html> """;
STR.
としてテンプレートプロセッサを指定し、\{x + y}
のようにテンプレート式を記載します。他の言語の文字列補間と比べると、多少野暮ったい感じにはなりますが、その分より強力な機能が提供されます(STR
インスタンスは、シングルトン・インスタンスによるステートレス補間を行うので、大文字のフィールド名になっています)。
テンプレート式(\{x + y}
)は、Javaプログラミング言語における新しい種類の式で、文字列テンプレートに式の結果を埋め込むために使用します。
STR
は、Javaプラットフォームで定義されたテンプレートプロセッサで、他に FMT
や RAW
などが提供されます。
STR
は、テンプレートに埋め込まれた各式を、文字列化された式の結果で置き換えます。一般的な文字列補完の用途で使うことができます。
FMT
は、STR
の文字列補完に加え、埋め込み式に書式指定子適用することができます。表示幅を定義したり、小数点表記を指定したりすることができます。
RAW
は、StringTemplate オブジェクトを生成するだけの標準的なテンプレートプロセッサです。カスタマイズされたテンプレートプロセッサを自作する場合などで利用します。
StringTemplate
文字列テンプレートは、StringTemplate
インターフェースとして定義されています。
package java.lang; public interface StringTemplate { // ... @FunctionalInterface public interface Processor<R, E extends Throwable> { R process(StringTemplate st) throws E; } // ... }
StringTemplate.Processor
がテンプレートプロセッサの関数インターフェースで、process
というメソッドで、StringTemplate
を引数に取り、R
型の処理後の値を生成します。
STR
の場合は、処理後の値として String
を生成することになります。
つまり、文字列補完を行う以下の STR
テンプレートプロセッサは、
String info = STR."My name is \{name}";
以下と同等になります。
StringTemplate st = RAW."My name is \{name}"; String info = STR.process(st);
RAW
は、StringTemplate
のインスタンスを生成するテンプレートプロセッサです。
Javaコンパイラは文字列テンプレートを、自動的に StringTemplate
に変換します。
StringTemplate
には、fragments()
と values()
というメソッドが定義されています。
以下のコードがあれば、
int x = 10, y = 20; StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
StringTemplate
は以下の状態になります。
StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
fragments
は StringTemplate
のインスタンス生成後は不変ですが、values
は各評価毎で新たに計算されたものとなります。
fragments()
と values()
を使い、文字列補間を行うテンプレートプロセッサを簡単に自作することができます。
var INTER = StringTemplate.Processor.of((StringTemplate st) -> { StringBuilder sb = new StringBuilder(); Iterator<String> fragIter = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragIter.next()); sb.append(value); } sb.append(fragIter.next()); return sb.toString(); }); int x = 10, y = 20; String s = INTER."\{x} plus \{y} equals \{x + y}"; // -> 10 plus 20 equals 30
事前提供されているユーティリティメソッド StringTemplate::interpolate
は、上記と同様のことを行うため、以下のように書いても同等です。
var INTER = StringTemplate.Processor.of(StringTemplate::interpolate);
FMT テンプレートプロセッサ
FMT
は、補間を行う点では STR
と同じですが、埋め込み式の左側に現れる書式指定子を解釈することができます。
フォーマット指定子は、java.util.Formatter
で定義されているものと同じです。
以下はゾーンテーブルの例で、テンプレートの書式指定子によってフォーマッティングしています。
record Rectangle(String name, double width, double height) { double area() { return width * height; } } Rectangle[] zone = new Rectangle[] { new Rectangle("Alfa", 17.8, 31.4), new Rectangle("Bravo", 9.6, 12.4), new Rectangle("Charlie", 7.1, 11.23), }; String table = FMT.""" Description Width Height Area %-12s\{zone[0].name} %7.2f\{zone[0].width} %7.2f\{zone[0].height} %7.2f\{zone[0].area()} %-12s\{zone[1].name} %7.2f\{zone[1].width} %7.2f\{zone[1].height} %7.2f\{zone[1].area()} %-12s\{zone[2].name} %7.2f\{zone[2].width} %7.2f\{zone[2].height} %7.2f\{zone[2].area()} \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()} """;
以下のような整形された文字列を得ることができます。
Description Width Height Area Alfa 17.80 31.40 558.92 Bravo 9.60 12.40 119.04 Charlie 7.10 11.23 79.73 Total 757.69
テンプレートプロセッサの自作
JSON オブジェクトを、入力値の検証付きで生成するテンプレートプロセッサを考えます。
String name = "Joan Smith"; String phone = "555-123-4567"; String address = "1 Maple Drive, Anytown"; try { JSONObject doc = JSON_VALIDATE.""" { "name": \{name}, "phone": \{phone}, "address": \{address} }; """; } catch (JSONException ex) { ... }
StringTemplate
を受け取り、values
の入力チェックを行い、JSONオブジェクトのインスタンスを返すテンプレートプロセッサは、以下のように書くことができます。
StringTemplate.Processor<JSONObject, JSONException> JSON_VALIDATE = (StringTemplate st) -> { String quote = "\""; List<Object> filtered = new ArrayList<>(); for (Object value : st.values()) { if (value instanceof String str) { if (str.contains(quote)) { throw new JSONException("Injection vulnerability"); } filtered.add(quote + str + quote); } else if (value instanceof Number || value instanceof Boolean) { filtered.add(value); } else { throw new JSONException("Invalid value type"); } } String jsonSource = StringTemplate.interpolate(st.fragments(), filtered); return new JSONObject(jsonSource); };
JDBC接続から ResultSet
を取得することを考えます。
以下のようなテンプレートプロセッサを用意すれば、
record QueryProcessor(Connection conn) implements StringTemplate.Processor<ResultSet, SQLException> { public ResultSet process(StringTemplate st) throws SQLException { // 1. Replace StringTemplate placeholders with PreparedStatement placeholders String query = String.join("?", st.fragments()); // 2. Create the PreparedStatement on the connection PreparedStatement ps = conn.prepareStatement(query); // 3. Set parameters of the PreparedStatement int index = 1; for (Object value : st.values()) { switch (value) { case Integer i -> ps.setInt(index++, i); case Float f -> ps.setFloat(index++, f); case Double d -> ps.setDouble(index++, d); case Boolean b -> ps.setBoolean(index++, b); default -> ps.setString(index++, String.valueOf(value)); } } // 4. Execute the PreparedStatement, returning a ResultSet return ps.executeQuery(); } }
以下のようにしてSQLを発行できます。
StringTemplate.Processor<ResultSet, SQLException> DB = new QueryProcessor(conn); ResultSet rs = DB."SELECT * FROM Person p WHERE p.last_name = \{name}";
ここで注目したいのが、以下のように文字列連結をした場合の、SQLインジェクションのリスクを回避できている ということです。
String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'"; ResultSet rs = conn.createStatement().executeQuery(query);
さらに、ResourceBundleからメッセージを構築するテンプレートプロセッサも考えられます。
record LocalizationProcessor(Locale locale) implements StringTemplate.Processor<String, RuntimeException> { public String process(StringTemplate st) { ResourceBundle resource = ResourceBundle.getBundle("resources", locale); String stencil = String.join("_", st.fragments()); String msgFormat = resource.getString(stencil.replace(' ', '.')); return MessageFormat.format(msgFormat, st.values().toArray()); } }
resources_jp.properties
に以下のメッセージを定義しておけき、
no.suitable._.found.for._(_)={1}に適切な{0}が見つかりません({2})
日本語ロケールで使用できます。
var LOCALIZE = new LocalizationProcessor(new Locale("jp")); var symbolKind = "field", name = "tax", type = "double"; LOCALIZE."no suitable \{symbolKind} found for \{name}(\{type})");
taxに適切なfieldが見つかりません(double)
まとめ
JEP 430 String Templates としてプレビューとなっている文字列テンプレートを見てきました。
文字列補間は、ほとんどの近代言語に存在しますし、HTMLやJSON、SQLといった言語間I/Fとして、テンプレートプロセッサは強力なツールとなるので、早いところ導入してほしいですね。