テキストブロックと新しいString API(Java 15以降)

1. 総概要

Java 15以前、JavaコードでJSONやSQLまたはHTMLの複数行の段落を書くのは大変でした。文字列を+で連結したり、\nを追加したり、すべての"をエスケープする必要がありました。テキストブロック(Java 15から正式に、JEP 378)がこの問題を解決します。Stringの新しいメソッドが一緒に来ています。

この記事を読み終わったら、テキストブロックを正しいインデントで書き、\と\sのエスケープを使うことができるようになります。また、formatted()、stripIndent()、translateEscapes()、およびJava 21までの追加APIをいつ使用するかを知ることができます。

2. 準備

すべての例はJava 21で実行されます。テスト部分ではJUnit 5とAssertJを使用しています。

<properties>
    <maven.compiler.release>21</maven.compiler.release>
</properties>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.25.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Spring Boot 3.xを使用している場合、spring-boot-starter-testにはこれら両方のライブラリがすでに含まれています。

3. テキストブロックの基本

3.1. 構文

テキストブロックは3つの引用符"""で開き、すぐに改行します。それは"""で終わります。

public class TextBlockDemo {
    public static void main(String[] args) {
        String json = """
                {
                  "id": 42,
                  "customer": "Lan",
                  "status": "PAID"
                }
                """;
        System.out.print(json);
    }
}

結果:

{
  "id": 42,
  "customer": "Lan",
  "status": "PAID"
}

このコードにはいくつか注目すべき点があります:

  • 内部の"はエスケープする必要がありません。
  • 各行の開始時の共通のインデントが削除されます。これは偶発的な空白と呼ばれます。"id"の前の2つのスペースのような相対インデントは保持されます。
  • 終了"""が独立した行にあるため、文字列は\nで終わります。

テキストブロックは通常のStringです。コンパイル時に完全に処理されるため、実行時に追加のコストはかかりません。

注意: 開き"""の直後にコンテンツを書くことはできません。行String s = """Hello""";はコンパイルエラーになります:非合法なテキストブロック開き区切り文字列。

3.2. 終了"""の位置がインデントを決定

コンパイラは、終了"""を含む行を含めて、最も少ない先頭の空白を持つ行を見つけます。その後、その正確な数のスペースをすべての行から削除します。したがって、終了"""を左に移動してインデントを能動的に追加できます。

String html = """
            <ul>
              <li>Order #1</li>
            </ul>
        """;
System.out.print(html);
    <ul>
      <li>Order #1</li>
    </ul>

終了"""はコンテンツより4列少ないインデントなので、各行の先頭に4つのスペースが保持されます。

一方、文字列の末尾に\n文字が必要ない場合は、コンテンツのすぐ後に"""を配置します:

String greeting = """
        こんにちは Lan""";
System.out.println("[" + greeting + "]");
[こんにちは Lan]

3.3. 行末の空白と改行文字

知っておくべき2つの追加ルールがあります:

  1. 各行の末尾の空白は常に削除されます。
  2. すべての改行タイプ(\r\n、\r)は\nに正規化されます。これは、ソースファイルがWindowsの行末で保存されている場合でも当てはまります。

ルール2はオペレーティングシステム間で結果を安定させるのに役立ちます。ただし、プロトコルで\r\nの使用が必須である場合、例えばHTTPヘッダーまたはRFC 4180に従うCSVファイルの場合は、改行文字を自分で置き換える必要があります。

4. 2つの新しいエスケープシーケンス:\<改行>と\s

4.1. \で行を結合

長いSQL文は通常、読みやすくするために複数の行に書かれます。しかし、ログを記録する場合など、単一行に配置する必要がある場合があります。行末の\は改行文字をブロックします:

String sql = """
        SELECT id, total \
        FROM orders \
        WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'

\の前のスペースは保持されます。理由は、行末にはもう位置していないからです:\文字が後に続きます。

4.2. \sで空白を保持

\sは1つのスペース(U+0020)と同等です。通常は削除される行末の空白を保持するために使用します。例えば、固定幅のデータを作成する場合:

String report = """
        Lan    \s
        Minh   \s
        """;
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan     ]
[Minh    ]

各行の長さは正確に8文字です。\sの前の4つのスペースは行末にはもうないため保持されます。

4.3. コンテンツに"""がある場合

連続した2つの"は問題ありません。連続した3つはテキストブロック終了文字と解釈されるため、少なくとも1つをエスケープする必要があります:

String doc = """
        Pythonのdocstringは \""" で開きます。
        """;

5. 付随する新しいString API

Java 15は、テキストブロックをサポートするために3つのメソッドをStringに追加しました。これらはコンパイラがテキストブロックを処理する際に実行するステップでもあります。

5.1. formatted()

formatted(Object... args)はString.format(this, args)と同じように動作しますが、文字列に対して直接呼び出すことができます。この書き方はテキストブロックに特に適しています:

record Order(long id, String customer, double total) {}

Order order = new Order(42, "Lan", 199_000);

String message = """
        注文#%d
        顧客:%s
        合計:%,.0f VND
        """.formatted(order.id(), order.customer(), order.total());

System.out.print(message);
注文#42
顧客:Lan
合計:199,000 VND

千の区切り文字はJVMのデフォルトLocaleに依存します。固定結果が必要な場合は、formatted()はLocaleパラメータを受け入れないため、String.format(Locale.US, ...)を使用してください。

5.2. stripIndent()

このメソッドは、テキストブロックの偶発的な空白を削除する正確なアルゴリズムを任意の文字列に適用します。複数行の文字列がソースコード内ではなくファイルまたはデータベースから来る場合に便利です:

String raw = "    SELECT *\n      FROM customers\n    ";
System.out.print(raw.stripIndent());
SELECT *
  FROM customers

最後の行には空白しか含まれないため、インデント決定に含まれ、その後空の文字列になります。これはテキストブロック内の終了"""の役割です。

5.3. translateEscapes()

このメソッドは、リテラル形式のエスケープシーケンスを実際の文字に変換します。設定ファイルから\nまたは\t文字列を含むテンプレートを読み取るときに必要になります:

String fromConfig = "こんにちは\\tLan\\n注文いただきありがとうございます";
System.out.println(fromConfig.translateEscapes());
こんにちは	Lan
注文いただきありがとうございます

\qなど構文的に正しくないエスケープはメソッドにIllegalArgumentExceptionをスローさせます。例外をキャッチせずにユーザー入力データで呼び出さないでください。注意:このメソッドは\uを処理しません。ユニコードエスケープはコンパイラによって既に翻訳されているからです。

6. その他の便利なメソッド(Java 11–21)

多くの小さなメソッドがバージョン全体で追加されています。テキストブロックと一緒に使用されることが多いです:

String text = """
        Lan
           \s
        Minh
        """;

// Java 11
System.out.println("  ".isBlank());                      // true
System.out.println(text.lines().filter(l -> !l.isBlank()).count()); // 2
System.out.println("\u2003Lan\u2003".strip());           // "Lan" (stripはUnicodeを理解、trimはしない)
System.out.println("-".repeat(10));                      // ----------

// Java 12
System.out.print("Lan\nMinh".indent(2));                 // 各行に2つのスペースを追加、改行で終了
int length = "Lan".transform(String::length);            // 3

// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8、[6, 13)内でのみ検索
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]

明確にする必要があるいくつかの詳細があります:

  • indent(n)は、n > 0の場合は各行の先頭にn個のスペースを追加し、n < 0の場合は最大n個の空白を削除します。常に改行を正規化し、最後に\nを追加します。
  • transform(Function)は、文字列変換ステップを自然に連鎖させることができます。例えば、template.transform(this::render).transform(String::strip)のように。
  • Java 21以降、StringBuilderにもrepeat(...)があります:new StringBuilder().repeat("=", 5)は=====を出力します。

7. Spring BootでのテキストブロックのUsage

Spring Boot 3.xアプリケーションでは、テキストブロックはSpring Data JPAの@Queryに非常に適しています。文字列を連結することなく複数行のクエリを保つことができます:

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("""
            SELECT o FROM Order o
            WHERE o.customer.email = :email
              AND o.status = 'PAID'
            ORDER BY o.createdAt DESC
            """)
    List<Order> findPaidOrders(@Param("email") String email);
}

テキストブロックはコンパイル時定数なので、アノテーション値として使用できます。パラメータは:emailを介してバインドされたままになり、SQLインジェクションから安全です。

8. テキストブロック動作のテストを書く

インデントと改行文字に関するルールは混乱を招きやすいです。したがって、期待される動作を確認するテストを書くべきです:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class TextBlockTest {

    @Test
    void givenClosingDelimiterOnOwnLine_thenEndsWithNewline() {
        String json = """
                {
                  "id": 42
                }
                """;

        assertThat(json).isEqualTo("{\n  \"id\": 42\n}\n");
    }

    @Test
    void givenBackslashAtLineEnd_thenLinesAreJoined() {
        String sql = """
                SELECT id \
                FROM orders""";

        assertThat(sql).isEqualTo("SELECT id FROM orders");
    }

    @Test
    void givenSpaceEscape_thenTrailingSpacesArePreserved() {
        String padded = """
                Lan  \s""";

        assertThat(padded).hasSize(6).endsWith("   ");
    }

    @Test
    void givenTemplate_whenFormatted_thenPlaceholdersAreReplaced() {
        String message = """
                こんにちは %s、注文 #%d が支払われました。""".formatted("Lan", 42);

        assertThat(message).isEqualTo("こんにちは Lan、注文 #42 が支払われました。");
    }
}

4つのテストはすべてパスします。最初のテストは最も重要です:終了"""が独立した行にあると、文字列の末尾に\nが作成されることを思い出させます。

9. よくあるエラーと使用すべきでない場合

タブとスペースの混在。 偶発的な空白を計算するとき、コンパイラは1つのタブを1つの空白文字と見なします。行が2つの異なるタイプのインデントを使用している場合、結果は、IDEでは配置が同じに見えても、ずれます。エディタを設定してスペースのみを使用してください。

末尾の\nを忘れる。 文字列を比較するか、テキストブロックを別の文字列に連結するときに、余分な\n文字は「同じに見えるが、equalsがfalseになる」というエラーを引き起こすことがよくあります。不要な場合は、コンテンツのすぐ後に"""を配置するか、strip()を呼び出してください。

formatted()を使用してSQLをユーザーデータと連結する。 テキストブロックはコードをより読みやすくするだけで、より安全にはしません。

// 誤り:SQLインジェクションの脆弱性
String sql = """
        SELECT * FROM customers WHERE email = '%s'
        """.formatted(email);

PreparedStatement、?パラメータ付きJdbcTemplate、または名前付きパラメータ付き@Queryを使用してください。

注意: JSONの場合、テキストブロックはテストデータまたは固定ペイロードに適しています。ペイロードが動的データに依存する場合は、Jacksonを使用してオブジェクトをシリアル化してください。文字列の手動連結は、顧客名の"や\などの特殊文字をエスケープしません。

短い1行の文字列。 "Lan"は3行のテキストブロックよりもまだ良いです。コンテンツが本当に複数行であるか、多くの"を含む場合にのみテキストブロックを使用してください。

テンプレートが大きすぎます。 Javaクラス内に数百行のHTMLメールテンプレートがあると、コードは保守が難しくなります。この場合、Thymeleafやファイルリソースを使用するなど、テンプレートファイルに移動する必要があります。src/main/resources。

10. 結論

テキストブロックは、JavaでJSON、SQL、HTMLを元の形式のようにコンパクトで読みやすく書くのに役立ちます。いくつかのルールを覚えておくだけです:

  • 開き"""は直ちに改行する必要があります。
  • 終了"""の位置がインデントと文字列が末尾に\nを持つかどうかを決定します。
  • 行末の空白が削除されます。改行は常に\nに正規化されます。
  • \を使用して行を結合し、\sを使用して空白を保持します。

APIに関しては、formatted()はテキストブロックに値を入力するのに非常に適しています。stripIndent()とtranslateEscapes()は、外部から読み取る文字列に同じロジックを適用できます。Java 21のlines()、strip()、indent()、transform()、splitWithDelimiters()などのメソッドは、外部ライブラリなしで複数行の文字列を処理するのに役立ちます。最後に、期待される動作を確認するテストを書き、ユーザーデータでSQLを自分で連結するためにテキストブロックを使用しないでください。

javatext-blockstringjava-21