JEP 355: Text Blocks (Preview)
Text Blocks (текстовые блоки) в статусе Preview (предварительная версия)
| Ответственный | Jim Laskey |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 13 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 326: Raw String Literals (Preview) |
| JEP 368: Text Blocks (Second Preview) | |
| JEP 378: Text Blocks | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2019/04/16 13:50 |
| Обновлён | 2026/07/29 18:59 |
| Задача | 8222530 |
Аннотация
Добавить в язык Java Text Blocks. Блок Text Blocks — это многострочный строковый литерал, в котором не нужно большинство escape-последовательностей, который автоматически форматирует строку предсказуемым образом и при необходимости даёт разработчику управлять форматированием. Это языковая возможность в статусе Preview в JDK 13.
История
Эта работа продолжает исследования, начатые в JEP 326 (Raw String Literals). Тот JEP был отозван и не вошёл в JDK 12.
Цели
-
Упростить написание программ на Java: сделать так, чтобы строки, занимающие несколько строк исходного кода, записывались легко и в типичных случаях без escape-последовательностей.
-
Сделать более читаемыми строки в программах на Java, содержащие код на других языках.
-
Поддержать переход со строковых литералов: любая новая конструкция должна выражать то же множество строк, что и строковый литерал, интерпретировать те же escape-последовательности и допускать те же операции, что и строковый литерал.
Что не является целью
-
Цель не в том, чтобы ввести для строк, выражаемых новой конструкцией, новый ссылочный тип, отличный от
java.lang.String. -
Цель не в том, чтобы ввести новые операторы, отличные от
+, принимающие операндыString. -
Text Blocks не поддерживают интерполяцию строк напрямую. Интерполяцию можно будет рассмотреть в одном из будущих JEP.
-
Text Blocks не поддерживают «сырые» строки, то есть строки, символы которых вообще не должны никак обрабатываться.
Мотивация
В Java, чтобы встроить фрагмент HTML, XML, SQL или JSON в строковый литерал "...", обычно приходится существенно править его, добавляя экранирование и конкатенацию, прежде чем код с этим фрагментом скомпилируется. Такой фрагмент часто трудно читать и тяжело сопровождать.
В более общем смысле потребность записывать в программе на Java короткие, средние и длинные блоки текста встречается почти повсеместно, будь то код на других языках программирования, структурированный текст эталонных файлов или сообщения на естественных языках. С одной стороны, язык Java признаёт эту потребность, допуская строки неограниченного размера и содержания. С другой стороны, в его устройство заложено допущение по умолчанию, что строки достаточно малы, чтобы уместиться на одной строке исходного файла, и достаточно просты, чтобы их было легко экранировать. Язык Java можно сделать более последовательным, если признать, что строки могут быть настолько большими, что занимают несколько строк исходного файла, и предусмотреть, что escape-последовательности в их содержимом могут обозначать не только отдельные символы, но и операции форматирования и компоновки.
Соответственно, читаемость и удобство написания широкого класса программ на Java улучшились бы, если бы в языке был механизм, позволяющий записывать строки более буквально, чем строковый литерал, — на нескольких строках и без визуального шума от экранирования. По сути, двумерный блок текста, а не одномерная последовательность символов.
Пример с HTML
С «одномерными» строковыми литералами
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
С «двумерным» блоком текста
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
Пример с SQL
С «одномерными» строковыми литералами
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` = 'INDIANAPOLIS'\n" +
"ORDER BY `EMP_ID`, `LAST_NAME`;\n";
С «двумерным» блоком текста
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";
Пример с другими языками
С «одномерными» строковыми литералами
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
" print('\"Hello, world\"');\n" +
"}\n" +
"\n" +
"hello();\n");
С «двумерным» блоком текста
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
function hello() {
print('"Hello, world"');
}
hello();
""");
Описание
Блок Text Blocks — это новый вид литерала в языке Java. Его можно использовать для записи строки везде, где может стоять строковый литерал, но он выразительнее и привносит меньше случайной сложности.
Блок Text Blocks состоит из нуля или более символов содержимого, заключённых между открывающим и закрывающим разделителями.
Открывающий разделитель — это последовательность из трёх символов двойной кавычки ("""), за которой следуют ноль или более пробельных символов, а затем признак конца строки. Содержимое начинается с первого символа после признака конца строки открывающего разделителя.
Закрывающий разделитель — это последовательность из трёх символов двойной кавычки. Содержимое заканчивается на последнем символе перед первой двойной кавычкой закрывающего разделителя.
В отличие от символов строкового литерала, содержимое может включать символы двойной кавычки напрямую. Использовать \" в блоке Text Blocks разрешено, но не нужно и не рекомендуется. «Толстые» разделители (""") выбраны для того, чтобы символы " могли встречаться без экранирования, а также чтобы визуально отличать блок Text Blocks от строкового литерала.
В отличие от символов строкового литерала, содержимое может включать признаки конца строки напрямую. Использовать \n в блоке Text Blocks разрешено, но не нужно и не рекомендуется. Например, блок Text Blocks:
"""
line 1
line 2
line 3
"""
эквивалентен строковому литералу:
"line 1\nline 2\nline 3\n"
или конкатенации строковых литералов:
"line 1\n" +
"line 2\n" +
"line 3\n"
Если признак конца строки в конце строки не нужен, закрывающий разделитель можно поставить на последней строке содержимого. Например, блок Text Blocks:
"""
line 1
line 2
line 3"""
эквивалентен строковому литералу:
"line 1\nline 2\nline 3"
Блок Text Blocks может обозначать пустую строку, хотя это не рекомендуется, потому что для этого нужны две строки исходного кода:
String empty = """
""";
Вот несколько примеров некорректных блоков Text Blocks:
String a = """"""; // no line terminator after opening delimiter
String b = """ """; // no line terminator after opening delimiter
String c = """
"; // no closing delimiter (text block continues to EOF)
String d = """
abc \ def
"""; // unescaped backslash (see below for escape processing)
Обработка во время компиляции
Блок Text Blocks — это константное выражение типа String, так же как строковый литерал. Однако, в отличие от строкового литерала, содержимое блока Text Blocks обрабатывается компилятором Java в три отдельных шага:
-
Признаки конца строки в содержимом преобразуются в LF (
\u000A). Цель этого преобразования — следовать принципу наименьшего удивления при переносе исходного кода Java между платформами. -
Удаляются случайные пробельные символы вокруг содержимого, добавленные ради соответствия отступам исходного кода Java.
-
Интерпретируются escape-последовательности в содержимом. Благодаря тому, что интерпретация выполняется последним шагом, разработчики могут писать escape-последовательности, например
\n, и предыдущие шаги не изменят и не удалят их.
Обработанное содержимое записывается в файл class как запись CONSTANT_String_info в пуле констант, так же как символы строкового литерала. В файле class не записывается, получена ли запись CONSTANT_String_info из блока Text Blocks или из строкового литерала.
Во время выполнения блок Text Blocks вычисляется в экземпляр String, так же как строковый литерал. Экземпляры String, полученные из блоков Text Blocks, неотличимы от экземпляров, полученных из строковых литералов. Два блока Text Blocks с одинаковым обработанным содержимым будут ссылаться на один и тот же экземпляр String благодаря интернированию, так же как и строковые литералы.
В следующих разделах обработка во время компиляции рассматривается подробнее.
1. Признаки конца строки
Компилятор Java нормализует признаки конца строки в содержимом: CR (\u000D) и CRLF (\u000D\u000A) заменяются на LF (\u000A). Так строка, полученная из содержимого, одинакова на всех платформах, даже если исходный код был перекодирован в кодировку платформы (см. javac -encoding).
Например, если исходный код Java, созданный на платформе Unix (где признак конца строки — LF), редактируется на платформе Windows (где признак конца строки — CRLF), то без нормализации содержимое стало бы на один символ длиннее для каждой строки. Любой алгоритм, рассчитывающий на то, что признак конца строки — LF, мог бы перестать работать, а любой тест, которому нужно проверить равенство строк с помощью String::equals, не прошёл бы.
Escape-последовательности \n (LF), \f (FF) и \r (CR) при нормализации не интерпретируются: обработка escape-последовательностей выполняется позже.
2. Случайные пробельные символы
Блоки Text Blocks в разделе «Мотивация» читались легче, чем соответствующие им конкатенированные строковые литералы, но при очевидной интерпретации содержимое блока Text Blocks включало бы пробелы, добавленные для отступа встроенной строки, чтобы она аккуратно выравнивалась по открывающему разделителю. Вот пример с HTML, где пробелы, добавленные разработчиком для отступа, показаны точками:
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..............""";
Поскольку открывающий разделитель обычно ставят на той же строке, что и оператор или выражение, использующее блок Text Blocks, то, что каждая строка начинается с 14 показанных пробелов, на самом деле ничего не значит. Если бы эти пробелы входили в содержимое, блок Text Blocks обозначал бы строку, отличную от той, что обозначают конкатенированные строковые литералы. Это мешало бы переходу и постоянно вызывало бы удивление: с подавляющей вероятностью разработчику эти пробелы в строке не нужны. Кроме того, закрывающий разделитель обычно выравнивают по содержимому, что тоже говорит о незначимости 14 показанных пробелов.
Пробелы могут встречаться и в конце каждой строки, особенно если блок Text Blocks заполняется копированием фрагментов из других файлов (которые сами могли быть собраны копированием из других файлов). Вот тот же пример с HTML с несколькими пробелами в конце строк, где пробелы снова показаны точками:
String html = """
..............<html>...
.............. <body>
.............. <p>Hello, world</p>....
.............. </body>.
..............</html>...
..............""";
Пробельные символы в конце строк чаще всего появляются ненамеренно, зависят от конкретного случая и ничего не значат. С подавляющей вероятностью разработчику до них нет дела. Пробельные символы в конце строк похожи на признаки конца строки: и то и другое — невидимые артефакты среды редактирования исходного кода. Поскольку наличие пробельных символов в конце строк никак не видно, их включение в содержимое постоянно вызывало бы удивление, ведь оно влияло бы на длину, хэш-код и т. д. строки.
Соответственно, уместная интерпретация содержимого блока Text Blocks — отличать случайные пробельные символы в начале и конце каждой строки от значимых пробельных символов. Компилятор Java обрабатывает содержимое, удаляя случайные пробельные символы, чтобы получить то, что имел в виду разработчик. После этого при желании можно управлять отступами с помощью String::indent. Границы показаны с помощью |:
|<html>|
| <body>|
| <p>Hello, world</p>|
| </body>|
|</html>|
Алгоритм пересчёта отступов принимает содержимое блока Text Blocks, признаки конца строки в котором уже нормализованы к LF. Он удаляет одинаковое количество пробельных символов из каждой строки содержимого, пока хотя бы в одной из строк в крайней левой позиции не окажется непробельный символ. Позиция открывающих символов """ на алгоритм не влияет, а позиция закрывающих символов """ влияет, если они стоят на отдельной строке. Алгоритм следующий:
-
Разбить содержимое блока Text Blocks по каждому LF и получить список отдельных строк. Обратите внимание: любая строка содержимого, состоявшая только из LF, станет пустой строкой в списке отдельных строк.
-
Добавить все непустые строки из списка отдельных строк в множество определяющих строк. (Пустые строки — строки без символов или состоящие только из пробельных символов — не влияют на видимый отступ. Если исключить пустые строки из множества определяющих строк, они не собьют шаг 4 алгоритма.)
-
Если последняя строка в списке отдельных строк (то есть строка с закрывающим разделителем) пустая, добавить её в множество определяющих строк. (Отступ закрывающего разделителя должен влиять на отступ содержимого в целом — принцип «значимой последней строки».)
-
Вычислить общий пробельный префикс множества определяющих строк: подсчитать число начальных пробельных символов в каждой строке и взять минимальное значение.
-
Удалить общий пробельный префикс из каждой непустой строки в списке отдельных строк.
-
Удалить все пробельные символы в конце строк из всех строк изменённого на шаге 5 списка отдельных строк. На этом шаге строки изменённого списка, состоящие только из пробельных символов, становятся пустыми, но не удаляются.
-
Построить итоговую строку, соединив все строки изменённого на шаге 6 списка отдельных строк и используя LF в качестве разделителя между строками. Если последняя строка в списке с шага 6 пустая, то соединяющий LF из предыдущей строки будет последним символом итоговой строки.
Escape-последовательности \b (backspace) и \t (табуляция) алгоритм не интерпретирует: обработка escape-последовательностей выполняется позже.
Алгоритм переформатирования отступов будет нормативно описан в The Java Language Specification. Разработчики получат к нему доступ через новый метод экземпляра String::stripIndent.
Правило для значимой завершающей строки
Обычно текстовый блок форматируют двумя способами: во-первых, левый край содержимого располагают под первым " открывающего разделителя, а во-вторых, закрывающий разделитель помещают на отдельную строку точно под открывающим разделителем. В получившейся строке не будет пробельных символов в начале ни одной строки, и в неё не войдёт завершающая пустая строка закрывающего разделителя.
Однако поскольку завершающая пустая строка считается определяющей строкой, её сдвиг влево уменьшает общий пробельный префикс и, следовательно, уменьшает количество пробельных символов, удаляемых из начала каждой строки. В крайнем случае, когда закрывающий разделитель сдвинут до упора влево, общий пробельный префикс становится нулевым, и удаление пробельных символов фактически отключается.
Например, если закрывающий разделитель сдвинут до упора влево, то случайных пробельных символов, которые можно было бы показать точками, нет:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
С учётом завершающей пустой строки с закрывающим разделителем общий пробельный префикс равен нулю, поэтому из начала каждой строки ничего не удаляется. Таким образом, алгоритм выдаёт следующее (| показывает левое поле):
| <html>
| <body>
| <p>Hello, world</p>
| </body>
| </html>
Предположим теперь, что закрывающий разделитель сдвинут не до упора влево, а расположен под t из html, то есть на восемь пробелов глубже объявления переменной:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
Пробелы, показанные точками, считаются случайными:
String html = """
........ <html>
........ <body>
........ <p>Hello, world</p>
........ </body>
........ </html>
........""";
С учётом завершающей пустой строки с закрывающим разделителем общий пробельный префикс равен восьми, поэтому из начала каждой строки удаляется восемь пробельных символов. Таким образом, алгоритм сохраняет существенный отступ содержимого относительно закрывающего разделителя:
| <html>
| <body>
| <p>Hello, world</p>
| </body>
| </html>
Наконец, предположим, что закрывающий разделитель сдвинут немного правее содержимого:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
Пробелы, показанные точками, считаются случайными:
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
.............. """;
Общий пробельный префикс равен 14, поэтому из начала каждой строки удаляется 14 пробельных символов. Завершающая пустая строка очищается до пустой строки, а поскольку она последняя, то затем отбрасывается. Иными словами, сдвиг закрывающего разделителя правее содержимого ни на что не влияет, и алгоритм снова сохраняет существенный отступ содержимого:
|<html>
| <body>
| <p>Hello, world</p>
| </body>
|</html>
3. Escape-последовательности
После переформатирования отступов содержимого интерпретируются все escape-последовательности в нём. Текстовый блок поддерживает те же escape-последовательности, что и строковый литерал, например \n, \t, \', \" и \\. Полный список приведён в разделе 3.10.6 The Java Language Specification. Разработчики получат доступ к обработке escape-последовательностей через новый метод экземпляра String::translateEscapes.
Благодаря тому что escape-последовательности интерпретируются на последнем шаге, разработчики могут использовать \n, \f и \r для вертикального форматирования строки, не влияя на преобразование символов конца строки на шаге 1, а также \b и \t для горизонтального форматирования строки, не влияя на удаление случайных пробельных символов на шаге 2. Рассмотрим, например, текстовый блок, содержащий escape-последовательность \r (CR):
String html = """
<html>\r
<body>\r
<p>Hello, world</p>\r
</body>\r
</html>\r
""";
Escape-последовательности CR обрабатываются только после того, как символы конца строки нормализованы в LF. Если показать LF (\u000A) и CR (\u000D) с помощью Unicode-escape-последовательностей, результат будет таким:
|<html>\u000D\u000A
| <body>\u000D\u000A
| <p>Hello, world</p>\u000D\u000A
| </body>\u000D\u000A
|</html>\u000D\u000A
Обратите внимание, что " можно свободно использовать внутри текстового блока, даже рядом с открывающим или закрывающим разделителем. Например:
String story = """
"When I use a word," Humpty Dumpty said,
in rather a scornful tone, "it means just what I
choose it to mean - neither more nor less."
"The question is," said Alice, "whether you
can make words mean so many different things."
"The question is," said Humpty Dumpty,
"which is to be master - that's all."
""";
Однако в последовательностях из трёх символов " нужно экранировать хотя бы один ", чтобы они не выглядели как закрывающий разделитель:
String code =
"""
String text = \"""
A text block inside a text block
\""";
""";
Конкатенация текстовых блоков
Текстовые блоки можно использовать везде, где можно использовать строковый литерал. Например, текстовые блоки и строковые литералы можно конкатенировать друг с другом в любом сочетании:
String code = "public void print(Object o) {" +
"""
System.out.println(Objects.toString(o));
}
""";
Однако конкатенация с участием текстового блока может получиться довольно громоздкой. Возьмём за отправную точку такой текстовый блок:
String code = """
public void print(Object o) {
System.out.println(Objects.toString(o));
}
""";
Предположим, его нужно изменить так, чтобы тип o брался из переменной. При использовании конкатенации текстовый блок с последующим кодом должен будет начинаться с новой строки. К сожалению, если просто вставить перевод строки в программу, как показано ниже, между типом и текстом, начинающимся с o, появится длинный промежуток из пробельных символов:
String code = """
public void print(""" + type + """
o) {
System.out.println(Objects.toString(o));
}
""";
Пробельные символы можно удалить вручную, но это ухудшает читаемость цитируемого кода:
String code = """
public void print(""" + type + """
o) {
System.out.println(Objects.toString(o));
}
""";
Более аккуратная альтернатива — использовать String::replace или String::format, как показано ниже:
String code = """
public void print($type o) {
System.out.println(Objects.toString(o));
}
""".replace("$type", type);
String code = String.format("""
public void print(%s o) {
System.out.println(Objects.toString(o));
}
""", type);
Ещё одна альтернатива предполагает добавление нового метода экземпляра String::formatted, который можно было бы использовать так:
String source = """
public void print(%s object) {
System.out.println(Objects.toString(object));
}
""".formatted(type);
Дополнительные методы
Для поддержки текстовых блоков будут добавлены следующие методы:
String::stripIndent(): удаляет случайные пробельные символы из содержимого текстового блокаString::translateEscapes(): преобразует escape-последовательностиString::formatted(Object... args): упрощает подстановку значений в текстовый блок
Альтернативы
Ничего не делать
Java более 20 лет успешно развивается со строковыми литералами, в которых переводы строк нужно экранировать. IDE облегчают сопровождение, поддерживая автоматическое форматирование и конкатенацию строк, занимающих несколько строк исходного кода. Класс String тоже развивался, и в нём появились методы, упрощающие обработку и форматирование длинных строк, например метод, представляющий строку как поток строк. Однако строки настолько фундаментальная часть языка Java, что недостатки строковых литералов очевидны огромному числу разработчиков. В других языках для JVM тоже продвинулись в том, как записываются длинные и сложные строки. Поэтому неудивительно, что многострочные строковые литералы неизменно входят в число самых востребованных возможностей Java. Введение многострочной конструкции низкой или умеренной сложности дало бы большую отдачу.
Разрешить строковому литералу занимать несколько строк
Многострочные строковые литералы можно было бы ввести в Java, просто разрешив символы конца строки в существующих строковых литералах. Однако это никак не решило бы проблему необходимости экранировать символы ". \" — самая частая escape-последовательность после \n, поскольку фрагменты кода встречаются часто. Единственный способ избежать экранирования " в строковом литерале — предусмотреть для строковых литералов альтернативную схему разделителей. Разделители подробно обсуждались в рамках JEP 326 (Raw String Literals), и полученный опыт был учтён при проектировании текстовых блоков, поэтому нарушать стабильность строковых литералов было бы ошибкой.
Заимствование многострочного литерала из другого языка
По словам Brian Goetz:
Многие предлагали, чтобы Java переняла многострочные строковые литералы из Swift или Rust. Однако подход «просто сделать так, как в языке X» по своей сути безответственен: почти каждая возможность каждого языка обусловлена другими возможностями этого языка. Вместо этого задача в том, чтобы учиться на том, как это делают другие языки, оценивать выбранные ими компромиссы (явные и неявные) и спрашивать, что из этого применимо с учётом ограничений нашего языка и ожиданий пользователей в нашем сообществе.
Для JEP 326 (Raw String Literals) мы изучили многие современные языки программирования и их поддержку многострочных строковых литералов. Результаты этих исследований повлияли на текущее предложение, например на выбор трёх символов " в качестве разделителей (хотя у этого выбора были и другие причины) и на признание необходимости автоматического управления отступами.
Удаление случайных пробельных символов
Если бы в Java появились многострочные строковые литералы без поддержки автоматического удаления случайных пробельных символов, многие разработчики писали бы метод для их удаления сами или добивались бы добавления такого метода в класс String. Однако это означает потенциально дорогие вычисления при каждом создании экземпляра строки во время выполнения, что уменьшило бы выгоду от интернирования строк. Наиболее подходящим решением представляется требование языка Java удалять случайные пробельные символы как в начале, так и в конце строк. Разработчики могут отказаться от удаления начальных пробельных символов, аккуратно расположив закрывающий разделитель.
Отказаться от удаления завершающих пробельных символов нельзя, поэтому в редких случаях, когда завершающие пробельные символы значимы (например, два пробела для тега <br /> в Markdown), разработчикам придётся прибегать к необычным мерам, чтобы принудительно их сохранить, например к восьмеричной escape-последовательности \040 (символ ASCII 32, пробел):
"""
The quick brown fox\040\040
jumps over the lazy dog
"""
"""
The quick brown fox""" + " \n" + """
jumps over the lazy dog
"""
"""
The quick brown fox |
jumps over the lazy dog
""".replace("|", "");
Необработанные строковые литералы
В JEP 326 (Raw String Literals) мы подошли к задаче записи строк без экранирования переводов строк и кавычек иначе, сосредоточившись на «необработанности» строк. Теперь мы считаем, что этот акцент был ошибочным: хотя необработанные строковые литералы легко могли занимать несколько строк исходного кода, цена поддержки неэкранированных разделителей в их содержимом была чрезмерной. Это снижало эффективность возможности в многострочном сценарии, а он крайне важен, поскольку многострочные (но не по-настоящему необработанные) фрагменты кода часто встраиваются в программы на Java. Хорошим результатом смены акцента с необработанности на многострочность стало новое внимание к единому языку escape-последовательностей для строковых литералов, текстовых блоков и связанных возможностей, которые могут быть добавлены в будущем.
Тестирование
Тесты, использующие строковые литералы для создания, интернирования и обработки экземпляров String, следует продублировать так, чтобы они использовали и текстовые блоки. Следует добавить негативные тесты для граничных случаев, связанных с символами конца строки и EOF.
Следует добавить тесты, проверяющие, что в текстовые блоки можно встраивать Java-in-Java, Markdown-in-Java, SQL-in-Java и хотя бы один JVM-language-in-Java.