13 テキスト・ブロック

JEP 378により、テキスト・ブロックという言語機能がJava SE 15以降に追加されました。JEPではこの機能について詳細に説明されていますが、この機能をどのように使用できるか、あるいは使用すべきかは必ずしも明確ではありません。このガイドでは、テキスト・ブロックの実際の使用方法に関するアドバイス、およびいくつかのスタイル・ガイドラインをまとめています。

概要

テキストブロックの主な役割は、複数の行にまたがる文字列をレンダリングするために必要なJava構文を最小限に抑えることによって、わかりやすくすることです。

JDKの以前のリリースでは、複数行のコード・スニペットを埋め込むには、明示的な行終端子、文字列連結およびデリミタを複雑に組み合せる必要がありました。テキスト・ブロックは、これらの障害のほとんどを取り除き、コード・スニペットおよびテキスト・シーケンスをほぼそのまま埋め込むことを可能にします。

テキスト・ブロックは、従来の二重引用符付き文字列リテラルを使用できる任意の場所で使用できるJava文字列表現の代替形式です。たとえば:

// Using a literal string
String dqName = "Pat Q. Smith";

// Using a text block
String tbName = """
                Pat Q. Smith""";

テキスト・ブロックから生成されるオブジェクトは、従来の二重引用符付き文字列と同じ特性を持つjava.lang.Stringです。これには、オブジェクト表現とインターニングが含まれます。前述の例のdqNameおよびtbNameを引き続き使用します:

// Both dqName and tbName are strings of equal value
dqName.equals(tbName)    // true

// Both dqName and tbName intern to the same string
dqName == tbName         // true

テキスト・ブロックは、文字列リテラルを使用できる任意の場所で使用できます。たとえば、テキスト・ブロックを文字列連結式で文字列リテラルと混在させることができます:

String str = "The old";
String tb = """
            the new""";
String together = str + " and " + tb + ".";

テキスト・ブロックはメソッド引数として使用できます:

System.out.println("""
    This is the first line
    This is the second line
    This is the third line
    """);

文字列メソッドは、テキスト・ブロックに適用できます:

"""
John Q. Smith""".substring(8).equals("Smith")    // true

文字列リテラルのかわりにテキスト・ブロックを使用すると、コードの可読性と明瞭さを高めることができます。これは主に、文字列リテラルを使用して複数行文字列を表す場合に発生します。この場合、引用符、改行エスケープ文字、連結演算子によってコードがかなり煩雑になります:

// ORIGINAL
String message = "'The time has come,' the Walrus said,\n" +
                 "'To talk of many things:\n" +
                 "Of shoes -- and ships -- and sealing-wax --\n" +
                 "Of cabbages -- and kings --\n" +
                 "And why the sea is boiling hot --\n" +
                 "And whether pigs have wings.'\n";

テキスト・ブロックを使用することで、煩雑さを大幅に解消できます:

// BETTER
String message = """
    'The time has come,' the Walrus said,
    'To talk of many things:
    Of shoes -- and ships -- and sealing-wax --
    Of cabbages -- and kings --
    And why the sea is boiling hot --
    And whether pigs have wings.'
    """;

テキスト・ブロックの構文

テキスト・ブロックは、3つの二重引用符文字で始まり、その後に行終端子が続きます。テキスト・ブロックを1行に置くことはできません。また、テキスト・ブロックの内容は、3つの開始二重引用符の後に、行終端子を挟まずに続けることもできません。この理由は、テキスト・ブロックは主に複数行文字列をサポートするように設計されており、先頭の行終端子を必須とすることで、インデント処理ルールが簡略化されるためです。「付随的な空白」を参照してください。

// ERROR
String name = """Pat Q. Smith""";

// ERROR
String name = """red
                 green
                 blue
                 """;

// OK
String name = """
    red
    green
    blue
    """;

この最後の例は、次の文字列リテラルと同じです:

String name = "red\n" +
              "green\n" +
              "blue\n";

テキスト・ブロック内のJavaコードのスニペットの例を次に示します:

String source = """
    String message = "Hello, World!";
    System.out.println(message);
    """;

埋め込まれた二重引用符をエスケープする必要はありません。同等の文字列リテラルは次のようになります:

String source = "String message = \"Hello, World!\";\n" +
                "System.out.println(message);\n";

最後の改行

この例は、"red\ngreen\nblue\n"と同等です:

String name = """
    red
    green
    blue
    """;

最後の\nのない複数行の文字列を表す場合はどうすればよいでしょうか。

String name = """
    red
    green
    blue""";

このテキスト・ブロックは、"red\ngreen\nblue"と同等です。つまり、最後の表示行に終了デリミタを配置することで、最後の\nが事実上削除されます。

または、\<line-terminator>エスケープ・シーケンスを使用して、暗黙的な改行文字の挿入を抑制することもできます:

String name = """
    red
    green
    blue\
    """;

「\<line-terminator>および\sエスケープ・シーケンス」を参照してください。

付随的な空白

テキスト・ブロックは、周囲のコードのインデントに合せてインデントするのが理想的です。たとえば:

void writeHTML() {
    String html = """
        <html>
            <body>
                <p>Hello World.</p>
            </body>
        </html>
        """;
    writeOutput(html);
}

しかし、ここでインデントに使用されるスペースが文字列の内容にどのように影響するかという疑問が生じます。単純な解釈では、これらの空白文字すべてがテキスト・ブロックに含まれることになります。その結果、コードを再度インデントすると、テキスト・ブロックの内容に影響します。これはエラーとなる可能性が高いです。

この問題を回避するために、テキスト・ブロックは、付随的な空白と必須の空白を区別します。Javaコンパイラは、付随的な空白を自動的に削除します。<html>および</html>の左側のインデントは、これらの行のインデントが最も小さいため、付随的なものとみなされます。したがって、これらがテキスト・ブロック内のテキストの左マージンを事実上決定します。ただし、<html>に対する<body>のインデントは、付随的な空白とはみなされません。この相対的なインデントは、文字列の内容の一部となることを意図しているものと考えられます。

次の例では、中点(·)を使用して付随的な空白を視覚化し、必須の空白は実際の空白として表示しています。

void writeHTML() {
    String html = """
········<html>
········    <body>
········        <p>Hello World.</p>
········    </body>
········</html>
········""";
    writeOutput(html);
}

付随的な空白を削除すると、テキスト・ブロックの内容は次のようになります:

<html>
    <body>
        <p>Hello World.</p>
    </body>
</html>

付随的な空白を決定するアルゴリズムについては、JEP 378で詳しく説明されています。しかし、最終的な効果は非常にシンプルです。先頭の空白が最も小さい行の先頭に空白がなくなるまで、テキスト・ブロックの内容全体が左にシフトされます。

空白を保持し、付随的な空白とみなされないようにするには、終了デリミタである3つの引用符を、周囲のコードに適したインデント位置に維持したまま、テキスト・ブロックの内容行を右にシフトします。たとえば:

void writeHTML() {
    String html = """
········    <html>
········        <body>
········            <p>Hello World.</p>
········        </body>
········    </html>
········""";
    writeOutput(html);
}

結果は次のようになります:

    <html>
        <body>
            <p>Hello World.</p>
        </body>
    </html>

テキスト・ブロックは、ソース行の最初の文字位置に終了デリミタを配置することで、付随的な空白の削除を無効にできます:

void writeHTML() {
    String html = """
                  <html>
                      <body>
                          <p>Hello World.</p>
                      </body>
                  </html>
""";
    writeOutput(html);
}

その結果、付随的な空白が削除されることはなく、各行に先頭の空白が文字列に含まれます:

                  <html>
                      <body>
                          <p>Hello World.</p>
                      </body>
                  </html>

保持するインデント量を制御するこの手法は、テキスト・ブロックの最後の行が行終端子で終わる場合にのみ機能します。最後の行が行終端子で終わらない場合は、String::indentを使用してインデントを明示的に制御する必要があります。次に例を示します。

String colors = """
    red
    green
    blue""";

すべてのインデントは付随的とみなされ、取り除かれます:

red
green
blue

文字列の内容にインデントを含めるには、テキスト・ブロックでindentメソッドを呼び出します:

String colors = """
    red
    green
    blue""".indent(4);

この結果、次のようになります:

    red
    green
    blue

末尾の空白

テキスト・ブロックの各行の末尾の空白も付随的とみなされ、Javaコンパイラによって削除されます。これは、テキスト・ブロックの内容を常に視覚的に識別できるようにするためです。これを行わない場合、末尾の空白を自動的に除去するテキスト・エディタによって、テキスト・ブロックの内容が知らないうちに変更される可能性があります。

テキスト・ブロックに末尾の空白を含める必要がある場合は、次のいずれかの方法を使用できます:

// character substitution
String r = """
    trailing$$$
    white space
    """.replace('$', ' ');


// character fence
String s = """
    trailing   |
    white space|
    """.replace("|\n", "\n");


// octal escape sequence for space
String t = """
    trailing\040\040\040
    white space
    """;

// \s escape sequence
String t = \s\s\s
    trailing\040\040\040
    white space
    """;

ノート:

Unicodeエスケープは、字句解析の前にソース・ファイルの読取り中に早期に変換されるため、\u0020は使用できません。対照的に、\040などの文字および文字列のエスケープは、字句解析によってソース・ファイルがトークンに分割され、文字列リテラルおよびテキスト・ブロックが識別された後に処理されます。

空白に関する潜在的な問題の検出

前述の例では、すべてのインデントは空白文字で構成されていました。しかし、タブ(\t)が使用される場合もあります。残念ながら、Javaコンパイラでは、異なるエディタでのタブ文字がどのように表示されるかを把握することはできません。そのため、個々の空白文字はすべて等しく扱われるというルールになっています。特定のシステム上で表示された際に、1つのタブ文字が最大8文字分の空白に相当する場合がありますが、1つのスペース文字は1つのタブ文字と同様に扱われます。

したがって、空白文字を混在させると、一貫性のない、意図しない結果が生じる可能性があります。次の例を考えてみましょう。ここでは、一部の行はスペースでインデントされ、一部の行はタブでインデントされています。タブは水平タブ記号()で表されています:

    String colors = """
····················red
␉   ␉   ␉   ␉   ␉   green
····················blue""";

この場合、2行目には空白文字が5つしかなく、その他の行には20文字あるため、付随的なインデントの削除は不均一になります。結果は次のようになります:

               red
green
               blue

Javaコンパイラのlintフラグ-Xlint:text-blocksを使用してテキスト・ブロックのlint検出を有効にすることで、付随的な空白に関連する問題を検出できます。lintの検出がオンの場合、この例では「一貫性のない空白のインデント」という警告が生成されます。

このlintフラグは、「末尾の空白は削除されます」という別の警告も有効にします。この警告は、テキスト・ブロック内の任意の行に末尾の空白がある場合に出力されます。末尾の空白を保持する必要がある場合は、「末尾の空白」の項で説明されているエスケープまたは置換方法のいずれかを使用します。

行終端子の正規化

複数行文字列リテラルの複雑さの1つは、ソース・ファイルで使用される行終端子(\n\rまたは\r\n)がプラットフォームによって異なることです。異なるプラットフォーム上のエディタで、行終端子が知らなううちに変更される可能性があります。または、ソース・ファイルが異なるプラットフォームで編集される場合、テキスト・ブロックに異なる行終端子が混在する可能性があります。これにより、混乱を招き、一貫性のない結果が生じる可能性があります。

これらの問題を回避するために、Javaコンパイラは、ソース・ファイルに実際に含まれている行終端子に関係なく、テキスト・ブロック内のすべての行終端子を\nに正規化します。次のテキスト・ブロック(ライン・フィード記号()とキャリッジ・リターン記号()は、それぞれ\n\rを表します)は:

String colors = """
    red␊
    green␍
    blue␍␊
    """;

次の文字列リテラルと同等です:

String colors = "red\ngreen\nblue\n";

プラットフォームの行終端子が必要な場合は、String::replaceAll("\n", System.lineSeparator())を使用できます。

エスケープ・シーケンスの変換

文字列リテラルと同様に、テキスト・ブロックは、エスケープ・シーケンス(\b\f\n\t\r\s\"\'\\\<line-terminator>および8進エスケープ)を認識します。文字列リテラルとは異なり、エスケープ・シーケンスは多くの場合必要ありません。ほとんどの場合、エスケープ・シーケンスのかわりに実際の文字(\n\t\"および\')を使用できます。次のテキスト・ブロック(およびは、\tおよび\nを表します)は:

String s = """
    Color␉   Shape␊
    Red␉ ␉   Circle␊
    Green␉   Square␊
    Blue␉␉   Triangle␊
    """;

次のようになります:

Color␉  Shape␊
Red␉ ␉  Circle␊
Green␉  Square␊
Blue␉␉  Triangle␊

3つ以上の二重引用符が連続して出現する場合は、エスケープが必要です。

String code = """
    String source = \"""
        String message = "Hello, World!";
        System.out.println(message);
        \""";
    """;

エスケープ変換は、Javaコンパイラによる処理の最後のステップとして行われるため、明示的なエスケープ・シーケンスを使用して、行終端子の正規化および空白の削除ステップを回避できます。たとえば:

String s = """
           red  \040
           green\040
           blue \040
           """;

\040は、末尾の空白が削除するまでスペースに変換されないため、すべての行の長さが等しくなります。中点(·)を使用して末尾のスペースを表します。結果は次のとおりです:

red···
green·
blue··

ノート:

前述のとおり、Unicodeエスケープ・シーケンス\u0020は、\040のかわりには使用できません

\<line-terminator>および\sエスケープ・シーケンス

\<line-terminator>エスケープ・シーケンスは、暗黙的な改行文字の挿入を明示的に抑制します。

たとえば、非常に長い文字列リテラルを小さい部分文字列の連結に分割してから、結果の文字列式を複数の行にハード・ラップすることはよく行われます。

  String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
                   "elit, sed do eiusmod tempor incididunt ut labore " +
                   "et dolore magna aliqua.";

\<line-terminator>エスケープ・シーケンスを使用すると、これは次のように表すことができます:

  String text = """
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """;

\sエスケープ・シーケンスは、単にスペース(\040、ASCII文字32、空白)に変換されます。エスケープ・シーケンスは、付随的な空白の削除後まで変換されないため、\sは、末尾の空白が削除されないようにするフェンスとして機能できます。次の例のように、各行の最後に\sを使用すると、各行の長さが正確に6文字であることが保証されます:

String colors = """
    red  \s
    green\s
    blue \s
    """;

テキスト・ブロックのスタイル・ガイドライン

G1. コードの可読性を高める場合、特に複数行の文字列では、テキスト・ブロックを使用してください。

// ORIGINAL
String message = "'The time has come,' the Walrus said,\n" +
                 "'To talk of many things:\n" +
                 "Of shoes -- and ships -- and sealing-wax --\n" +
                 "Of cabbages -- and kings --\n" +
                 "And why the sea is boiling hot --\n" +
                 "And whether pigs have wings.'\n";

// BETTER
String message = """
    'The time has come,' the Walrus said,
    'To talk of many things:
    Of shoes -- and ships -- and sealing-wax --
    Of cabbages -- and kings --
    And why the sea is boiling hot --
    And whether pigs have wings.'
    """;

G2. 文字列が、連結や改行のエスケープ処理を行わなくても1行に収まる場合は、引き続き文字列リテラルを使用してください。

// ORIGINAL - is a text block helpful here?
String name = """
              Pat Q. Smith""";

// BETTER - a string literal works fine
String name = "Pat Q. Smith";

G3. 可読性が保たれる場合は、埋込みエスケープ・シーケンスを使用します。

var data = """
    Name | Address | City
    Bob Smith | 123 Anytown St\nApt 100 | Vancouver
    Jon Brown | 1000 Golden Place\nSuite 5 | Santa Ana
    """;

G4. ほとんどの複数行文字列では、開始デリミタを前の行の右端に配置し、終了デリミタをテキスト・ブロックの左端に、独立した行で配置します。

String string = """
    red
    green
    blue
    """;

G5. 開始デリミタと終了デリミタ、およびテキスト・ブロックの左余白を揃えないようにしてください。変数名または修飾子が変更された場合は、テキスト・ブロックのインデントを変更する必要があります。

// ORIGINAL
String string = """
                red
                green
                blue
                """;

// ORIGINAL - after variable declaration changes
static String rgbNames = """
                         red
                         green
                         blue
                         """;

// BETTER
String string = """
    red
    green
    blue
    """;

// BETTER - after variable declaration changes
static String rgbNames = """
    red
    green
    blue
    """;

G6. 複雑な式内のインライン・テキスト・ブロックは、読みやすさを損なう可能性があるため、避けてください。ローカル変数または静的finalフィールドにリファクタリングすることを検討してください。

// ORIGINAL
String poem = new String(Files.readAllBytes(Paths.get("jabberwocky.txt")));
String middleVerses = Pattern.compile("\\n\\n")
                             .splitAsStream(poem)
                             .match(verse -> !"""
                                   ’Twas brillig, and the slithy toves
                                   Did gyre and gimble in the wabe;
                                   All mimsy were the borogoves,
                                   And the mome raths outgrabe.
                                   """.equals(verse))
                             .collect(Collectors.joining("\n\n"));

// BETTER
String firstLastVerse = """
    ’Twas brillig, and the slithy toves
    Did gyre and gimble in the wabe;
    All mimsy were the borogoves,
    And the mome raths outgrabe.
    """;
String poem = new String(Files.readAllBytes(Paths.get("jabberwocky.txt")));
String middleVerses = Pattern.compile("\\n\\n")
                             .splitAsStream(poem)
                             .match(verse -> !firstLastVerse.equals(verse))
                             .collect(Collectors.joining("\n\n"));

G7. テキスト・ブロックのインデントには、空白のみを使用するか、タブのみを使用します。空白を混在させると、インデントが不揃いになります。

// ORIGINAL
    String colors = """
········red
␉       green
········blue""";    // result: "·······red\ngreen\n·······blue"

// PROBABLY WHAT WAS INTENDED
    String colors = """
········red
········green
········blue""";    // result: "red\ngreen\nblue"

G8. テキスト・ブロックに3つ以上の二重引用符が連続して含まれている場合は、3つの二重引用符の連続ごとに最初の二重引用符をエスケープします。

// ORIGINAL
String code = """
    String source = \"\"\"
        String message = "Hello, World!";
        System.out.println(message);
        \"\"\";
    """;

// BETTER
String code = """
    String source = \"""
        String message = "Hello, World!";
        System.out.println(message);
        \""";
    """;

G9. ほとんどのテキスト・ブロックは、隣接するJavaコードに合せてインデントする必要があります。

    // ORIGINAL - odd indentation
    void printPoem() {
        String poem = """
’Twas brillig, and the slithy toves
Did gyre and gimble in the wabe;
All mimsy were the borogoves,
And the mome raths outgrabe.
""";
        System.out.print(poem);
    }

    // BETTER
    void printPoem() {
        String poem = """
            ’Twas brillig, and the slithy toves
            Did gyre and gimble in the wabe;
            All mimsy were the borogoves,
            And the mome raths outgrabe.
            """;
        System.out.print(poem);
    }

G10. 水平スクロールまたは行の折り返しを回避するために、幅の広い文字列は完全に左揃えにすることをお薦めします。

// ORIGINAL

class Outer {
    class Inner {
        void printPoetry() {
            String lilacs = """
                Over the breast of the spring, the land, amid cities,
                Amid lanes and through old woods, where lately the violets peep’d from the ground, spotting the gray debris,
                Amid the grass in the fields each side of the lanes, passing the endless grass,
                Passing the yellow-spear’d wheat, every grain from its shroud in the dark-brown fields uprisen,
                Passing the apple-tree blows of white and pink in the orchards,
                Carrying a corpse to where it shall rest in the grave,
                Night and day journeys a coffin.
                """;
            System.out.println(lilacs);
        }
    }
}

// BETTER

class Outer {
    class Inner {
        void printPoetry() {
            String lilacs = """
Over the breast of the spring, the land, amid cities,
Amid lanes and through old woods, where lately the violets peep’d from the ground, spotting the gray debris,
Amid the grass in the fields each side of the lanes, passing the endless grass,
Passing the yellow-spear’d wheat, every grain from its shroud in the dark-brown fields uprisen,
Passing the apple-tree blows of white and pink in the orchards,
Carrying a corpse to where it shall rest in the grave,
Night and day journeys a coffin.
""";
            System.out.println(lilacs);
        }
    }
}

G11. 同様に、行数が多いために終了デリミタが縦方向に画面から外れる可能性がある場合も、テキスト・ブロックを完全に左揃えにすることは理にかなっています。これにより、終了デリミタが画面から外れた場合でも、読者は左余白を使用してインデントを追跡できます。

// ORIGINAL

String validWords = """
                    aa
                    aah
                    aahed
                    aahing
                    aahs
                    aal
                    aalii
                    aaliis
...
                    zythum
                    zythums
                    zyzzyva
                    zyzzyvas
                    zzz
                    zzzs
                    """;


// BETTER

String validWords = """
aa
aah
aahed
aahing
aahs
aal
aalii
aaliis
...
zythum
zythums
zyzzyva
zyzzyvas
zzz
zzzs
""";

G12. テキスト・ブロックの最後の改行を除外する必要がある場合は、\<line-terminator>エスケープ・シーケンスを使用してください。これにより、テキスト・ブロックの枠が適切になり、終了デリミタでインデントを管理できます。

// ORIGINAL

String name = """
    red
    green
    blue""".indent(4);


// BETTER

String name = """
        red
        green
        blue\
    """;

テキスト・ブロックに関連する文字列メソッド

テキストブ・ロック機能の一部として、Stringクラスにいくつかの新しいメソッドが含まれています。

  • String formatted(Object... args): このメソッドは、String.format(this, args)と同等です。利点は、インスタンス・メソッドであるため、テキスト・ブロックの末尾から連鎖できることです:

    String output = """
        Name: %s
        Phone: %s
        Address: %s
        Salary: $%.2f
        """.formatted(name, phone, address, salary);
  • String stripIndent(): stripIndentメソッドは、Javaコンパイラで使用されるものと同じアルゴリズムを使用して、複数行の文字列から付随的な空白を削除します。これは、テキストを入力データとして読み取るプログラムがあり、テキスト・ブロックの場合と同じ方法でインデントを削除する場合に便利です。

  • String translateEscapes(): translateEscapesメソッドは、エスケープ・シーケンス(\b\f\n\t\r\s\"\'\\\<line-terminator>および8進エスケープ)の変換を実行し、Javaコンパイラがテキスト・ブロックおよび文字列リテラルを処理するために使用されます。これは、テキストを入力データとして読み取るプログラムがあり、エスケープ・シーケンス処理を実行する場合に便利です。

    ノート:

    Unicodeエスケープ(\uNNNN)は処理されません

参照情報

「The Walrus and the Carpenter」
Lewis Carroll、Through the Looking-Glass and What Alice Found There、1872年。
「Jabberwocky」
Lewis Carroll、Mischmasch、1855年。
「When Lilacs Last in the Dooryard Bloom'd」
Walt Whitman、Sequel to Drum-Taps、1865年。