はじめに (対象読者・この記事でわかること)
本記事は、Java で文字列リテラルを扱う際に「${}」という記法が期待したように展開されないことに疑問を持った開発者を対象としています。Java 初学者から実務でコードを書いているエンジニアまで、幅広い読者が対象です。この記事を読むことで、以下のことが理解できるようになります。
- Java が文字列リテラル内で変数展開を自動的に行わない理由
- 他言語(例: JavaScript、Kotlin)のテンプレート文字列との違い
- 文字列中に変数や式を埋め込むための代表的な手法(
String.format、MessageFormat、Text Blocks、String concatenationなど)
また、実務で「${}」をそのまま文字列として書きたくなるケースと、その対策例も併せて紹介します。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本的な文法(変数宣言、メソッド呼び出しなど)
- JDK がインストールされている環境でのコンパイル・実行手順
Java の文字列リテラルと ${} の挙動(概要・背景)
1. Java の文字列リテラルは「リテラル」そのもの
Java における文字列リテラルは、" "(ダブルクオート)で囲まれた文字列です。コンパイラはその中身をそのままバイトコードに埋め込み、実行時に変更しません。したがって、"${user}" のように書けば、実行結果は文字列 ${user} がそのまま出力されます。
2. 他言語との違い:テンプレート文字列は存在しない
JavaScript のテンプレートリテラル(`Hello ${name}`)や Kotlin の文字列テンプレート("Hello $name")は、コンパイラが文字列中の ${} や $ を検知し、式展開を行います。Java には同等の機能は標準では用意されていません。歴史的に、Java はシンプルさと明確な型安全性を重視した設計方針を取ってきたため、文字列埋め込みは明示的な API 呼び出しで実現するようになっています。
3. 標準 API で提供される文字列埋め込み手段
| 手法 | 主な利用シーン | 例 |
|---|---|---|
String.concat / + 演算子 |
簡単な結合 | "Hello, " + name |
String.format |
書式指定が必要な場合 | String.format("Hello, %s!", name) |
MessageFormat |
ロケール対応のメッセージ | MessageFormat.format("Hello, {0}!", name) |
Text Blocks(Java 15+) |
複数行文字列・可読性向上 | """Hello, ${name}"""(※展開はされない) |
java.util.Formatter |
高度な書式指定 | new Formatter().format("Hex: %04x", value).toString() |
これらはすべて 明示的に 文字列を組み立てる手段であり、${} が自動で評価されることはありません。
文字列に変数や式を埋め込む実装方法(具体的な手順や実装例)
以下では、実務で頻繁に使われる 3 つの手法を中心に、サンプルコードとともに段階的に解説します。
ステップ 1: String.format を使った基本的な埋め込み
String.format は C 言語由来の書式指定子を利用して文字列を組み立てます。シンプルかつ可読性が高く、ロケール対応も可能です。
Javapublic class Greeting { public static void main(String[] args) { String name = "Taro"; int age = 30; // %s は文字列、%d は整数 String message = String.format("こんにちは、%s さん。年齢は %d 歳です。", name, age); System.out.println(message); } }
ポイント
- 書式指定子
%s、%d、%fなどはjava.util.Formatterと同様のルールです。 - 引数の順序が書式文字列と一致していなければ
MissingFormatArgumentExceptionがスローされます。 - ロケールを指定したい場合は
String.format(Locale.JAPAN, ...)と書くと、数値の区切りや日付形式がロケールに合わせて変化します。
ステップ 2: MessageFormat でロケール対応と引数の再利用
MessageFormat はプレースホルダ {0}, {1} を使い、引数のインデックスで埋め込みます。ロケールごとにメッセージパターンを外部ファイルに分離できる点が特徴です。
Javaimport java.text.MessageFormat; import java.util.Locale; public class MailTemplate { public static void main(String[] args) { String template = "Dear {0},\nYour order {1} has been shipped on {2,date,full}."; Object[] argsArray = { "Hanako", 12345, new java.util.Date() }; // デフォルトロケール(日本)でフォーマット String result = MessageFormat.format(template, argsArray); System.out.println(result); } }
ポイント
{2,date,full}のように型とスタイルを指定でき、日付や数値のローカライズが自動で行われます。- パラメータの順序を変えるだけで同一テンプレートを多言語で使い回せます(リソースバンドルと合わせて利用が一般的)。
- ただし、
MessageFormatは内部でjava.text.Format系を使用するため、パフォーマンスがString.formatよりやや劣ります。
ステップ 3: StringBuilder で大量結合時の高速化
大量の文字列結合やループ内での組み立ては + 演算子より StringBuilder が推奨されます。
Javapublic class CsvBuilder { public static void main(String[] args) { String[] headers = {"ID", "名前", "年齢"}; String[][] rows = { {"1", "Taro", "28"}, {"2", "Hanako", "31"}, {"3", "Jiro", "22"} }; StringBuilder sb = new StringBuilder(); // ヘッダー sb.append(String.join(",", headers)).append('\n'); // データ行 for (String[] row : rows) { sb.append(String.join(",", row)).append('\n'); } System.out.println(sb.toString()); } }
ポイント
StringBuilderは可変長の文字列バッファを保持し、appendが O(1) で実行されるため、+連結が内部的にStringBuilderを生成するよりもメモリ効率が良いです。StringBuilderはスレッド非安全です。マルチスレッド環境で必要ならStringBuffer(同期あり)を使用してください。
ステップ 4: Java 21+ の文字列テンプレート(プレビュー機能)
Java 21 では、プレビュー機能として「文字列テンプレート API(java.lang.StringTemplate)」が追加されました。これは ${} 形式の埋め込みを 明示的に サポートします。
Javaimport java.lang.StringTemplate; public class TemplateDemo { public static void main(String[] args) { String name = "Taro"; int score = 85; // テンプレート文字列の作成(プレビュー機能のため -source 21 が必要) StringTemplate tmpl = StringTemplate.of("学生 ${name} の得点は ${score} 点です。"); String result = tmpl.evaluate(Map.of("name", name, "score", score)); System.out.println(result); // => 学生 Taro の得点は 85 点です。 } }
ポイント
- 現在はプレビュー段階(
--enable-previewが必要)で、正式リリースはまだです。 StringTemplateは名前付きプレースホルダをサポートし、型安全性やコンパイル時チェックの拡張が期待されています。- 将来的に標準化されれば、
${}が文字列リテラルの中で直接評価される Java が実現します。
ハマった点やエラー解決
1. String.format のプレースホルダが足りない
JavaString s = String.format("Hello %s %s", "World"); // 引数が 1 つ足りない
エラー: java.util.MissingFormatArgumentException: Format specifier '%s'
対処: プレースホルダの数と引数の数を合わせるか、%1$s のようにインデックス指定を利用する。
2. MessageFormat で {} がエスケープできない
MessageFormat は単に { と } をエスケープできません。代わりにシングルクオートで囲む必要があります。
JavaString pattern = "'{'This is a literal brace'}'"; System.out.println(MessageFormat.format(pattern)); // => {This is a literal brace}
3. StringTemplate のプレビューコンパイル失敗
コンパイルオプションが不足
javac --enable-preview --release 21 TemplateDemo.java のように、プレビュー機能を有効にし、ターゲットバージョンを 21 に設定してください。
まとめ
本記事では、Java の文字列リテラルにおいて ${} が評価されない仕組みと、代替として利用できる主要な文字列組み立て手法を紹介しました。
- Java は文字列リテラルをそのまま保持する設計 のため、
${}は単なる文字列になる。 String.format、MessageFormat、StringBuilderなどの API を組み合わせることで、柔軟かつロケール対応の文字列埋め込みが可能。- Java 21 のプレビュー機能
StringTemplateが登場すれば、将来的にテンプレート文字列が標準化され、${}が直接評価されるようになる見込み。
これらを理解すれば、Java でも可読性の高い文字列処理が実現でき、他言語と同様に動的メッセージ生成が行えるようになります。次回は、StringTemplate の詳細な使い方と、実務プロジェクトでの導入ステップに迫ります。
参考資料
- Java SE Documentation – String (Java Platform SE 21)
- Java SE Documentation – Formatter (Java Platform SE 21)
- Java SE Documentation – MessageFormat (Java Platform SE 21)
- JEP 430: String Templates (Preview)
- 書籍: 「Effective Java(第3版)」 – Joshua Bloch
