Javaのテキストブロックとは、""" で囲んで複数行の文字列を「見たままの形」で書ける文字列リテラルです。 ほかの言語でヒアドキュメントと呼ばれる書き方に近く、Java 15 から標準機能として使えます。SQL・JSON・HTML のように改行と " が多い文字列を、\n や + を使わずに書けます。

この記事では、閉じ """ の位置でインデントと末尾の改行がどう変わるかを、空白を「·」、改行を「⏎」に置き換えた実行結果で並べて確認します。あわせて、コンパイルエラーの再現、エスケープの一覧表、formatted() による変数の埋め込み、そのまま貼れる SQL・JSON・HTML の例までを1ページにまとめました。掲載したコードはすべて OpenJDK 21.0.10 で実行し、同じコードを OpenJDK 25.0.4.1 でも実行して出力が一致することを確かめています。

テキストブロックの早見表

やりたいこと 書き方 結果
複数行の文字列を書く """ の直後で改行し、内容を書き、""" で閉じる 見たままの複数行の String
末尾に改行を付ける 閉じ """ を次の行に置く 最後の行の後ろに \n が付く
末尾に改行を付けない 閉じ """ を最後の行の末尾に置く 最後の行で終わる
行頭の空白を残す 閉じ """ を内容より左に置く 左にずらした分だけ空白が残る
長い1行を折り返して書く 行末に \ を書く その位置の改行が入らない
行末の空白を残す 行末に \s を書く 空白1つとして残る
" を書く そのまま " と書く エスケープ不要
""" を書く \""" と書く """ という3文字
変数を埋め込む """ ... %s ... """.formatted(値) %s の位置に値が入る
改行コードをそろえる 何もしなくてよい ソースが CRLF でも常に LF(\n)
項目 内容
使えるバージョン Java 15 以降(13・14 ではプレビュー機能)
型 通常の文字列と同じ java.lang.String
開始の決まり """ の直後は必ず改行(同じ行に内容を書くとコンパイルエラー)
取り除かれるもの 共通のインデント、各行の行末の空白
変数の直接埋め込み 構文としては用意されていない(formatted() などを使う)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaテキストブロックの使い方|複数行文字列とインデントの規則」

テキストブロックの基本の書き方と、従来の + 連結との比較

まずは、同じ JSON 文字列を従来の + 連結とテキストブロックの両方で書き、結果が同じになることを確かめます。ファイル名を Compare.java として保存し、java Compare.java で実行できます(Java 11 以降は、1ファイルのプログラムをコンパイルなしで実行できます)。

public class Compare {
    public static void main(String[] args) {
        // 従来の + 連結
        String old = "{\n" +
                     "  \"id\": 101,\n" +
                     "  \"title\": \"備品の棚卸し\"\n" +
                     "}\n";

        // テキストブロック
        String block = """
                {
                  "id": 101,
                  "title": "備品の棚卸し"
                }
                """;

        System.out.print(block);
        System.out.println("equals: " + old.equals(block));
        System.out.println("==    : " + (old == block));
        System.out.println("型    : " + block.getClass().getName());
    }
}

実行結果(Java 21.0.10):

{
  "id": 101,
  "title": "備品の棚卸し"
}
equals: true
==    : true
型    : java.lang.String

テキストブロックは、新しい型ではなく「String の別の書き方」です。 equals が true になるだけでなく、== も true になっています。どちらもコンパイル時に同じ内容の文字列定数になり、同じインスタンスが共有されるためです。つまり、実行時の速度やメモリの面で差は生まれません。違いは「ソースコードの読みやすさ」にあります。

比べる点 + 連結 テキストブロック
改行 行ごとに \n を書く 改行した位置がそのまま改行になる
" \" とエスケープする そのまま書ける
行の連結 行ごとに + を書く 不要
インデント 文字列の中に空白を書く ソースのインデントは自動で取り除かれる
コンパイル後 文字列定数 同じ文字列定数

String なので、メソッドの引数に直接渡すことも、+ でほかの文字列とつなぐこともできます。

閉じ """ の位置でインデントと末尾改行がどう変わるか

テキストブロックでいちばん大切な規則が、閉じ """ をどこに置くかです。位置によって「行頭に空白が残るか」「末尾に改行が付くか」が変わります。4つの置き方を、空白を「·」、改行を「⏎」に置き換えて並べます。

public class Show {
    // 空白を「·」、改行を「⏎」に置き換えて表示する
    static String vis(String s) {
        return s.replace(" ", "·").replace("\r", "␍").replace("\n", "⏎\n");
    }

    public static void main(String[] args) {
        // A: 閉じ """ を内容と同じ列に置く
        String a = """
                <ul>
                  <li>りんご</li>
                </ul>
                """;

        // B: 閉じ """ を最後の行の末尾に置く
        String b = """
                <ul>
                  <li>りんご</li>
                </ul>""";

        // C: 閉じ """ を内容より4つ左に置く
        String c = """
                <ul>
                  <li>りんご</li>
                </ul>
            """;

        // D: 閉じ """ を内容より右に置く
        String d = """
                <ul>
                  <li>りんご</li>
                </ul>
                        """;

        System.out.println("--- A: 同じ列 (length=" + a.length() + ")");
        System.out.println(vis(a) + "[終わり]");
        System.out.println("--- B: 末尾 (length=" + b.length() + ")");
        System.out.println(vis(b) + "[終わり]");
        System.out.println("--- C: 4つ左 (length=" + c.length() + ")");
        System.out.println(vis(c) + "[終わり]");
        System.out.println("--- D: 右 (length=" + d.length() + ")");
        System.out.println(vis(d) + "[終わり]");
    }
}

実行結果(Java 21.0.10):

--- A: 同じ列 (length=26)
<ul>⏎
··<li>りんご</li>⏎
</ul>⏎
[終わり]
--- B: 末尾 (length=25)
<ul>⏎
··<li>りんご</li>⏎
</ul>[終わり]
--- C: 4つ左 (length=38)
····<ul>⏎
······<li>りんご</li>⏎
····</ul>⏎
[終わり]
--- D: 右 (length=26)
<ul>⏎
··<li>りんご</li>⏎
</ul>⏎
[終わり]
置き方 行頭の空白 末尾の改行 向いている用途
A: 内容と同じ列 残らない 付く ファイルに書き出す文章、SQL、複数行のメッセージ
B: 最後の行の末尾 残らない 付かない 比較に使う期待値、1行として扱う値
C: 内容より左 左にずらした分だけ残る 付く 全体を字下げして出力したいとき
D: 内容より右 残らない(Aと同じ) 付く 特に使い分ける場面はない

インデントが決まる仕組み

なぜ C だけ空白が残り、D は A と同じになるのでしょうか。コンパイラは次の手順でインデントを決めています。

  1. 内容のある行(空白だけの行は除く) と、閉じ """ だけが書かれた行を集める。
  2. それぞれの行頭の空白の数を数え、いちばん少ない数を求める。
  3. すべての行の行頭から、その数だけ空白を取り除く。

A では内容の3行が16・18・16個、閉じ """ の行が16個なので、最小は16です。全行から16個取り除くと、<li> の行にだけ2個残ります。C では閉じ """ の行が12個なので最小が12になり、内容の行に4個ずつ残ります。D では閉じ """ の行が24個ですが、最小は内容の行の16個のままなので、結果は A と変わりません。

「閉じ """ は、左にずらしたときだけインデントに影響する」 と覚えると、迷わず使えます。B のように閉じ """ が内容と同じ行にある場合は、その行は「内容のある行」として数えられます。

flowchart LR
    A["1. 改行コードを<br>LFにそろえる"] --> B["2. 共通のインデントと<br>行末の空白を取り除く"]
    B --> C["3. エスケープ<br>(\\n や \\s など)を解釈する"]

処理は上の3段階の順に進みます。この順番が、あとで出てくる「\s を使うと行末の空白が残る」理由になります。

タブと空白を混ぜるとどうなるか

インデントを数えるとき、タブ1つは空白1つと同じ「1文字」として扱われます。1行目をタブ1つ、2行目と閉じ """ を空白8個で字下げしたコード(Tab.java)を実行した結果が次のとおりです。

タブ1つ
·······空白8つ

エディタ上では同じ位置にそろって見えても、最小のインデントは「タブの1文字」になるため、2行目に空白が7個残りました。javac -Xlint:text-blocks を付けてコンパイルすると、次の警告で知らせてくれます。

Tab.java:3: warning: [text-blocks] inconsistent white space indentation
        String s = """
                   ^

テキストブロックの字下げは、タブか空白のどちらかにそろえると、見た目どおりの結果になります。

開始 """ の直後に文字を書くとコンパイルエラーになる

テキストブロックは、開始の """ の直後で必ず改行する決まりです。同じ行に内容を書いた場合のエラーを再現します。

public class Err1 {
    public static void main(String[] args) {
        String s = """Hello
            World
            """;
        System.out.println(s);
    }
}

実行結果(Java 21.0.10。Java 25.0.4.1 でも同じメッセージ):

Err1.java:3: error: illegal text block open delimiter sequence, missing line terminator
        String s = """Hello
                      ^
Err1.java:5: error: illegal text block open delimiter sequence, missing line terminator
            """;
               ^
2 errors
error: compilation failed

missing line terminator(行の終わりがない)というメッセージのとおり、開始の """ の後ろに改行が必要です。2つ目のエラーは、最初の """ が開始として成立しなかったために、閉じのつもりで書いた """ が「新しい開始」と解釈されて出ています。直すのは3行目の1か所だけです。

        String s = """
            Hello
            World
            """;

この決まりがあるため、"""abc""" のように1行で書くことはできません。1行で済む文字列は、通常の "abc" で書きます。なお、開始の """ の直後の改行は文字列に含まれません。内容は次の行から始まります。

閉じ忘れたときのエラー

閉じの """ を書き忘れた場合は、別のメッセージになります。

Err2.java:3: error: unclosed text block
        String s = """
                   ^

unclosed text block(閉じていないテキストブロック)と、開始位置を示してくれます。内容に """ を含めたくて途中で閉じてしまった場合も、このエラーや文法エラーにつながります。書き方は次の章のエスケープ一覧を参照してください。

エラーメッセージ 原因 直し方
illegal text block open delimiter sequence, missing line terminator 開始 """ と同じ行に内容がある """ の直後で改行する
unclosed text block 閉じ """ がない 末尾に """ を追加する
illegal escape character \d など定義されていないエスケープを書いた \\d と書く

エスケープ一覧表と行末の空白が消える規則

テキストブロックの中でも、通常の文字列と同じエスケープが使えます。加えて、テキストブロックのために用意された \(行末)と \s があります。

書き方 意味 テキストブロックでの使いどころ
\+改行 改行を入れずに次の行へ続ける 長い1行をソース上だけ折り返す
\s 空白1つ 行末の空白を残す
\""" """ という3文字 内容に " を3つ続けて書く
\" " 通常は不要(そのまま " と書ける)
\n 改行 行の途中に改行を入れる
\t タブ タブを目に見える形で書く
\\ \ そのもの Windows のパス、正規表現
\r CR 改行を CRLF にしたいとき(\r を行末に書く)

次のコードで、ひととおり確かめます。

public class Esc {
    static String vis(String s) {
        return s.replace(" ", "·").replace("\t", "→").replace("\n", "⏎\n");
    }

    public static void main(String[] args) {
        // 1. \ (行末) : 改行を入れずに次の行へ続ける
        String oneLine = """
                SELECT id, name \
                FROM members \
                WHERE active = true
                """;
        System.out.println("1) " + vis(oneLine));

        // 2. 行末の空白は取り除かれる(下の行の「赤」の後ろに空白を3つ書いている)
        String trimmed = """
                赤   
                青
                """;
        System.out.println("2) " + vis(trimmed));

        // 3. \s : 空白1つ。行末の空白を残したいときに使う
        String kept = """
                赤  \s
                青
                """;
        System.out.println("3) " + vis(kept));

        // 4. " はそのまま書ける。""" を書くときは1つだけ \ を付ける
        String quotes = """
                {"name": "Sato"}
                3つ続ける: \"""
                """;
        System.out.println("4) " + vis(quotes));

        // 5. \n と \t は通常の文字列と同じ
        String nt = """
                a\tb\nc
                """;
        System.out.println("5) " + vis(nt));

        // 6. \\ でバックスラッシュそのもの
        String path = """
                C:\\work\\memo.txt
                """;
        System.out.println("6) " + vis(path));

        // 7. 空のテキストブロック
        String empty = """
                """;
        System.out.println("7) length=" + empty.length());

        // 8. 途中の空行は改行だけが残る
        String blank = """
                1行目

                3行目
                """;
        System.out.println("8) " + vis(blank));
    }
}

実行結果(Java 21.0.10。「→」はタブ):

1) SELECT·id,·name·FROM·members·WHERE·active·=·true⏎

2) 赤⏎
青⏎

3) 赤···⏎
青⏎

4) {"name":·"Sato"}⏎
3つ続ける:·"""⏎

5) a→b⏎
c⏎

6) C:\work\memo.txt⏎

7) length=0
8) 1行目⏎
⏎
3行目⏎

行末の空白は自動で取り除かれる

2) の結果を見ると、「赤」の後ろに書いた3つの空白がなくなっています。テキストブロックでは、各行の行末にある空白が自動で取り除かれます。 エディタやコピー&ペーストで紛れ込んだ目に見えない空白が、文字列の内容に影響しないようにするための規則です。

行末の空白を意図して残したいときは、3) のように \s を使います。エスケープの解釈は「行末の空白を取り除いた後」に行われるため、\s とその手前の空白は取り除かれずに残ります。3) では空白2つ+\s で、合計3つの空白になっています。固定長のデータや、行末に空白が必要な表示を作るときに役立ちます。

行末に空白があると、-Xlint:text-blocks を付けたコンパイルで次の警告が出ます。意図したものかどうかを見直すきっかけになります。

Esc.java:16: warning: [text-blocks] trailing white space will be removed
        String trimmed = """
                         ^

行末の \ で長い1行を折り返す

1) では、3行に分けて書いた SQL が1行の文字列になっています。行末の \ は「ここでは改行しない」という意味です。\ の直前の空白は残るので、単語の区切りの空白は \ の前に書きます。ログのメッセージや長い説明文のように、文字列としては1行にしたいけれどソースでは折り返したい場合に向いています。

" はそのまま、""" だけエスケープ

4) のとおり、" は1つでも2つ続けてもそのまま書けます。3つ続く場合だけ、どれか1つに \ を付けます。JSON や HTML の属性のように " が多い文字列ほど、テキストブロックの読みやすさが生きてきます。

変数を埋め込む(formatted と String.format)

テキストブロックには、${name} のように変数を直接書く構文はありません。値を埋め込むときは、formatted() メソッドを使うのが手軽です。formatted() は Java 15 で String に追加されたメソッドで、String.format(この文字列, 引数...) と同じ結果を返します。

import java.text.MessageFormat;

public class Fmt {
    public static void main(String[] args) {
        String name = "佐藤";
        int count = 3;
        double total = 12800.5;

        // 1. formatted() : テキストブロックにそのまま続けて書ける
        String mail = """
                %s 様
                ご注文 %d 点を受け付けました。
                合計: %,.1f 円
                """.formatted(name, count, total);
        System.out.print(mail);
        System.out.println("---");

        // 2. String.format : 結果は同じ
        String template = """
                %s 様
                ご注文 %d 点を受け付けました。
                合計: %,.1f 円
                """;
        System.out.println(String.format(template, name, count, total).equals(mail));
        System.out.println("---");

        // 3. % そのものを出すときは %%
        System.out.print("""
                進み具合: %d%%
                """.formatted(80));
        System.out.println("---");

        // 4. 同じ値を何度も使うときは %1$s のように番号で指定
        System.out.print("""
                <a href="/members/%1$s">%1$s</a> (%2$d件)
                """.formatted("sato", 5));
        System.out.println("---");

        // 5. 名前に近い形で書きたいときは replace
        String json = """
                {"name": "${name}", "count": ${count}}
                """.replace("${name}", name).replace("${count}", String.valueOf(count));
        System.out.print(json);
        System.out.println("---");

        // 6. MessageFormat : {0} {1} の番号。' は '' と2つ書く
        System.out.print(MessageFormat.format("""
                {0} 様、{1} 件の新着があります。
                """, name, count));
    }
}

実行結果(Java 21.0.10):

佐藤 様
ご注文 3 点を受け付けました。
合計: 12,800.5 円
---
true
---
進み具合: 80%
---
<a href="/members/sato">sato</a> (5件)
---
{"name": "佐藤", "count": 3}
---
佐藤 様、3 件の新着があります。
方法 書き方 向いている場面
formatted() """...%s...""".formatted(a, b) 第一候補。テンプレートのすぐ後ろに値を書ける
String.format String.format(template, a, b) テンプレートを定数として別に持つとき、ロケールを指定するとき
%1$s の番号指定 """...%1$s...%1$s...""".formatted(a) 同じ値を複数の場所に入れるとき
replace .replace("${name}", name) 置き換える場所が少なく、名前で示したいとき
MessageFormat MessageFormat.format(text, a, b) {0} 形式のメッセージ定義と合わせるとき

よく使う書式指定子は次のとおりです。

指定子 意味 例 → 結果
%s 文字列(どの型でも可) "%s".formatted("A") → A
%d 整数 "%d".formatted(3) → 3
%,d 3桁区切りの整数 "%,d".formatted(12800) → 12,800
%.1f 小数第1位まで "%.1f".formatted(2.55) → 2.6
%% % そのもの "%d%%".formatted(80) → 80%
%n 実行環境の改行コード Windows では CRLF、Linux では LF

テキストブロックに % を含む文章(例: 「80%」)を書いて formatted() を呼ぶときは、%% と書く点を押さえておくと、UnknownFormatConversionException などの例外に出会わずに済みます。値を埋め込まないテキストブロックでは、% はそのまま書けます。

文字列テンプレート(STR."...")の現状

変数を \{name} の形で直接書ける「文字列テンプレート」は、Java 21 と 22 でプレビュー機能として提供されました。その後の経過は次のとおりです。

版 状況 確認方法
Java 21 プレビュー(--enable-preview を付けると使える) 手元の Java 21.0.10 で実行
Java 22 2回目のプレビュー JEP 465 のページの記載で確認
正式化の提案(JEP 465) Closed / Withdrawn(取り下げ) OpenJDK の JEP 465 のページで確認
Java 25 構文として含まれていない 手元の Java 25.0.4.1 で実行

同じコードを2つの版で実行した結果です。

public class Tmpl {
    public static void main(String[] args) {
        String name = "佐藤";
        String s = STR."こんにちは、\{name} さん";
        System.out.println(s);
    }
}

Java 21.0.10 で java --enable-preview --source 21 Tmpl.java を実行:

Note: Tmpl.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.
こんにちは、佐藤 さん

Java 25.0.4.1 で java Tmpl.java を実行:

Tmpl.java:4: error: illegal escape character
        String s = STR."こんにちは、\{name} さん";
                               ^
1 error
error: compilation failed

Java 25 では \{ が定義されていないエスケープとして扱われ、コンパイルエラーになりました。長く使うコードでは、formatted() を選んでおくと、どの版でも同じように動きます。

SQL・JSON・HTML のコピペ例

よく使う3種類の文字列を、そのまま貼って使える形でまとめます。

public class Samples {
    public static void main(String[] args) {
        // SQL
        String sql = """
                SELECT o.id, o.ordered_at, m.name
                FROM orders o
                JOIN members m ON m.id = o.member_id
                WHERE o.status = ?
                  AND o.ordered_at >= ?
                ORDER BY o.ordered_at DESC
                """;
        System.out.print(sql);
        System.out.println("=====");

        // JSON
        String json = """
                {
                  "id": 101,
                  "title": "備品の棚卸し",
                  "tags": ["総務", "月次"],
                  "done": false
                }
                """;
        System.out.print(json);
        System.out.println("=====");

        // HTML
        String html = """
                <!DOCTYPE html>
                <html lang="ja">
                  <body>
                    <h1 class="title">お知らせ</h1>
                    <p>今月の記事は 3 件です。</p>
                  </body>
                </html>
                """;
        System.out.print(html);
    }
}

実行結果(Java 21.0.10):

SELECT o.id, o.ordered_at, m.name
FROM orders o
JOIN members m ON m.id = o.member_id
WHERE o.status = ?
  AND o.ordered_at >= ?
ORDER BY o.ordered_at DESC
=====
{
  "id": 101,
  "title": "備品の棚卸し",
  "tags": ["総務", "月次"],
  "done": false
}
=====
<!DOCTYPE html>
<html lang="ja">
  <body>
    <h1 class="title">お知らせ</h1>
    <p>今月の記事は 3 件です。</p>
  </body>
</html>

ソースコードの字下げ(16個の空白)は取り除かれ、SQL の AND や JSON の中の字下げだけが残っています。

用途ごとのポイント

用途 ポイント
SQL 値は ? のプレースホルダで渡す。formatted() で値を SQL に埋め込むと、入力しだいで SQL の構造が変わってしまうため、PreparedStatement の setString などで渡す
JSON " をそのまま書けるので、テストの期待値や送信するサンプルを書きやすい。値を動的に組み立てる場合は JSON ライブラリでオブジェクトから生成すると、エスケープを任せられる
HTML 画面に表示する値を埋め込むときは、< や & を HTML エスケープしてから渡す
テストの期待値 末尾の改行の有無(A の置き方か B の置き方か)を、比べる相手に合わせる
複数行のメッセージ 行末の \ で折り返すと、文字列は1行のままソースを読みやすくできる

SQL を + 連結で書くと、行末の空白を書き忘れて nameFROM のように単語がつながることがあります。テキストブロックは改行がそのまま区切りになるため、この種の書き間違いが起きにくくなります。

+ でつないで一部だけ差し替える

テーブル名のようにプレースホルダにできない部分を差し込みたいときは、テキストブロック同士を + でつなぐこともできます。

public class Concat {
    public static void main(String[] args) {
        String table = "members";
        // テキストブロックも String なので + で連結できる
        String sql = """
                SELECT id, name
                FROM\s""" + table + """

                WHERE active = true
                """;
        System.out.print(sql);
    }
}

実行結果(Java 21.0.10):

SELECT id, name
FROM members
WHERE active = true

FROM の後ろの空白は行末にあたるので、\s で残しています。2つ目のテキストブロックは、開始 """ の直後の改行が内容に含まれないため、空行を1つ入れて改行を作っています。読みやすさの面では、こうした場合も %s と formatted() を使うほうがすっきりします。差し込む値は、あらかじめ決めた候補の中から選ぶ形にしておくと安心です。

stripIndent・translateEscapes・indent を通常の文字列に使う

テキストブロックと同じ処理を、通常の String に対して実行時に行うメソッドが、Java 15 で追加されています。ファイルや画面から受け取った文字列を整えるときに使えます。

メソッド 働き 追加された版
stripIndent() 共通のインデントと行末の空白を取り除く(テキストブロックと同じ規則) Java 15
translateEscapes() 文字として入っている \n や \t をエスケープとして解釈する Java 15
indent(n) 各行に n 個の空白を足す(負の数なら取り除く)。各行の末尾を \n にそろえる Java 12
formatted(args...) String.format(this, args...) と同じ Java 15
public class Strip {
    static String vis(String s) {
        return s.replace(" ", "·").replace("\t", "→").replace("\n", "⏎\n");
    }

    public static void main(String[] args) {
        // 1. ファイルや画面から受け取った、インデント付きの文字列を想定
        String raw = "    件名: 在庫の確認   \n      本文: 棚Aを確認\n    以上";
        System.out.println("[元の文字列]");
        System.out.println(vis(raw));
        System.out.println("[stripIndent() の後]");
        System.out.println(vis(raw.stripIndent()));

        // 2. 末尾が改行で終わると、最後の「空の行」がインデント0として数えられる
        String rawNl = raw + "\n";
        System.out.println("[末尾に改行がある文字列に stripIndent()]");
        System.out.println(vis(rawNl.stripIndent()));

        // 3. 文字として入っている \n や \t を、エスケープとして解釈し直す
        String literal = "名前\\t数量\\nりんご\\t3";
        System.out.println("[元の文字列]");
        System.out.println(literal);
        System.out.println("[translateEscapes() の後]");
        System.out.println(literal.translateEscapes());

        // 4. indent(n) : 各行にn個の空白を足す(負の数なら削る)。末尾に改行が付く
        System.out.println("[indent(2)]");
        System.out.println(vis("a\nb".indent(2)));
    }
}

実行結果(Java 21.0.10):

[元の文字列]
····件名:·在庫の確認···⏎
······本文:·棚Aを確認⏎
····以上
[stripIndent() の後]
件名:·在庫の確認⏎
··本文:·棚Aを確認⏎
以上
[末尾に改行がある文字列に stripIndent()]
····件名:·在庫の確認⏎
······本文:·棚Aを確認⏎
····以上⏎

[元の文字列]
名前\t数量\nりんご\t3
[translateEscapes() の後]
名前  数量
りんご 3
[indent(2)]
··a⏎
··b⏎

2 の結果に注目してください。 末尾が改行で終わる文字列に stripIndent() を使うと、行末の空白は取り除かれますが、インデントはそのまま残りました。最後の改行の後ろにある「空の行」が、テキストブロックでいう「閉じ """ だけの行(インデント0)」と同じ扱いになるためです。これは、先ほどの C の置き方(閉じ """ を左端に寄せる)と同じ規則です。受け取った文字列のインデントを取り除きたいときは、末尾の改行を外してから stripIndent() を呼ぶと、期待どおりになります。

改行コードは常に LF になる

Windows で書いたソースファイルは、改行が CRLF(\r\n)で保存されていることがあります。テキストブロックの中の改行は、ソースファイルの改行コードにかかわらず、常に LF(\n)にそろえられます。 改行をすべて CRLF にしたソースファイルで確かめます。

public class Crlf {
    public static void main(String[] args) {
        String s = """
                1行目
                2行目
                """;
        System.out.println("CRを含む: " + s.contains("\r"));
        System.out.println("length  : " + s.length());
        for (char ch : s.toCharArray()) {
            if (ch == 10 || ch == 13) System.out.print((int) ch + " ");
        }
        System.out.println();
        String win = s.replace("\n", "\r\n");
        System.out.println("CRLFに変換後 length: " + win.length());
        System.out.println("lineSeparator 版 length: " + s.replace("\n", System.lineSeparator()).length());
    }
}

実行結果(Java 21.0.10、Linux。ソースファイルは CRLF で保存):

CRを含む: false
length  : 8
10 10 
CRLFに変換後 length: 10
lineSeparator 版 length: 8

ソースが CRLF でも、文字列に入っているのは文字コード10(LF)だけです。同じソースからは、どの OS でコンパイルしても同じ文字列ができます。チームの中で OS が混ざっていても、テストの期待値がずれないのはこの規則のおかげです。

やりたいこと 書き方
CRLF の文字列にしたい(CSV の出力など) text.replace("\n", "\r\n")
実行する OS の改行に合わせたい text.replace("\n", System.lineSeparator())
特定の行だけ CRLF にしたい 行末に \r を書く
行ごとに処理したい text.lines()(LF・CR・CRLF のどれでも区切れる)

なお、System.out.println や %n は実行環境の改行コードを使います。テキストブロックの中の改行(常に LF)とは別の仕組みなので、ファイルに書き出す形式が決まっている場合は、上の表の方法で明示的にそろえます。

Java 21 と Java 25 の違い

この記事のコードを、OpenJDK 21.0.10 と OpenJDK 25.0.4.1(Ubuntu のパッケージを展開して使用)の両方で実行して、出力を比べました。

確認したコード Java 21.0.10 Java 25.0.4.1
閉じ """ の位置(Show) 上記の結果 同じ出力
エスケープ一覧(Esc) 上記の結果 同じ出力
変数の埋め込み(Fmt) 上記の結果 同じ出力
SQL・JSON・HTML(Samples) 上記の結果 同じ出力
+ 連結との比較(Compare) 上記の結果 同じ出力
stripIndent など(Strip) 上記の結果 同じ出力
改行コード(Crlf) 上記の結果 同じ出力
開始 """ 直後の文字(Err1) コンパイルエラー 同じメッセージ
文字列テンプレート(Tmpl) --enable-preview で実行できる コンパイルエラー

テキストブロックそのものの動きは、21 と 25 で変わりません。 違いが出たのは、プレビューだった文字列テンプレートだけです。

よくあるつまずきと直し方

症状 原因 直し方
出力の行頭に余分な空白が付く 閉じ """ が内容より左にある 閉じ """ を内容と同じ列にそろえる
出力の最後に空行が入る 閉じ """ を次の行に置き、さらに println で出力している print を使う、または閉じ """ を最後の行の末尾に置く
assertEquals で期待値と一致しない 末尾の改行の有無が違う 比べる相手に合わせて A か B の置き方を選ぶ
行末に書いた空白がなくなる 行末の空白は自動で取り除かれる 行末に \s を書く
エディタではそろっているのに字下げがずれる タブと空白が混ざっている どちらかにそろえる(-Xlint:text-blocks で検出できる)
formatted() で例外が出る 文章中の % が書式指定子と解釈された %% と書く
正規表現の \d でコンパイルエラー \d は Java のエスケープに定義がない \\d と書く(通常の文字列と同じ)
開始行でコンパイルエラー """ の直後に内容を書いている """ の直後で改行する

書く前のチェックリスト

  • [ ] 開始の """ の直後で改行した
  • [ ] 閉じの """ の位置を、末尾の改行が必要かどうかで決めた
  • [ ] 字下げはタブか空白のどちらかにそろえた
  • [ ] 行末に必要な空白は \s で書いた
  • [ ] 値の埋め込みは formatted() を使い、文章中の % は %% にした
  • [ ] SQL の値は ? で渡すようにした

よくある質問

Q. テキストブロックは Java のどのバージョンから使えますか? A. 標準機能として使えるのは Java 15 からです。Java 13 と 14 ではプレビュー機能として提供されていました。行末の \ と \s は、プレビューの途中(Java 14)で加わったエスケープです。Java 8 や 11 では使えないため、その場合は + 連結や String.join("\n", ...) で書きます。

Q. テキストブロックの中に変数を直接書けますか? A. ${name} のような構文はありません。"""...%s...""".formatted(name) のように、formatted() で埋め込むのが基本です。\{name} と書ける文字列テンプレートは Java 21・22 でプレビューとして提供されましたが、正式化の提案(JEP 465)は取り下げられており、手元の Java 25.0.4.1 ではコンパイルエラーになりました。

Q. 末尾の改行をなくすにはどうすればよいですか? A. 閉じの """ を、最後の行の末尾に続けて書きます(</ul>""" のように)。閉じの """ の位置はそのままにして、最後の行の末尾に \ を書く方法でも、最後の改行が入らなくなります。

Q. テキストブロックを使うと処理は遅くなりますか? A. 変わりません。インデントの除去やエスケープの解釈はコンパイル時に済み、クラスファイルには通常の文字列定数として入ります。この記事の実行結果でも、同じ内容の + 連結の文字列と == で比べて true になりました。

まとめ

  • テキストブロックは """ で囲む複数行の文字列リテラルで、型は通常の String。Java 15 以降で使える
  • 開始の """ の直後は必ず改行する。 同じ行に内容を書くと illegal text block open delimiter sequence のコンパイルエラーになる
  • 閉じの """ の位置で結果が決まる。 次の行に置くと末尾に改行が付き、最後の行の末尾に置くと付かない。内容より左に置くと、その分だけ行頭に空白が残る
  • 行末の空白は自動で取り除かれる。 残したいときは \s、改行を入れずに折り返したいときは行末に \ を書く
  • " はそのまま書ける。 """ を書くときだけ \""" とする
  • 変数の埋め込みは formatted() が手軽。文章中の % は %% と書く。SQL の値は ? で渡す
  • 改行はソースの改行コードにかかわらず常に LF。 CRLF が必要なときは replace("\n", "\r\n") で変換する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaテキストブロックの使い方|複数行文字列とインデントの規則」

株式会社RJCでは、Javaのテキストブロックのような言語機能の細かな規則を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?