文本块和新的 String API (Java 15+)
1. 概述
在 Java 15 之前,在 Java 代码中编写多行的 JSON、SQL 或 HTML 相当麻烦。你必须用 + 拼接字符串、添加 \n 并对每个双引号进行转义。文本块(从 Java 15 正式推出,JEP 378)解决了这个问题。随之而来的是 String 中的几个新方法。
读完本文,你将能正确编写文本块的缩进,并使用 \ 和 \s 转义序列。你还将了解何时使用 formatted()、stripIndent()、translateEscapes() 以及一些补充 API 直到 Java 21。
2. 准备
所有示例运行在 Java 21 上。测试部分使用 JUnit 5 和 AssertJ:
<properties>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
如果你使用 Spring Boot 3.x,spring-boot-starter-test 已经包含了这两个库。
3. 文本块基础
3.1. 语法
文本块以三个双引号 """ 开始,后面紧接着换行。它以 """ 结束:
public class TextBlockDemo {
public static void main(String[] args) {
String json = """
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
""";
System.out.print(json);
}
}
输出结果:
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
上面的代码有几个值得注意的地方:
- 内部的
"不需要转义。 - 每行开头的共同缩进部分被移除。这称为附属空白。相对缩进,如
"id"前的 2 个空格,仍然被保留。 - 因为
"""关闭符在单独的一行,字符串以\n结尾。
文本块就是一个普通的 String。它在编译时完全处理,所以运行时没有额外开销。
注意: 不能在
"""开始后立即写内容。String s = """Hello""";会报编译错误:illegal text block open delimiter sequence。
3.2. 关闭 """ 的位置决定缩进
编译器找出开头空白最少的一行,包括含有 """ 关闭符的那一行。然后它从所有行中移除恰好那么多的空白。因此,你可以通过将 """ 关闭符向左移动来主动增加缩进:
String html = """
<ul>
<li>Order #1</li>
</ul>
""";
System.out.print(html);
<ul>
<li>Order #1</li>
</ul>
""" 关闭符比内容少缩进 4 列,所以每行保留 4 个空格在开头。
但是如果你不想要字符串末尾的 \n 字符,把 """ 放在内容后面紧接着:
String greeting = """
Xin chào Lan""";
System.out.println("[" + greeting + "]");
[Xin chào Lan]
3.3. 行尾空白和换行符
还有两条你需要了解的规则:
- 行尾的空白始终被移除。
- 所有类型的换行符(
\r\n、\r)都被规范化为\n。即使源文件使用 Windows 行尾也是如此。
规则 2 有助于在操作系统间获得一致的结果。但如果你的协议要求使用 \r\n,例如 HTTP 头或遵循 RFC 4180 的 CSV 文件,你需要自己替换换行符。
4. 两个新的转义序列:\<换行> 和 \s
4.1. 用 \ 连接行
长的 SQL 语句通常为了易读性被写成多行。但有时你又需要它在一行上,比如在记录日志时。行尾的 \ 会阻止换行符:
String sql = """
SELECT id, total \
FROM orders \
WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'
\ 之前的空格被保留了。原因是它不再位于行尾:\ 字符在它后面。
4.2. 用 \s 保留空白
\s 相当于一个空格(U+0020)。你用它来保留行尾的空白,这些空白通常会被删除,例如当创建固定宽度的数据时:
String report = """
Lan \s
Minh \s
""";
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan ]
[Minh ]
每行恰好 8 个字符长。\s 之前的四个空格被保留,因为它们不再位于行尾。
4.3. 当内容中有 """ 时
连续两个 " 没问题。三个连续的 " 会被理解为文本块的结束符,所以你需要转义至少一个:
String doc = """
Python docstring dùng \""" để mở và đóng.
""";
5. 配套的新 String API
Java 15 为 String 添加了三个方法来支持文本块。这些也是编译器处理文本块时执行的步骤。
5.1. formatted()
formatted(Object... args) 的工作方式类似 String.format(this, args),但你可以直接在字符串上调用。这种写法特别适合文本块:
record Order(long id, String customer, double total) {}
Order order = new Order(42, "Lan", 199_000);
String message = """
订单 #%d
客户: %s
总价: %,.0f VND
""".formatted(order.id(), order.customer(), order.total());
System.out.print(message);
订单 #42
客户: Lan
总价: 199,000 VND
千位分隔符取决于 JVM 的默认 Locale。如果需要固定结果,使用 String.format(Locale.US, ...),因为 formatted() 不接受 Locale 参数。
5.2. stripIndent()
此方法将文本块的去附属空白算法应用于任意字符串。当多行字符串来自文件或数据库而不是源代码时,它很有用:
String raw = " SELECT *\n FROM customers\n ";
System.out.print(raw.stripIndent());
SELECT *
FROM customers
最后一行只包含空白,所以它被计入缩进确定中,然后变成空字符串。这正是文本块中 """ 关闭符的作用。
5.3. translateEscapes()
此方法将字面形式的转义序列转换为实际字符。当从包含 \n 或 \t 字符串的配置文件读取模板时,你会需要它:
String fromConfig = "你好\\tLan\\n谢谢你的订单";
System.out.println(fromConfig.translateEscapes());
你好 Lan
谢谢你的订单
语法错误的转义,如 \q,会导致该方法抛出 IllegalArgumentException。不要在用户输入的数据上调用它而不捕获异常。另外注意:此方法不处理 \u,因为 unicode 转义已经由编译器提前翻译了。
6. 其他有用的方法(Java 11–21)
许多小方法在各个版本中被添加。它们通常与文本块一起使用:
String text = """
Lan
\s
Minh
""";
// Java 11
System.out.println(" ".isBlank()); // true
System.out.println(text.lines().filter(l -> !l.isBlank()).count()); // 2
System.out.println("\u2003Lan\u2003".strip()); // "Lan" (strip 理解 Unicode,trim 则不)
System.out.println("-".repeat(10)); // ----------
// Java 12
System.out.print("Lan\nMinh".indent(2)); // 每行增加 2 个空格,以 \n 结尾
int length = "Lan".transform(String::length); // 3
// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8,仅在 [6, 13) 中查找
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]
几个需要澄清的细节:
indent(n)如果n > 0则在每行开头添加 n 个空格,如果n < 0则移除最多 n 个空白。它总是规范化换行符并在末尾添加\n。transform(Function)允许你自然地链接字符串转换步骤,例如template.transform(this::render).transform(String::strip)。- 从 Java 21 开始,
StringBuilder也有repeat(...):new StringBuilder().repeat("=", 5)生成=====。
7. 在 Spring Boot 中使用文本块
在 Spring Boot 3.x 应用中,文本块非常适合 Spring Data JPA 的 @Query。你可以保持多行查询而不需要字符串拼接:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT o FROM Order o
WHERE o.customer.email = :email
AND o.status = 'PAID'
ORDER BY o.createdAt DESC
""")
List<Order> findPaidOrders(@Param("email") String email);
}
文本块是编译时常数,所以可以用作注解值。参数仍通过 :email 绑定,对 SQL 注入是安全的。
8. 编写测试以验证文本块行为
关于缩进和换行符的规则很容易混淆。因此,你应该编写测试来确认你期望的行为:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TextBlockTest {
@Test
void givenClosingDelimiterOnOwnLine_thenEndsWithNewline() {
String json = """
{
"id": 42
}
""";
assertThat(json).isEqualTo("{\n \"id\": 42\n}\n");
}
@Test
void givenBackslashAtLineEnd_thenLinesAreJoined() {
String sql = """
SELECT id \
FROM orders""";
assertThat(sql).isEqualTo("SELECT id FROM orders");
}
@Test
void givenSpaceEscape_thenTrailingSpacesArePreserved() {
String padded = """
Lan \s""";
assertThat(padded).hasSize(6).endsWith(" ");
}
@Test
void givenTemplate_whenFormatted_thenPlaceholdersAreReplaced() {
String message = """
你好 %s,订单 #%d 已支付。""".formatted("Lan", 42);
assertThat(message).isEqualTo("你好 Lan,订单 #42 已支付。");
}
}
四个测试都通过了。第一个测试最值得保留:它提醒你 """ 关闭符在单独的一行会在字符串末尾生成 \n。
9. 常见错误和何时不应使用
混合制表符和空格。 计算附属空白时,编译器将一个制表符视为一个空白字符,与一个空格相同。如果各行使用两种不同的缩进方式,结果会出错,尽管在 IDE 中看起来仍然对齐。配置编辑器只使用空格。
忘记末尾的 \n。 当比较字符串或将文本块连接到另一个字符串时,多余的 \n 字符很容易导致“看起来相同但 equals 返回 false”的错误。如果不需要它,将 """ 放在内容后面,或调用 strip()。
用 formatted() 将 SQL 与用户数据连接。 文本块只是使代码更易读,不会使其更安全。
// 错误:SQL 注入漏洞
String sql = """
SELECT * FROM customers WHERE email = '%s'
""".formatted(email);
使用 PreparedStatement、带 ? 参数的 JdbcTemplate,或带命名参数的 @Query。
注意: 对于 JSON,文本块适合测试数据或固定有效负载。如果有效负载取决于动态数据,使用 Jackson 来序列化对象。自行拼接字符串不会转义特殊字符,如客户名称中的
"或\。
短的单行字符串。 "Lan" 仍然比三行文本块更好。只有当内容真正有多行或包含多个 " 时才使用文本块。
模板过大。 几百行的 HTML 电子邮件在 Java 类中会使代码难以维护。在这种情况下,应该将其移到模板文件,例如使用 Thymeleaf 或 src/main/resources 中的资源。
10. 结论
文本块帮助你在 Java 中编写 JSON、SQL、HTML,使其简洁易读,就像原始形式一样。你只需要记住几条规则:
"""开始必须紧跟换行。"""关闭符的位置决定缩进以及字符串是否以\n结尾。- 行尾空白被移除。换行符始终被规范化为
\n。 - 使用
\连接行,使用\s保留空白。
关于 API,formatted() 非常适合向文本块填充值。stripIndent() 和 translateEscapes() 允许你对从外部读取的字符串应用相同的逻辑。Java 21 的 lines()、strip()、indent()、transform() 和 splitWithDelimiters() 等方法帮助你处理多行字符串而无需额外的库。最后,编写测试来确认期望的行为,不要用文本块来自己拼接 SQL 和用户数据。