Programmer’s Guide To Text Blocks

原文はこちら (written by Jim Laskey and Stuart Marks on August 6, 2019) 。
http://cr.openjdk.java.net/~jlaskey/Strings/TextBlocksGuide_v9.html

JEP 355により、Java SE 13にPreview機能としてテキストブロックが導入されました。JEPで機能の詳細の説明がありますが、この機能の利用方法、利用すべきものが明確とはいえません。そのため、このガイドではスタイルガイドラインとともに、実用的なテキストブロックの利用方法をご紹介します。

JEP 355: Text Blocks (Preview)
http://openjdk.java.net/jeps/355

Introduction

テキストブロックの原則は、複数の行にまたがる文字列をレンダリングするために必要なJava構文を最小限に抑えることにより、明確さを提供することです。

以前のリリースでは、複数行のコードスニペットを埋め込むには、明示的な行末記号、文字列の連結、および区切り文字という、やっかいで面倒な設定が必要でした。テキストブロックはこれらの障害のほとんどを排除し、コードスニペットとテキストシーケンスをほぼそのまま埋め込むことができます。

テキストブロックは、従来の二重引用符で囲まれた文字列リテラルを使用できる場所であればどこでも使用できるJava文字列表現の代替形式です。 例えば以下のような感じです。

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

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

テキストブロックから生成されるオブジェクトはこれまでのダブルクォートで囲んだ文字列と同じ性質を持つjava.lang.Stringです。これにはオブジェクト表現とinterning(正準表現)が含まれます。上記の例で使った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
    """);

Stringメソッドをテキストブロックに適用できます。

"""
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.'
    """;

Using Text Blocks

Preview Feature

テキストブロックは現時点でJava LanguageのPreview機能です。

JEP 12: Preview Language and VM Features
http://openjdk.java.net/jeps/12

Javaコードでテキストブロックを利用するためには、javacコマンドに–enable-previewと-source 13というフラグを付けてコンパイルし、実行時にはjavaコマンドラインに–enable-previewフラグを付ける必要があります。

javac --enable-preview -source 13 -d classes TextBlockExample.java
java --enable-preview -cp classes com.example.TextBlockExample

JShellを使ってテキストブロックを試す場合にも、–enable-previewフラグが必要です。

jshell --enable-preview

mainプログラムを持つ1個のソースファイルの場合であれば、–enable-preview フラグと–source フラグ(いずれもハイフンが2個必要です)をjavaコマンドとともに使って、コンパイル、実行できます。

java --enable-preview --source 13 TextBlockExample.java

(シングルソースコードプログラムについてはJEP 330に記載があります)

JEP 330: Launch Single-File Source-Code Programs
https://openjdk.java.net/jeps/330

Text Block Syntax

テキストブロックは3個のダブルクォーテーション文字で始まり、行末文字が続きます。テキストブロックは1行で記載できず、3個のダブルクォーテーション文字の後に直接コンテンツを配置することもできません(必ず行末文字が必要です)。この理由はテキストブロックは主として複数行の文字列をサポートするために設計されたもので、最初の行末文字が必要なのは、インデント処理ルールをシンプルにするためです(Incidental White Spaceの章をご覧ください)。

// 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";

That Final New Line

上記の例に注目してください。

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

この例は”red\ngreen\nblue\n” と同等です。最後の改行文字 \n を使わずに複数行の文字列を表現したい場合、どうすればよいでしょうか?

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

このテキストブロックは”red\ngreen\nblue”と同等です。従って、最後の行に終了区切り文字を配置すると、最後の \n が削除されます。

Incidental White Space

テキストブロックを周囲のコードのインデントに一致するようにインデントするのが理想的です。以下はその例です。

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

しかしながら、これはインデントのための空白が文字列の内容にどのように影響するのか、といった疑問が生じます。単純に解釈すると、テキストブロックのあらゆる空白を含むことになるでしょう。その結果、コードを再度インデントすると、テキストブロックの内容に影響が及びます。

この問題を回避するため、テキストブロックは必須の空白と付随する空白を区別します。Javaコンパイラは自動的に付随する空白を取り除きます。<html>と</html>の左側にあるインデントは、これらの行のインデントがもっとも少ないため、付随するものと見なします。これにより、効率的にテキストブロックのテキスト左余白を判断します。しかしながら、<body>のインデントは<html>に比べて付随する空白とは見なされません。推定上、この相対的なインデントを文字列の内容の一部にしようとしています。

以下の例では、ハイフンを使って付随する空白を可視化しています。必須の空白は通常の空白です。

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

付随する空白は取り除かれるため、結果としてテキストブロックのコンテンツは以下のようになります。

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

付随する空白を判断するアルゴリズムは、JEP 355で詳細に説明されています。にもかかわらず、それにもかかわらず、正味の効果は非常に単純です。行頭からの空白が最も少ない行から先頭の空白がなくなるまで、テキストブロックのコンテンツ全体を左に移動します。

余白を保持し、付随する余白と見なさないようにするには、周囲のコードに適したインデントで閉じている三重引用符の区切り文字を維持しながら、テキストブロックのコンテンツ行を右に移動します。

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

Trailing White Space

テキストブロックの各行の末尾の空白も付随的なものと見なされ、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
    """;

Note: Unicodeエスケープは字句解析に先立ち、ソースファイル読み取り時に早期に変換されるため、\u0020 は利用できません。対照的に、字句解析でソースファイルをトークンに分割し、文字列リテラルとテキストブロックを識別した後、\040のような文字と文字列のエスケープが処理されます。

Detecting Potential Issues with White Space

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

空白文字を混在させると、一貫性のない意図しない効果が生じる可能性があります。 次の例を考えてみましょう。いくつかの行はスペースでインデントされ、いくつかの行はタブでインデントされています(␉で視覚化しています)。

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

この場合、2行目には空白文字が5つしかなく、他の行には20文字あるため、以下のように付随するインデントを不均等に除去してしまいます。

               red
green
               blue

Javaコンパイラの構文チェック (lint) フラグ -Xlint:text-blocks を使用して、テキストブロックの構文チェックをオンにすると、付随する空白に関連する問題を検出できます。構文チェックがオンの場合、上記の例は「一貫性のない空白のインデント(“inconsistent white space indentation”)」との警告が出ます。

このlintフラグは、「末尾の空白が削除されます(”trailing white space will be removed”)」という別の警告も有効にします。これは、テキストブロック内の行の末尾に空白がある場合に出力されます。末尾の空白を保持する必要がある場合は、上記のセクションで説明したエスケープまたは置換のテクニックのいずれかを使用してください。

Normalization Of Line Terminators

複数行の文字列リテラルの複雑さの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())を使って置き換えることができます。

Translation Of Escape Sequences

文字列リテラルと同様に、テキストブロックはエスケープシーケンス、\b、\f、\n、\t、\r、\”、\’、\、および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··

Note: 前述の通り、Unicodeエスケープシーケンス \u0020 を\040 の代わりとして利用できません。

Style Guidelines For Text Blocks

G1. You should use a text block when it improves the clarity of the code, particularly with multi-line strings.

コードの明快さが改善される場合、特に複数行の文字列の場合、テキストブロックを使うべきです。

// 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. If a string fits on a single line, without concatenation and escaped newlines, you should probably continue to use a string literal.

文字列連結やエスケープされた改行文字を使わずに文字列が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. Use embedded escape sequences when they maintain readability.

可読性を維持する場合、埋め込みエスケープシーケンスを使うべきです。

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

G4. For most multi-line strings, place the opening delimiter at the right end of the previous line, and place the closing delimiter on its own line, at the left margin of the text block.

ほとんどの複数行の文字列では、前の行の右端で区切り文字を開始し、テキストブロックの左余白を付けた新しい行に終端の区切り文字を同一行に置くべきです。

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

G5. Avoid aligning the opening and closing delimiters and the text block’s left margin. This requires reindentation of the text block if the variable name or modifiers are changed.

開始および終了の区切り文字とテキストブロックの左余白を揃えないようにします。変数名または修飾子が変更された場合、これにはテキストブロックの再インデントが必要です。

// 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. Avoid in-line text blocks within complex expressions, as doing so can distort readability. Consider refactoring to a local variable or to a static final field.

可読性が下がる可能性があるため、複雑な表現内でインラインテキストブロックを使用しないでください。ローカル変数またはstatic 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. Either use only spaces or only tabs for the indentation of a text block. Mixing white space will lead to a result with irregular indentation.

テキストブロックのインデントには、スペースのみまたはタブのみを使用します。両者を混在させると、不規則なインデントが発生します。

// 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. When a text block contains sequences of three or more double quotes, escape the first double quote of every run of three double quotes.

テキストブロックに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. Most text blocks should be indented to align with neighbouring Java code.

近隣の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. It is recommended to fully left justify a wide string in order to avoid horizontal scrolling or line wrapping.

水平スクロールや行の折り返しを避けるために、幅の広い文字列(行に多数の単語が含まれる文字列)を完全に左揃えにすることをお勧めします。

// 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. Similarly, it is also reasonable to fully left justify a text block when a high line count causes the closing delimiter is likely to vertically scroll out of view. This allows the reader to track indentation with the left margin when the closing delimiter is out of view.

同様に、行数が多くて終端の区切り文字を垂直方向にスクロールすると見えなくなる場合も、テキストブロックを完全に左揃えにするのは合理的です。これにより、読者は終端の区切り文字が見えなくなったときにも左余白でインデントを追跡できます。

// 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
""";

Preview String Methods

テキストブロック導入に伴い、いくつかの新たなメソッドがStringクラスに追加されています。これらのメソッドはプレビュー機能であることを示すために、削除のためのdeprecatedのマークが付いていることにご注意ください。

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, \”, \’, \ と8進エスケープ) の変換を実行します。Javaコンパイラがこれを使い、テキストブロックや文字列リテラルを処理します。入力データとしてテキストを読み取るプログラムで、エスケープシーケンス処理をしたい場合に、このメソッドは有用です。Unicodeエスケープ(\uNNNN)は処理しませんのでご注意ください。

References

“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.

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中