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

    • 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)

Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)

周五下午四点,李眉在运维群里发了一张截图——生产环境刚抛了一个 NullPointerException,堆栈只有短短三行,没有任何线索指向到底是哪个变量为 null。

"又是 NPE!"小崔哀嚎,"这段代码有六层链式调用,我根本不知道 a.b.c.d().e.f 哪一步爆了。"

白歌在群里回复:"从 JDK 14 开始,升级你的 JDK,JVM 会精确告诉你哪个变量是 null。另外 JDK 12 和 JDK 15 还给 String 加了几个让你写代码更舒服的方法——indent、transform、formatted——今天就一并说了。"


上半部分:Helpful NullPointerExceptions(JDK 14)

定义表

概念描述
Helpful NPE(有帮助的空指针异常)JDK 14 正式特性,JVM 在抛出 NullPointerException 时精确定位哪个变量或表达式为 null
精确消息(Precise Message)NPE 消息中指明变量名或表达式片段,例如 Cannot invoke "String.length()" because "name" is null
链式定位对于 a.b.c.d() 这样的链式调用,JVM 精确定位到中间哪个引用为 null
-XX:+ShowCodeDetailsInExceptionMessagesJVM 参数,控制是否启用详细的 NPE 消息(JDK 14 默认开启)

传统 NPE vs Helpful NPE


Helpful NPE 实战

public class HelpfulNPEDemo {
    // 飞翔科技员工层级
    static class Employee {
        String name;
        Department department;
        Employee(String name, Department department) {
            this.name = name;
            this.department = department;
        }
    }

    static class Department {
        String name;
        Manager manager;
        Department(String name, Manager manager) {
            this.name = name;
            this.manager = manager;
        }
    }

    static class Manager {
        String name;
        String phone;
        Manager(String name, String phone) {
            this.name = name;
            this.phone = phone;
        }
    }

    public static void main(String[] args) {
        // === 场景一:链式调用中精确定位 ===
        Manager m1 = new Manager("大翔", "138-0000-0001");
        Department dept = new Department("技术部", m1);
        Employee e = new Employee("小崔", dept);

        // 正常调用
        System.out.println("经理电话: " + e.department.manager.phone);

        // === 场景二:NPE 精确定位 ===
        Manager m2 = new Manager("白歌", null);  // phone 为 null
        Department dept2 = new Department("架构组", m2);
        Employee e2 = new Employee("孔蓝", dept2);

        try {
            // phone 为 null 时调用 length()
            System.out.println("电话长度: " + e2.department.manager.phone.length());
        } catch (NullPointerException ex) {
            System.out.println("=== JDK 14 Helpful NPE 消息 ===");
            System.out.println(ex.getMessage());
        }

        // === 场景三:中间引用为 null ===
        Department dept3 = new Department("测试部", null);  // manager 为 null
        Employee e3 = new Employee("实习生", dept3);

        try {
            System.out.println("经理姓名: " + e3.department.manager.name);
        } catch (NullPointerException ex) {
            System.out.println("\n=== 精确定位 manager 为 null ===");
            System.out.println(ex.getMessage());
        }
    }
}

输出:

经理电话: 138-0000-0001
=== JDK 14 Helpful NPE 消息 ===
Cannot invoke "String.length()" because "e2.department.manager.phone" is null

=== 精确定位 manager 为 null ===
Cannot invoke "SealedOrderSystem$Manager.name()" because "e3.department.manager" is null

下半部分:String 新增方法(JDK 12 / JDK 15)

定义表

概念描述JDK 版本
indent(int n)调整每行缩进:正数为添加 n 个空格,负数为去除最多 n 个前导空格,自动添加行尾换行符JDK 12
transform(Function)将字符串传递给指定函数进行链式转换,返回任意类型JDK 12
stripIndent()去除公共前导空格(与文本块使用相同算法),文本块的核心配套方法JDK 15
translateEscapes()将字符串中的转义序列(\n、\t 等)翻译为实际字符JDK 15
formatted(Object...)等价于 String.format(format, args) 的实例方法版本,支持链式调用JDK 15

indent():一行控制多行缩进

import java.util.stream.Collectors;

public class StringIndentDemo {
    public static void main(String[] args) {
        String original = "飞翔科技\n技术部\n2020年成立";

        // indent(4):每行前面加 4 个空格
        System.out.println("=== indent(4) 效果 ===");
        System.out.println(original.indent(4));

        // indent(-2):每行去除最多 2 个前导空格
        String indented = "    飞翔科技\n    技术部";
        System.out.println("=== indent(-2) 效果 ===");
        System.out.println(indented.indent(-2));

        // 配合 Stream 处理多行文本
        String employeeList = "大翔\n白歌\n小崔\n孔蓝";
        String numbered = employeeList.lines()
                .map(line -> "  * " + line)
                .collect(Collectors.joining("\n"));
        System.out.println("=== indent 配合 Stream ===");
        System.out.println(numbered);
    }
}

输出:

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

=== indent(-2) 效果 ===
  飞翔科技
  技术部

=== indent 配合 Stream ===
  * 大翔
  * 白歌
  * 小崔
  * 孔蓝

transform():自定义链式转换

import java.util.Arrays;
import java.util.List;

public class StringTransformDemo {
    public static void main(String[] args) {
        // === transform 实现链式转换 ===
        String csv = "大翔,白歌,小崔,孔蓝";

        List<String> employees = csv
                .transform(String::toUpperCase)    // 先转大写
                .transform(s -> s.split(","))      // 再按逗号分割
                .transform(Arrays::asList);        // 转为 List

        System.out.println("转换结果: " + employees);

        // === 飞翔科技实战:API 响应处理 ===
        String apiResponse = """
                {
                  "code": 200,
                  "data": ["大翔", "白歌", "小崔"]
                }""";

        // transform 让处理链更清晰
        String summary = apiResponse
                .transform(s -> s.contains("200") ? "成功" : "失败")
                .transform(s -> "API 调用状态: " + s);

        System.out.println(summary);

        // === 与 formatted 和 strip 的链式组合 ===
        String template = "员工: %s | 部门: %s | 薪资: %d";
        String result = template
                .formatted("大翔", "技术部", 35000)   // 参数填充
                .transform(String::toUpperCase);        // 转大写
        System.out.println(result);
    }
}

输出:

转换结果: [大翔, 白歌, 小崔, 孔蓝]
API 调用状态: 成功
员工: 大翔 | 部门: 技术部 | 薪资: 35000

translateEscapes():转义序列实测

public class StringTranslateEscapesDemo {
    public static void main(String[] args) {
        // === 从配置文件读取的转义字符串 ===
        String configValue = "技术部\\n大翔\\tCTO\\n白歌\\t架构师";

        System.out.println("=== 翻译前(一字不差) ===");
        System.out.println(configValue);

        System.out.println("\n=== translateEscapes() 后 ===");
        System.out.println(configValue.translateEscapes());

        // === 飞翔科技:日志模板处理 ===
        String logTemplate = "错误信息:\\n\\t文件路径:/data/logs/app.log\\n\\t错误码:500";
        System.out.println("\n=== 日志模板翻译 ===");
        System.out.println(logTemplate.translateEscapes());
    }
}

输出:

=== 翻译前(一字不差) ===
技术部\n大翔\tCTO\n白歌\t架构师

=== translateEscapes() 后 ===
技术部
大翔	CTO
白歌	架构师

=== 日志模板翻译 ===
错误信息:
	文件路径:/data/logs/app.log
	错误码:500

易错场景

indent() 会自动添加行尾换行符

public class StringIndentPitfall {
    public static void main(String[] args) {
        String name = "飞翔科技";

        // indent(0) 看起来什么都不做……
        String result = name.indent(0);

        // 但实际上它加了一个换行符!
        System.out.println("原始长度: " + name.length());
        System.out.println("indent(0) 后长度: " + result.length());
        System.out.println("末尾有换行符?" + result.endsWith("\n"));
    }
}

输出:

原始长度: 4
indent(0) 后长度: 5
末尾有换行符?true

transform 不能处理 null

public class StringTransformNullPitfall {
    public static void main(String[] args) {
        String name = null;

        try {
            // ❌ 空指针异常:null 不能调用实例方法
            String result = name.transform(String::toUpperCase);
        } catch (NullPointerException e) {
            System.out.println("NPE: " + e.getMessage());
        }

        // ✅ 防御性写法:先判空
        String name2 = null;
        String result = (name2 == null) ? "未知" : name2.transform(String::toUpperCase);
        System.out.println("防御性结果: " + result);
    }
}

输出:

NPE: Cannot invoke "String.transform(java.util.function.Function)" because "name" is null
防御性结果: 未知

translateEscapes 只处理字面量转义序列

public class StringTranslateEscapesPitfall {
    public static void main(String[] args) {
        // === 情况一:Java 编译器已经处理过的转义 ===
        String alreadyEscaped = "飞翔\n科技";  // 编译后就是一个换行符
        System.out.println("已转义的字符串:");
        System.out.println(alreadyEscaped);
        System.out.println("translateEscapes 对它无效(已经是真正的换行符了)");
        System.out.println(alreadyEscaped.translateEscapes());

        // === 情况二:字面量反斜杠(从文件/网络读入的) ===
        String literal = "飞翔\\n科技";  // 反斜杠n是两个字面量字符
        System.out.println("\n字面量转义字符串:");
        System.out.println("翻译前: " + literal);
        System.out.println("翻译后: " + literal.translateEscapes());
    }
}

输出:

已转义的字符串:
飞翔
科技
translateEscapes 对它无效(已经是真正的换行符了)
飞翔
科技

字面量转义字符串:
翻译前: 飞翔\n科技
翻译后: 飞翔
科技

面试考点

面试官常问的三个问题:

问题一:"Helpful NPE 需要什么配置才能启用?对性能有影响吗?"

答案:JDK 14 起默认启用,无需任何配置。如需禁用,添加 JVM 参数 -XX:-ShowCodeDetailsInExceptionMessages。性能上,Helpful NPE 只在 NPE 真正抛出时才计算详细消息,正常路径零开销。消息计算额外消耗极小(仅跟踪变量名或表达式片段),对生产环境影响可以忽略不计。这是少有的"零配置、零开销、纯粹收益"的 JDK 特性。

问题二:"indent(n) 返回的是新字符串吗?n 为负数会怎样?"

答案:是的,String 不可变,indent() 始终返回新字符串。n 为正数时每行前面加 n 个空格;n 为负数时每行最多删除 n 个前导空格(不会删除非空格字符)。无论 n 是多少,indent() 都会在末尾添加一个换行符 \n(除非输入已经是空字符串)。这意味着 "".indent(0) 返回 "\n",这是很多人踩过的坑。

问题三:"formatted() 和 String.format() 应该在什么场景下分别使用?"

答案:功能完全等价,选择取决于代码风格。静态方法 String.format() 适合独立调用或参数复杂时的多行格式化。实例方法 formatted() 更适合链式调用风格——特别是与文本块配合:"""...""".formatted(args).transform(...)。JDK 团队添加 formatted() 的主要动机就是支持文本块的流畅 API 风格。


李眉的运维笔记:"升级到 JDK 17 后,生产环境的 NPE 平均排查时间从 25 分钟降低到 3 分钟。以前拿到 NPE 堆栈要逐行猜测哪个变量为空,现在 JVM 直接告诉你答案。白歌说这是'JDK 14 最有温度的特性'——它解决的问题不炫,但每个 Java 开发者每天都在为此浪费时间。"

上一页
Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
下一页
Stream 增强(JDK 12 / JDK 16)