※本記事は、Mala Guptaによる”Text Blocks Come to Java“を翻訳したものです。


長く待ち望まれてきた複数行文字列がJava 13で実現

著者:Mala Gupta

2019年10月16日

Java 13のテキスト・ブロックを使うことで、複数行文字列リテラルを簡単に使えるようになります。文字列リテラル内の特殊文字をエスケープすることや、複数行にまたがる値に連結演算子を使うことは不要になります。さらに、文字列を書式設定する方法も制御できるようになります。テキスト・ブロックとは、複数行文字列を表すJavaの用語です。テキスト・ブロックにより、コードの可読性は大幅に向上します。

本記事では、テキスト・ブロックとは何か、テキスト・ブロックによって対処できる問題、そしてテキスト・ブロックの使い方について説明します。それでは始めます。

テキスト・ブロックとは

Stringデータタイプはおそらく、Java開発者がもっともよく使う型の1つです。任意の言語で数文字の文字列から複数行文字列まで、何でも格納できます。しかし、この柔軟性のため、一部のString値を読み取ることや変更することが難しくなる場合があります。例を挙げるなら、引用符が埋め込まれている文字列、エスケープ文字、複数行にわたる文字列などです。

それでは、Java 13の新しいプレビュー機能であるテキスト・ブロックが、どのように役立つかについて見ていきます。

テキスト・ブロックを使用して、複数行Stringリテラルを簡単に定義することができます。通常のStringリテラルを使って実現した場合に見にくさの原因となる、連結演算子やエスケープ・シーケンスの追加が不要です。さらに、String値をどのように書式設定するかも制御できます。例として、次のHTMLスニペットについて考えてみます。

    String html = """
    <HTML>
      <BODY>
        <H1>"Java 13 is here!"</H1>
      </BODY>
    </HTML>""";

ブロックの開始と終了を表す3つの二重引用符に注目してください。以前のJavaでは代わりにどのように記述していたかについて考えてみます。

    String html1 = 
        "<HTML>\n\t<BODY>\n\t\t<H1>\"Java 13 is here!\"</H1> \n\t</BODY>\n</HTML>\n";

次の方が一般的かもしれません。

    String html = "<HTML>" +
    "\n\t" + "<BODY>" +
    "\n\t\t" + "<H1>\"Java 13 is here!\"</H1>" +
    "\n\t" + "</BODY>" +
    "\n" + "</HTML>";

いずれも、テキスト・ブロックほどわかりやすくはありません。

構文

前述のように、テキスト・ブロックは、3つの二重引用符(""")を、開始および終了を表すデリミタとして使って定義します。開始デリミタの後には、0個以上の空白と行終端文字(改行)を置くことができます。テキスト・ブロックの値は、この行終端文字のから始まります。終了デリミタの場合、このようなルールはありません。

したがって、次の例は無効なテキスト・ブロックとなります。開始デリミタの後に行終端文字が存在しないからです。

    String multilineValue1 = """ """;
    String multilineValue2 = """""";

プレビュー言語機能

テキスト・ブロックは、Java 13でプレビュー言語機能としてリリースされました。しかし、プレビュー言語機能は未完成または開発途中の機能ではありません。実際のところ、開発者に使ってもらう準備は整っているものの、細かい部分は今後のJavaリリースで変更される可能性もある機能という意味です。このような扱いになっていることには、理由があります。

開発者は、6か月周期の新しいリリース・サイクルにより、言語の新機能を使えるようになっています。しかし、Javaチームは、Javaに言語機能を恒久的に追加する前に、その機能に対する開発者の意見について評価します。フィードバック次第で、プレビュー機能が微調整されてからJava SEに追加されることも、完全に削除されることもあります。そこで、テキスト・ブロックについてフィードバックがある方は、JDKメーリング・リスト(メンバー登録が必要です)で共有してください。

プレビュー言語機能を使うためには、コンパイル時と実行時に明確に有効化する必要があります。これにより、意図せずにプレビュー機能を使ってしまうことがないようにしています。テキスト・ブロックを含むソース・ファイルをコンパイルするためには、--enable-previewオプションと-release 13オプションを使用します。コマンドラインを使ってソース・ファイルJava13.javaをコンパイルする例を次に示します。

    javac --enable-preview --release 13 Java13.java

プレビュー機能は変更される可能性もあることを強調するため、先ほどのコマンドを実行した際に、図1のようなコンパイラ警告が表示されます。

プレビュー機能が使われているコードに対して表示されるコンパイラ警告

図1:>プレビュー機能が使われているコードに対して表示されるコンパイラ警告

クラスJava13を実行するときも、--enable-previewオプションを使う必要があります。

    java --enable-preview Java13

次は、テキスト・ブロックの実装について見てみます。

同じStringデータタイプ

従来のString値もテキスト・ブロックも、コンパイルされると同じ型、すなわちStringになります。バイトコードのクラス・ファイルでは、String値が従来のStringによるものか、テキスト・ブロックによるものかは区別されません。このことは、テキスト・ブロックの値が文字列プールに格納されていることを示しています。

次のコードをご覧ください。皆さんは、変数traditonalStringtextBlockStringが同じStringインスタンスを指すと思うでしょうか。

    String traditionalString = "Java";
    String textBlockString = """
    Java""";
    System.out.println(traditionalString == textBlockString);

この2つは内容が同じであるため、同じインスタンスを指します。先ほどのコードでは、trueが出力されます。

本記事の冒頭で、従来のStringでは複数行のString値が扱いづらくなることについて説明しました。続くいくつかのセクションでは、テキスト・ブロックがどのように役立つかについて取り上げます。

複数行の値の扱いを改善

開発者は、JSON、HTML、XMLや正規表現(regex)データなどの複数行文字列値を頻繁に扱います。テキスト・ブロックを使うことで、複数行JSON値の扱いが次のようにシンプルになります。

    String json = """
        {
          "name": "web",
          "version": "1.0.0",
          "dependencies": "AppA"
        }
    """;

エスケープ・シーケンスや連結演算子によって見にくくなることがないため、JSON値を容易に編集できます。念のため、メリットを感じないと思う方のために、従来のStringで同じJSON値を定義した場合の例も挙げておきます。

    String json = 
        "{" +
          "\"name\": \"web\"," +
          "\"version\": \"1.0.0\"," +
          "\"dependencies\": \"AppA\" + 
        "}";

この例は、読者のSven Bloesl氏の提案によって改善されています。

SQL問合せをString値として格納するためには、SQL問合せをコピーして貼り付けるか、または自分で書くかのいずれかを行います。String変数を使って(Java 12またはそれ以前のバージョンで)複数行SQL問合せを次のようにして格納したとします。

    String query = 
      "SELECT name, age" +
      "FROM EMP" + 
      "WHERE name = \'John\'" +
      "AND age > 20";

このコードは、無効な問合せを表しています。各行の末尾に空白がないため、この問合せは次のように解釈されます。

    SELECT name, ageFROM EMPWHERE name = 'John'AND age > 20

Karim Ourrai氏とBrian Goetz氏の報告により、誤った例がこのセクションから削除されました。

テキスト・ブロックを使うことにより、同じような問題を避けることができます。

    String query = """
      SELECT name, age
      FROM EMP
      WHERE name = 'John'
        AND age > 20
      """;

テキスト・ブロック内のエスケープ・シーケンス

Stringリテラルと同様に、テキスト・ブロックにもさまざまなエスケープ・シーケンスを追加できます。たとえば、テキスト・ブロックに新しい行を含める場合、値を複数行にわたって配置することも、\nのようなエスケープ・シーケンスを使うこともできます。次のコードでは、I'mhappyは別々の行になります。

    String html = """
    <HTML>
      <BODY>
        <H1>I'm \nhappy</H1>
      </BODY>
    </HTML>""";

ご想像のとおり、無効なエスケープ・シーケンスやエスケープしていないバックスラッシュは許可されません。

意味のない空白とインデント

大きな疑問として、不要な空白がどのように扱われるかが挙げられます。実は、この点はコンパイラがエレガントに処理してくれます。テキスト・ブロックでは、すべての行で一番左側にある非空白文字、または一番左側にある終了デリミタによって、意味のある空白が始まる場所が定義されます。

図2の場合、getHTML()で返されるString値の中で、一番左側にある非空白文字は<<HTML>の最初の部分)です。この位置は、終了デリミタの位置とも一致しています。

不要な空白(青)と意味のある空白(緑)を表したコード

図2:>不要な空白(青)と意味のある空白(緑)を表したコード

このコードでは、図3に示す文字列が返されます(最初と最後の行では、先頭に空白が含まれません)。

先ほどのコードから返される文字列(緑色の四角は文字列に含まれる空白を示します)

図3:>先ほどのコードから返される文字列(緑色の四角は文字列に含まれる空白を示します)

空白が削除されないように、必須としてマークするためには、終了デリミタ、またはいずれかの非空白文字を左に動かします。図4に示すように、終了デリミタ"""を8文字分左に動かしてみます。

終了デリミタを左に動かした、図2のコード

図4:>終了デリミタを左に動かした、図2のコード

変更したコードでは、図5に示すString値が返されます。各行の先頭に8文字分の空白(緑色の四角)が追加されています。その他の空白も緑色の四角で表されています。

図4のコードから出力される文字列(先頭に空白(緑色の四角)が追加されています)

図5:>図4のコードから出力される文字列(先頭に空白(緑色の四角)が追加されています)

デフォルトでは、それぞれの行の末尾にある空白はテキスト・ブロックから削除されます。その空白を保持する必要がある場合は、8進エスケープ・シーケンス\040(ASCIIでは、空白は文字32となります)を使って強制的に空白を含めることができます。次に例を示します。この例では、テキスト・ブロックの2行目の末尾に空白を追加しています。

    String campaign = """
                Don't leave home without -
                money &\040
                carry bag.
                Reduce | Reuse
                """;

なお、必須の空白にタブ(\t)が含まれている場合、タブは展開されず、1つの空白としてカウントされることに注意してください。

テキスト・ブロックの連結

テキスト・ブロックは、従来のString値と連結できます。その逆も可能です。次に例を示します。

    String concatenate() {
        return """
                Items to avoid -
                Single
                Use
                Plastics
                """ 
                + 
                "Let's pledge to find alternatives";
    }

String値を連結する理由の1つに、変数の値の挿入があります。

    String concatenate(Object obj) {
        return """
                Items to avoid -
                Single
                Use
                """
                + obj + """
                Let's pledge to find 
                alternatives""";
    }

テキスト・ブロックは、文字列が想定される任意の場所で使うことができます。そのため、たとえばString.replaceメソッドでも、特別な処理をせずに使うことができます。

    String concatenateReplace(Object obj) {
        return """
            Items to avoid -
            Single
            Use
            $type
            Let's pledge to find
            alternatives""".replace("$type", obj.toString());
    }

同じように、format()をはじめとする、Stringの任意のメソッドも使用できます。

まとめ

テキスト・ブロックにより、開発者は複数行文字列値をより簡単に扱えるようになります。現時点でテキスト・ブロックはプレビュー機能であり、変更される可能性もあることに留意してください。 ただし、そのような状況ではあっても、コーディング作業を大いに軽減してくれるはずです。

Java Magazine 日本版Vol.47の他の記事

Java 13のswitch式と再実装されたSocket APIの内側
言語の内側:シールド型
TeaVMを使ってブラウザでJavaを動かす
ツールをよく知る
クイズに挑戦:1次元配列(中級者向け)
クイズに挑戦:カスタム例外(上級者向け)
クイズに挑戦:ロケールの読取りと設定(上級者向け)
クイズに挑戦:関数型インタフェース(上級者向け)


Mala Gupta

Mala Gupta(@eMalaGupta):Java Champion。JetBrainsのデベロッパー・アドボケート。eJavaGuru.comの創設者で、認定試験に関する何冊かの人気書籍を執筆。Delhi Java User Groupの共同リーダーであり、Women Who Codeデリー支部のディレクターも務める。