跳到主要内容

JEP 378:文本块

概括

_将文本块_添加到 Java 语言中。文本块是多行字符串文字,它不需要大多数转义序列,以可预测的方式自动格式化字符串,并让开发人员在需要时控制格式。

历史

文本块是由JEP 355于 2019 年初提出的,作为JEP 326 (原始字符串文字)中开始的探索的后续,它最初针对 JDK 12,但最终被撤回,并且没有出现在该版本中。 JEP 355于 2019 年 6 月作为预览功能针对 JDK 13。 JDK 13 的反馈建议在 JDK 14 中再次预览文本块,并添加两个新的转义序列。因此,JEP 368于 2019 年 11 月作为预览功能面向JDK 14 。对 JDK 14 的反馈表明,文本块已准备好在 JDK 15 中成为最终且永久的,无需进一步更改。

目标

  • 通过轻松表达跨越多行源代码的字符串,同时避免常见情况下的转义序列,从而简化 Java 程序的编写任务。

  • 增强 Java 程序中表示用非 Java 语言编写的代码的字符串的可读性。

  • 通过规定任何新构造都可以与字符串文字表达相同的字符串集,解释相同的转义序列,并以与字符串文字相同的方式进行操作,从而支持从字符串文字迁移。

  • 添加转义序列以管理显式空白和换行符控制。

非目标

  • java.lang.String为任何新构造表达的字符串定义与 不同的新引用类型不是目标。

  • +定义不同于、接受操作数的新运算符并不是目标String

  • 文本块不直接支持字符串插值。未来的 JEP 中可能会考虑插值。同时,新的实例方法String::formatted可以在需要插值的情况下提供帮助。

  • 文本块不支持原始字符串,即字符未经过任何方式处理的字符串。

动机

在 Java 中,在字符串文字中嵌入 HTML、XML、SQL 或 JSON 的片段"..."通常需要在包含该片段的代码编译之前使用转义和连接进行大量编辑。该代码片段通常难以阅读且难以维护。

更一般地说,在 Java 程序中表示短、中、长文本块的需求几乎是普遍的,无论文本是来自其他编程语言的代码、表示黄金文件的结构化文本还是自然语言的消息。一方面,Java 语言通过允许无限大小和内容的字符串来认识到这种需求;另一方面,它体现了一个设计默认值,即字符串应该足够小,可以在源文件的单行上表示(由“字符包围),并且足够简单,可以轻松转义。这种设计默认值与大数字不一致Java 程序中字符串太长,无法轻松地放在一行中。

因此,拥有一种比字符串文字更字面地表示字符串的语言机制——跨多行且没有转义的视觉混乱,将提高大量 Java 程序的可读性和可写性。本质上,是二维文本块,而不是一维字符序列。

尽管如此,预测 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();
""");

描述

除了添加了有关新转义序列的小节之外,本节与此 JEP 的前身JEP 355中的同一节相同。

文本块_是Java 语言中的一种新型文字。它可用于在字符串文字_可能出现的任何地方表示字符串,但提供了更大的表达能力和更少的意外复杂性。

文本块由零个或多个内容字符组成,并由开始和结束定界符括起来。

开始定界符_是三个双引号字符 ( ) 的序列,"""后跟零个或多个空格,后跟行终止符。内容从起始定界符的行终止符之后的第一个字符开始。_

_结束分隔符_是三个双引号字符的序列。内容以结束分隔符的第一个双引号之前的最后一个字符结束。

与字符串文字中的字符不同,内容可以直接包含双引号字符。\"允许在文本块中使用,但不是必需的或不建议使用。选择粗分隔符 ( """) 以便"字符可以显示为未转义,并且还可以在视觉上区分文本块和字符串文字。

与字符串文字中的字符不同,内容可以直接包含行终止符。\n允许在文本块中使用,但不是必需的或不建议使用。例如,文本块:

"""
line 1
line 2
line 3
"""

相当于字符串文字:

"line 1\nline 2\nline 3\n"

或字符串文字的串联:

"line 1\n" +
"line 2\n" +
"line 3\n"

如果字符串末尾不需要行终止符,则可以将结束分隔符放在内容的最后一行。例如,文本块:

"""
line 1
line 2
line 3"""

相当于字符串文字:

"line 1\nline 2\nline 3"

文本块可以表示空字符串,但不建议这样做,因为它需要两行源代码:

String empty = """
""";

以下是格式错误的文本块的一些示例:

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)

编译时处理

文本块是类型的常量表达式String,就像字符串文字一样。但是,与字符串文字不同,Java 编译器通过三个不同的步骤处理文本块的内容:

  1. 内容中的行终止符被翻译为 LF ( \u000A)。此翻译的目的是在跨平台移动 Java 源代码时遵循最小意外原则。

  2. 为了匹配 Java 源代码的缩进而引入的内容周围的附带空白已被删除。

  3. 内容中的转义序列被解释。将解释作为最后一步意味着开发人员可以编写转义序列,而\n不会被前面的步骤修改或删除。

class处理后的内容作为常量池中的条目记录在文件中CONSTANT_String_info,就像字符串文字的字符一样。该class文件不记录条目是CONSTANT_String_info源自文本块还是字符串文字。

在运行时,文本块被评估为 的实例String,就像字符串文字一样。String从文本块派生的实例与从字符串文字派生的实例没有区别。String由于驻留,具有相同处理内容的两个文本块将引用同一个实例,就像字符串文字一样。

以下各节更详细地讨论编译时处理。

1. 线路终止符

Java 编译器将内容中的行终止符从 CR ( ) 和 CRLF ( )_规范化_为 LF ( )。这可以确保从内容派生的字符串在跨平台上是等效的,即使源代码已转换为平台编码(请参阅 参考资料)。\u000D``\u000D\u000A``\u000A``javac -encoding

例如,如果在 Unix 平台(行终止符为 LF)上创建的 Java 源代码在 Windows 平台(行终止符为 CRLF)上编辑,那么如果不进行规范化,内容将每增加一个字符。线。任何依赖 LF 作为行终止符的算法都可能会失败,并且任何需要验证字符串相等性的测试String::equals都会失败。

转义序列\n(LF)、\f(FF) 和\r(CR)在标准化期间_不会_被解释;转义处理稍后发生。

2. 附带的空白

上面显示的文本块比它们的串联字符串文字更容易阅读,但是对文本块内容的明显解释将包括添加的空格以缩进嵌入的字符串,以便它与开始分隔符整齐地对齐。以下是使用点来可视化开发人员为缩进添加的空格的 HTML 示例:

String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..............""";

由于起始定界符通常与使用文本块的语句或表达式出现在同一行,因此每行开始 14 个可视化空格这一事实没有实际意义。在内容中包含这些空格意味着文本块表示的字符串与连接的字符串文字表示的字符串不同。这会损害迁移,并且会反复出现令人意外的情况:开发人员极有可能不_希望_字符串中存在这些空格。此外,结束分隔符的位置通常与内容对齐,这进一步表明 14 个可视化空格无关紧要。

空格也可能出现在每行的末尾,特别是当文本块是通过从其他文件复制粘贴片段来填充时(这些片段本身可能是通过从更多文件复制粘贴而形成的)。下面是用一些尾随空格重新设计的 HTML 示例,再次使用点来可视化空格:

String html = """
..............<html>...
.............. <body>
.............. <p>Hello, world</p>....
.............. </body>.
..............</html>...
..............""";

尾随空白通常是无意的、特殊的且无关紧要的。开发者极有可能不关心它。尾随空白字符与行终止符类似,因为两者都是源代码编辑环境的不可见产物。由于没有视觉指导来说明尾随空白字符的存在,将它们包含在内容中将是一个反复出现的意外来源,因为它会影响字符串的长度、哈希码等。

因此,对文本块内容的适当解释是区分每行开头和结尾处的_附带空白与__基本空白_。 Java 编译器通过删除偶然的空白来处理内容,以产生开发人员想要的结果。String::indent如果需要的话,可以用来进一步操作缩进。用于|可视化边距:

|<html>|
| <body>|
| <p>Hello, world</p>|
| </body>|
|</html>|

_重新缩进算法_采用行终止符已标准化为 LF 的文本块的内容。它从每行内容中删除相同数量的空格,直到至少其中一行的最左侧位置包含非空格字符。开始字符的位置"""对算法没有影响,但结束字符的位置"""如果放置在自己的行上则会产生影响。算法如下:

  1. 在每个 LF 处分割文本块的内容,生成_单独行_的列表。请注意,内容中任何仅是 LF 的行都将成为各个行列表中的空行。

  2. 将单个行列表中的所有_非空行添加到一组__确定行_中。 (空行——空的或完全由空白组成的行——对缩进没有明显的影响。从确定行集中排除空行可以避免算法的第 4 步失败。)

  3. 如果单行列表中的最后一行(即带有结束分隔符的行)为_空白_,则将其添加到确定行集中。 (结束分隔符的缩进应该影响整个内容的缩进——一个重要的尾随行策略。)

  4. 通过计算每行上前导空白字符的数量并取最小计数,计算确定行集的_公共空白前缀。_

  5. 从各个行列表中的每个_非空_行中删除公共空格前缀。

  6. 从步骤 5 中修改的单行列表中的所有行中删除所有尾随空白。此步骤会折叠修改列表中的全空白行,使它们为空,但不会丢弃它们。

  7. 通过连接步骤 6 中修改后的单行列表中的所有行来构造结果字符串,并使用 LF 作为行之间的分隔符。如果步骤 6 中的列表中的最后一行为空,则前一行中的连接 LF 将是结果字符串中的最后一个字符。

算法_不_解释转义序列\b(退格键)、\t(制表符)和\s(空格键) ;转义处理稍后发生。同样,转义序列不会阻止行终止符上的行分割,因为在转义处理之前该序列被视为两个单独的字符。\<line-terminator>

重新缩进算法将在_Java 语言规范_中成为规范。开发人员可以通过String::stripIndent新的实例方法来访问它。

重大尾随政策

通常,可以通过两种方式格式化文本块:首先,将内容的左边缘放置在第一个"开始分隔符的下方,其次,将结束分隔符放在其自己的行上,以恰好显示在开始分隔符的下方。生成的字符串在任何行的开头都没有空格,并且不包含结束分隔符的尾随空白行。

但是,由于尾部空白行被视为_确定行_,因此将其向左移动可以减少公共空白前缀,从而减少从每行开头剥离的空白量。在极端情况下,结束分隔符一直向左移动,这会将公共空白前缀减少到零,从而有效地选择退出空白剥离。

例如,当结束分隔符一直向左移动时,就不会出现用点来可视化的附带空白:

String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";

包括带有结束分隔符的尾随空白行,公共空白前缀为零,因此从每行的开头删除零空白。该算法因此产生:(用于|可视化左边距)

|              <html>
| <body>
| <p>Hello, world</p>
| </body>
| </html>

或者,假设结束分隔符没有一直向左移动,而是在 of 下方,t因此html它比变量声明深八个空格:

String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";

用点可视化的空间被认为是偶然的:

String html = """
........ <html>
........ <body>
........ <p>Hello, world</p>
........ </body>
........ </html>
........""";

包括带有结束分隔符的尾随空白行,常见的空格前缀为 8 个,因此从每行的开头删除了 8 个空格。因此,该算法保留了内容相对于结束分隔符的基本缩进:

|      <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. 转义序列

内容重新缩进后,内容中的任何_转义序列_都会被解释。文本块支持字符串文字中支持的所有转义序列,包括\n\t\'\"\\。有关完整列表,请参阅_《Java 语言规范》_的第 3.10.6 节。开发人员将可以通过新的实例方法访问转义处理。String::translateEscapes

将转义解释为最后一步允许开发人员使用\n\f\r进行字符串的垂直格式化,而不会影响步骤 1 中行终止符的翻译,并使用\b\t进行字符串的水平格式化,而不影响删除附带的空格在步骤 2 中。例如,考虑包含\r转义序列 (CR) 的文本块:

String html = """
<html>\r
<body>\r
<p>Hello, world</p>\r
</body>\r
</html>\r
""";

在行终止符标准化为 LF 之前,不会处理 CR 转义符。使用 Unicode 转义符可视化 LF ( \u000A) 和 CR ( \u000D),结果是:

|<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."
"""; // Note the newline before the closing delimiter

String code =
"""
String empty = "";
""";

但是,由三个字符组成的序列"需要至少"转义一个字符,以避免模仿结束分隔符。 (n 个字符的序列"需要至少转义其中的一个。)紧接在结束定界符之前Math.floorDiv(n,3)使用也需要转义。"例如:

String code = 
"""
String text = \"""
A text block inside a text block
\""";
""";

String tutorial1 =
"""
A common character
in Java programs
is \"""";

String tutorial2 =
"""
The empty string literal
is formed from " characters
as follows: \"\"""";

System.out.println("""
1 "
2 ""
3 ""\"
4 ""\""
5 ""\"""
6 ""\"""\"
7 ""\"""\""
8 ""\"""\"""
9 ""\"""\"""\"
10 ""\"""\"""\""
11 ""\"""\"""\"""
12 ""\"""\"""\"""\"
""");

新的转义序列

为了更好地控制换行符和空格的处理,我们引入了两个新的转义序列。

首先,\<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.\
""";

由于字符文字和传统字符串文字不允许嵌入换行符的简单原因,\<line-terminator>转义序列仅适用于文本块。

其次,新的\s转义序列只是转换为单个空格 ( \u0020)。

转义序列直到偶然的空格剥离之后才会被翻译,因此\s可以充当栅栏来防止剥离尾随空白。\s在本示例中的每行末尾使用可保证每行恰好有六个字符长:

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

转义\s序列可用于文本块、传统字符串文字和字符文字。

文本块的串联

文本块可以用在可以使用字符串文字的任何地方。例如,文本块和字符串文字可以互换连接:

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::replaceor 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():用于翻译转义序列
  • String::formatted(Object... args):简化文本块中的值替换

备择方案

没做什么

Java 凭借需要转义换行符的字符串文字已经繁荣了 20 多年。 IDE 通过支持跨多行源代码的字符串的自动格式化和串联来减轻维护负担。该类String还发展到包含简化长字符串的处理和格式化的方法,例如将字符串呈现为行流的方法。然而,字符串是 Java 语言的基本组成部分,因此字符串文字的缺点对于广大开发人员来说是显而易见的。其他 JVM 语言在表示长度和复杂字符串方面也取得了进步。因此,毫不奇怪,多行字符串文字一直是 Java 最受欢迎的功能之一。引入低到中等复杂度的多行结构将会带来很高的回报。

允许字符串文字跨越多行

只需在现有字符串文字中允许行终止符,就可以在 Java 中引入多行字符串文字。然而,这对逃避角色的痛苦无济于事"。由于代码片段的频率,\"是 后最常出现的转义序列。\n避免"在字符串文字中转义的唯一方法是为字符串文字提供备用分隔符方案。JEP 326 (原始字符串文字)对分隔符进行了很多讨论,并且吸取的教训用于指导文本块的设计,因此破坏字符串文字的稳定性会被误导。

采用另一种语言的多字符串文字

根据布莱恩·戈茨的说法:

许多人建议 Java 应该采用 Swift 或 Rust 的多行字符串文字。然而,“只做 X 语言所做的事情”的做法本质上是不负责任的;几乎每种语言的每个特征都受到该语言的其他特征的制约。相反,游戏是学习其他语言的做事方式,评估它们选择的权衡(显式和隐式),并询问什么可以应用于我们拥有的语言的约束和我们社区内的用户期望。

对于JEP 326(原始字符串文字),我们调查了许多现代编程语言及其对多行字符串文字的支持。这些调查的结果影响了当前的提案,例如选择三个"字符作为分隔符(尽管这种选择还有其他原因)以及认识到自动缩进管理的必要性。

不要删除偶然的空白

如果 Java 引入了多行字符串文字而不支持自动删除附带的空格,那么许多开发人员会编写一个方法来自己删除它,或者游说类String包含删除方法。然而,这意味着每次在运行时实例化字符串时都会进行潜在的昂贵计算,这会降低字符串驻留的好处。让 Java 语言强制删除前导位置和尾随位置的附带空白似乎是最合适的解决方案。开发人员可以通过仔细放置结束分隔符来选择不删除前导空格。

原始字符串文字

对于JEP 326(原始字符串文字),我们采用了不同的方法来解决在不转义换行符和引号的情况下表示字符串的问题,重点关注字符串的原始性。我们现在认为这种关注是错误的,因为虽然原始字符串文字可以轻松跨越多行源代码,但在其内容中支持未转义分隔符的成本非常高。这限制了该功能在多行用例中的有效性,这是一个关键的用例,因为在 Java 程序中嵌入多行(但不是真正的原始)代码片段的频率很高。从原始性到多行性的一个很好的结果是重新关注在字符串文字、文本块和将来可能添加的相关功能之间拥有一致的转义语言。

测试

使用字符串文字来创建、驻留和操作 实例的测试String也应该复制以使用文本块。对于涉及行终止符和 EOF 的极端情况,应添加负面测试。

应添加测试以确保文本块可以嵌入 Java-in-Java、Markdown-in-Java、SQL-in-Java 和至少一种 JVM-语言-in-Java。