乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)

周五下午,黄俪从前端组冲进了后端办公区,手里举着一张打印出来的 JSON 字符串。

"你们后端返回的这个错误响应,我在 Chrome DevTools 里看到是正常的,但为什么你们的单元测试里写成了这副模样?"她把纸拍在桌上——上面是一行超过 200 个字符的 Java 字符串拼接,\" 和 \n 交织在一起,比天书还难懂。

小崔尴尬地挠了挠头:"那是老代码……每次改 SQL 或者 JSON 模板我都要数转义符。"

白歌不紧不慢地打开 IDEA,敲下三个双引号:"JDK 15 的文本块,一个特性消灭所有字符串转义地狱。黄俪你看着——"


定义表

概念描述
文本块(Text Block)JDK 15 正式特性,使用 """...""" 三引号语法定义多行字符串,自动处理缩进和转义
公共前导空格(Incidental White Space)编译器自动去除每行共有的前导空格,以内容行的最小缩进量为基准
续行符 \行尾的反斜杠,阻止该行的换行符被包含到字符串中,将多行拼接为一行
强制空格 \s在行尾强制保留一个空格字符,防止编译器自动修剪尾随空格
String.formatted()JDK 15 新增实例方法,等价于 String.format(format, args),支持文本块的参数化

传统多行字符串的痛点


基础用法:三引号定义多行字符串

public class TextBlockBasic {
    public static void main(String[] args) {
        // === 传统写法:拼接 + 转义地狱 ===
        String oldJson = "{\n" +
                "  \"company\": \"飞翔科技\",\n" +
                "  \"founded\": 2020,\n" +
                "  \"employees\": [\"大翔\", \"白歌\", \"小崔\"]\n" +
                "}";
        System.out.println("=== 传统写法 ===");
        System.out.println(oldJson);

        // === JDK 15 文本块:所见即所得 ===
        String newJson = """
                {
                  "company": "飞翔科技",
                  "founded": 2020,
                  "employees": ["大翔", "白歌", "小崔"]
                }""";
        System.out.println("\n=== 文本块写法 ===");
        System.out.println(newJson);
    }
}

输出:

=== 传统写法 ===
{
  "company": "飞翔科技",
  "founded": 2020,
  "employees": ["大翔", "白歌", "小崔"]
}

=== 文本块写法 ===
{
  "company": "飞翔科技",
  "founded": 2020,
  "employees": ["大翔", "白歌", "小崔"]
}

进阶用法:缩进控制与特殊字符

自动去除公共前导空格

public class TextBlockIndent {
    public static void main(String[] args) {
        // 尽管文本块在代码中缩进了 3 层(类 → 方法 → 代码块),
        // 编译器自动去除公共前导空格,输出格式保持整洁
        String html = """
                <html>
                  <body>
                    <h1>飞翔科技 - 员工管理系统</h1>
                    <p>当前在线:12人</p>
                  </body>
                </html>""";

        System.out.println(html);
        System.out.println("---分隔线---");

        // === 控制最终缩进:关闭引号的位置决定基准线 ===
        String htmlIndented = """
                <html>
                  <body>
                    <h1>飞翔科技</h1>
                  </body>
                </html>
                """;  // 注意:关闭 """ 左移了一级缩进
        // 此时基准线是关闭 """ 的位置,输出每行前面会多一级缩进
        System.out.println(htmlIndented);
    }
}

输出:

<html>
  <body>
    <h1>飞翔科技 - 员工管理系统</h1>
    <p>当前在线:12人</p>
  </body>
</html>
---分隔线---
    <html>
      <body>
        <h1>飞翔科技</h1>
      </body>
    </html>
    

续行符 \ 与强制空格 \s

public class TextBlockLineControl {
    public static void main(String[] args) {
        // === 续行符 \:多行源码拼成一行实际字符串 ===
        String singleLine = """
                飞翔科技是一家成立于2020年的\
                互联网科技公司,致力于企业级\
                应用开发与数字化转型服务。""";
        System.out.println("续行符效果(实际是一行):");
        System.out.println(singleLine);

        // === 强制空格 \s:保留行尾空格 ===
        // 通常编译器会删除行尾空格,但用 \s 可强制保留
        String withTrailingSpace = """
                员工姓名:大翔      \s
                员工姓名:白歌      \s
                员工姓名:小崔      \s""";
        System.out.println("\n强制保留行尾空格:");
        System.out.println(withTrailingSpace);
        System.out.println("(注意上面每行末尾的空格被保留了)");
    }
}

输出:

续行符效果(实际是一行):
飞翔科技是一家成立于2020年的互联网科技公司,致力于企业级应用开发与数字化转型服务。

强制保留行尾空格:
员工姓名:大翔      
员工姓名:白歌      
员工姓名:小崔      
(注意上面每行末尾的空格被保留了)

实战场景:SQL / JSON / HTML 模板

飞翔科技项目中的 SQL 模板

public class TextBlockSQL {
    public static void main(String[] args) {
        String department = "技术部";
        int minSalary = 10000;

        // === JDK 15:文本块 + formatted() ===
        String sql = """
                SELECT e.id, e.name, e.salary, d.name AS dept_name
                FROM employees e
                JOIN departments d ON e.dept_id = d.id
                WHERE d.name = '%s'
                  AND e.salary >= %d
                ORDER BY e.salary DESC
                LIMIT 20""".formatted(department, minSalary);

        System.out.println("生成的 SQL:");
        System.out.println(sql);

        // === 传统写法对比 ===
        String oldSql = "SELECT e.id, e.name, e.salary, d.name AS dept_name\n" +
                "FROM employees e\n" +
                "JOIN departments d ON e.dept_id = d.id\n" +
                "WHERE d.name = '" + department + "'\n" +
                "  AND e.salary >= " + minSalary + "\n" +
                "ORDER BY e.salary DESC\n" +
                "LIMIT 20";
        System.out.println("\n=== 传统写法(同样功能,三倍的维护成本)===");
        System.out.println(oldSql);
    }
}

输出:

生成的 SQL:
SELECT e.id, e.name, e.salary, d.name AS dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.name = '技术部'
  AND e.salary >= 10000
ORDER BY e.salary DESC
LIMIT 20

=== 传统写法(同样功能,三倍的维护成本)===
SELECT e.id, e.name, e.salary, d.name AS dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.name = '技术部'
  AND e.salary >= 10000
ORDER BY e.salary DESC
LIMIT 20

文本块与 String 方法的协同

文本块返回的仍然是普通的 String 对象,可以无缝使用 JDK 11 引入的 lines()、strip() 等方法以及 JDK 12/15 新增的方法。

import java.util.stream.Collectors;

public class TextBlockStringMethods {
    public static void main(String[] args) {
        String employeeData = """
                大翔,CTO,35000
                白歌,架构师,30000
                小崔,后端开发,12000
                孔蓝,后端开发,15000
                黄俪,前端开发,14000
                李眉,运维工程师,13000""";

        // === lines() 按行分割 + Stream 处理 ===
        System.out.println("=== 薪资超过 15000 的员工 ===");
        employeeData.lines()
                .filter(line -> {
                    String[] parts = line.split(",");
                    return Integer.parseInt(parts[2]) > 15000;
                })
                .forEach(System.out::println);

        // === stripIndent() 手动去除缩进 ===
        String indented = "    飞翔科技\n    技术部\n    2020年成立";
        System.out.println("\n=== stripIndent() 效果 ===");
        System.out.println(indented.stripIndent());

        // === translateEscapes() 处理转义序列 ===
        String escaped = "飞翔\\n科技\\t技术部";
        System.out.println("\n=== translateEscapes() 效果 ===");
        System.out.println("原文: " + escaped);
        System.out.println("翻译后: " + escaped.translateEscapes());
    }
}

输出:

=== 薪资超过 15000 的员工 ===
大翔,CTO,35000
白歌,架构师,30000

=== stripIndent() 效果 ===
飞翔科技
技术部
2020年成立

=== translateEscapes() 效果 ===
原文: 飞翔\n科技\t技术部
翻译后: 飞翔
科技	技术部

易错场景

文本块开头不能紧跟内容

public class TextBlockStartPitfall {
    public static void main(String[] args) {
        // ❌ 编译错误:文本块内容必须从下一行开始
        // String bad = """Hello""";

        // ✅ 正确:第一行留空,从下一行开始
        String good = """
                Hello""";
        System.out.println(good);
    }
}

输出:

Hello

三引号内部的引号不需要转义

public class TextBlockQuotePitfall {
    public static void main(String[] args) {
        // === 文本块内包含双引号,无需转义 ===
        String json = """
                {
                  "message": "白歌说:"文本块消灭了转义。""
                }""";
        System.out.println(json);

        // 但连续三个双引号仍需要至少转义第一个
        String withTripleQuote = """
                白歌在代码注释中写道:\"""文本块是 JDK 15 最好的特性。\""" """;
        System.out.println(withTripleQuote);
    }
}

输出:

{
  "message": "白歌说:"文本块消灭了转义。""
}
白歌在代码注释中写道:"""文本块是 JDK 15 最好的特性。""" 

行尾空格默认被删除

public class TextBlockTrailingSpacePitfall {
    public static void main(String[] args) {
        // === 行尾空格默认被修剪 ===
        String withoutSlashS = """
                姓名:大翔      
                姓名:白歌      """;
        // 每行末尾的空格被自动去除了

        // === 加 \s 强制保留 ===
        String withSlashS = """
                姓名:大翔      \s
                姓名:白歌      \s""";

        System.out.println("不加 \\s 的长度:" + withoutSlashS.length());
        System.out.println("加了 \\s 的长度:" + withSlashS.length());
    }
}

输出:

不加 \s 的长度:12
加了 \s 的长度:21

面试考点

面试官常问的三个问题:

问题一:"文本块和普通字符串有本质区别吗?"

答案:没有本质区别。文本块编译后就是一个普通的 String 对象,存放在常量池中,遵循相同的 equals() 和 hashCode() 规则。"""...""" 是编译期的语法糖——编译器负责去除公共前导空格、处理续行符 \ 和强制空格 \s,最终生成与手写拼接完全等价的字符串实例。用 == 比较两个内容相同的文本块,如果 JVM 将它们放入常量池则返回 true。

问题二:"String.formatted() 和 String.format() 有什么区别?"

答案:功能等价,调用方式不同。String.format(format, args...) 是静态方法,format.formatted(args...) 是 JDK 15 新增的实例方法。formatted() 更适合文本块的链式风格:"""...""".formatted(args),读起来更自然。底层实现上 formatted() 内部直接调用 String.format(this, args)。

问题三:"stripIndent() 和文本块的自动缩进去除有什么区别?"

答案:文本块的缩进去除是编译期完成的——编译器在编译时计算公共前导空格并去除。而 stripIndent() 是运行时方法,可以手动处理任意字符串的缩进。stripIndent() 使用与文本块相同的算法(以内容行的最小缩进量为基准),常用于处理从文件或网络读取的多行字符串。JDK 15 引入这个方法正是为了文本块生态的完整性。


黄俪在团队 Wiki 上更新了前端后端的接口文档模板:"现在所有 REST API 的请求/响应示例都用文本块写,新人再也不用来问我'这段 JSON 的对齐怎么调'了。白歌说得对——格式自由,才是真正的开发自由。"

上一页
Switch 表达式(JDK 12 预览 / JDK 14 正式)
下一页
Records 记录类(JDK 14 预览 / JDK 16 正式)