String 新增方法(JDK 11)
周五下午,飞翔科技的代码评审会上,白歌指着小崔的代码皱起了眉头。
if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("名称不能为空"); }"小崔,这段代码有三个问题。" 白歌边说边在投影仪上标注。
孔蓝小声问旁边的小崔:"不应该就一个空值判断吗,怎么有三个问题?"
白歌听到了,转头笑道:"第一,
null检查和trim().isEmpty()分两步,容易遗漏;第二,trim()只能处理 ASCII 空格,遇到中文全角空格就失效;第三——你们知道 JDK 11 的isBlank()一行就能搞定吗?"小崔一拍脑门:"又忘了升级 JDK 的好处!"
定义表
| 方法 | 版本 | 功能 | 返回值 | 等效旧写法 |
|---|---|---|---|---|
isBlank() | JDK 11 | 字符串为空或仅含空白字符(含 Unicode 空白) | boolean | str == null || str.trim().isEmpty() |
strip() | JDK 11 | 去除首尾空白字符(支持 Unicode) | String | trim()(但 trim 不支持全角空格) |
stripLeading() | JDK 11 | 去除开头空白字符 | String | 正则 replaceFirst("^\\s+", "") |
stripTrailing() | JDK 11 | 去除结尾空白字符 | String | 正则 replaceFirst("\\s+$", "") |
lines() | JDK 11 | 按换行符分割,返回 Stream<String> | Stream<String> | split("\\R") 或 BufferedReader.lines() |
repeat(int n) | JDK 11 | 将字符串重复 n 次拼接 | String | String.join("", Collections.nCopies(n, str)) |
Mermaid 流程图:isBlank vs isEmpty vs trim 选择决策树
Mermaid 流程图:strip 与 trim 的 Unicode 处理差异
isBlank() — 空白判断
4.1 isBlank vs isEmpty 精准对比
public class IsBlankDemo {
public static void main(String[] args) {
// === 测试用例 ===
String[] testCases = {
null, // null
"", // 空字符串
" ", // 三个 ASCII 空格
"\t\n", // 制表符 + 换行
" ", // 中文全角空格 (U+3000)
"飞翔科技", // 正常文本
" 飞翔科技 ", // 带空格文本
};
System.out.println("字符串".repeat(12) + " | isEmpty | isBlank | trim().isEmpty()");
System.out.println("-".repeat(60));
for (String s : testCases) {
String display = s == null ? "null" : "\"" + s + "\"";
boolean isEmpty = s != null && s.isEmpty();
boolean isBlank = s != null && s.isBlank();
boolean trimEmpty = s != null && s.trim().isEmpty();
System.out.printf("%-20s | %-7b | %-7b | %-17b%n",
display, isEmpty, isBlank, trimEmpty);
}
}
}
输出:
字符串 | isEmpty | isBlank | trim().isEmpty()
------------------------------------------------------------
null | false | false | false
"" | true | true | true
" " | false | true | true
"\t\n" | false | true | true
" " | false | true | false ← 注意差异!
"飞翔科技" | false | false | false
" 飞翔科技 " | false | false | false
关键发现:对于中文全角空格
" "(U+3000),trim()无法识别,trim().isEmpty()返回false;而isBlank()正确返回true。
4.2 实战:用户输入验证
public class UserInputValidation {
public static void main(String[] args) {
// === 飞翔科技用户注册表单验证 ===
String[] inputs = {
"大翔", // 合法
"", // 空字符串
" ", // 纯空格
" ", // 中文全角空格
null, // null
};
for (String name : inputs) {
try {
validateUsername(name);
System.out.println("✅ 用户名 '" + name + "' 验证通过");
} catch (IllegalArgumentException e) {
System.out.println("❌ " + e.getMessage());
}
}
}
// JDK 11 推荐写法:一行搞定
static void validateUsername(String username) {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (username.strip().length() < 2) {
throw new IllegalArgumentException("用户名至少需要2个字符");
}
}
}
输出:
✅ 用户名 '大翔' 验证通过
❌ 用户名不能为空
❌ 用户名不能为空
❌ 用户名不能为空
❌ 用户名不能为空
strip() — Unicode 安全去空白
5.1 trim vs strip 终极对决
public class StripVsTrimDemo {
public static void main(String[] args) {
// 测试用例:各种 Unicode 空白字符
String[][] testCases = {
{"ASCII空格前后", " 飞翔科技 "},
{"中文全角空格前后", "\u3000飞翔科技\u3000"},
{"不间断空格前后", "\u00A0飞翔科技\u00A0"},
{"制表符换行符", "\t\n飞翔科技\t\n"},
{"混合空白字符", " \u3000\u00A0飞翔科技\u00A0\u3000 "},
};
System.out.println("场景".repeat(8) + " | trim结果 | strip结果 | 一致?");
System.out.println("-".repeat(70));
for (String[] tc : testCases) {
String scene = tc[0];
String input = tc[1];
String trimmed = input.trim();
String stripped = input.strip();
boolean same = trimmed.equals(stripped);
System.out.printf("%-20s | \"%s\" | \"%s\" | %s%n",
scene, trimmed, stripped, same);
}
}
}
输出:
场景场景场景场景场景场景场景场景 | trim结果 | strip结果 | 一致?
----------------------------------------------------------------------
ASCII空格前后 | "飞翔科技" | "飞翔科技" | true
中文全角空格前后 | " 飞翔科技 " | "飞翔科技" | false ← trim 失效!
不间断空格前后 | " 飞翔科技 " | "飞翔科技" | false ← trim 失效!
制表符换行符 | "飞翔科技" | "飞翔科技" | true
混合空白字符 | " 飞翔科技 " | "飞翔科技" | false ← trim 失效!
白歌总结:只要你处理的文本可能包含中文(如用户输入、网页爬虫内容),请用
strip()替换trim(),不要再写trim()了。
5.2 stripLeading / stripTrailing 定向清除
public class StripDirectionalDemo {
public static void main(String[] args) {
String log = " 2026-06-14 [INFO] 飞翔科技服务器启动成功 ";
System.out.println("原始: [" + log + "]");
System.out.println("strip: [" + log.strip() + "]");
System.out.println("leading:[" + log.stripLeading() + "]");
System.out.println("trailing:[" + log.stripTrailing() + "]");
// === 实战:日志格式化 ===
String rawLog = "\t\tERROR: 数据库连接超时\n";
String cleanLog = rawLog.stripLeading().stripTrailing();
System.out.println("\n清理后: [" + cleanLog + "]");
// 只去前导空白(保留尾部缩进格式)
String code = " int a = 1;\n";
String unindented = code.stripLeading();
System.out.println("去前导缩进: [" + unindented + "]");
}
}
输出:
原始: [ 2026-06-14 [INFO] 飞翔科技服务器启动成功 ]
strip: [2026-06-14 [INFO] 飞翔科技服务器启动成功]
leading:[2026-06-14 [INFO] 飞翔科技服务器启动成功 ]
trailing:[ 2026-06-14 [INFO] 飞翔科技服务器启动成功]
清理后: [ERROR: 数据库连接超时]
去前导缩进: [int a = 1;
]
lines() — 按行分割为 Stream
import java.util.stream.Collectors;
public class LinesDemo {
public static void main(String[] args) {
String multiline = "大翔\n白歌\n小崔\n孔蓝\n黄俪";
// === JDK 11 lines():直接返回 Stream ===
System.out.println("=== 逐行打印 ===");
multiline.lines().forEach(line -> System.out.println(" → " + line));
// 链式流操作
System.out.println("\n=== 筛选两字姓名 ===");
var twoCharNames = multiline.lines()
.filter(name -> name.strip().length() == 2)
.collect(Collectors.joining(", "));
System.out.println(twoCharNames);
// === 对比 JDK 8 的 split ===
// split("\\n") 无法处理 \r\n,split("\\R") 可处理但返回数组
String[] lines = multiline.split("\\R");
System.out.println("\n=== split 方式(需要转数组再转流) ===");
for (String line : lines) {
System.out.println(" → " + line);
}
// === 实战:解析飞翔科技员工名单 CSV ===
String csv = "编号,姓名,部门\n1001,大翔,技术部\n1002,白歌,技术部\n1003,Frank,市场部";
var techDept = csv.lines()
.skip(1) // 跳过表头
.map(line -> line.split(","))
.filter(parts -> parts[2].equals("技术部"))
.map(parts -> parts[1])
.collect(Collectors.joining(", "));
System.out.println("\n技术部员工: " + techDept);
}
}
输出:
=== 逐行打印 ===
→ 大翔
→ 白歌
→ 小崔
→ 孔蓝
→ 黄俪
=== 筛选两字姓名 ===
大翔, 白歌, 小崔, 孔蓝, 黄俪
=== split 方式(需要转数组再转流) ===
→ 大翔
→ 白歌
→ 小崔
→ 孔蓝
→ 黄俪
技术部员工: 大翔, 白歌
repeat() — 字符串重复
public class RepeatDemo {
public static void main(String[] args) {
// === 基础用法 ===
String star = "*";
System.out.println("5个星号: " + star.repeat(5)); // *****
// === 分隔线生成 ===
String separator = "─".repeat(40);
System.out.println(separator);
// === 实战:飞翔科技报表格式化 ===
printReport("飞翔科技2026年Q2部门绩效",
new String[][]{
{"技术部", "95"},
{"市场部", "88"},
{"运维部", "92"},
}
);
// === 边界情况 ===
System.out.println("\n重复0次: [" + "X".repeat(0) + "]"); // ""(空字符串)
// "X".repeat(-1); // ❌ IllegalArgumentException: count is negative
}
static void printReport(String title, String[][] data) {
int width = 40;
System.out.println("=" .repeat(width));
System.out.println(" " + title);
System.out.println("=" .repeat(width));
for (String[] row : data) {
String dept = row[0];
String score = row[1];
int barLen = Integer.parseInt(score) / 5; // 按比例
System.out.printf(" %-6s │ %s %s%n",
dept,
"█".repeat(barLen),
score
);
}
System.out.println("─".repeat(width));
}
}
输出:
5个星号: *****
────────────────────────────────────────
========================================
飞翔科技2026年Q2部门绩效
========================================
技术部 │ ███████████████████ 95
市场部 │ █████████████████ 88
运维部 │ ██████████████████ 92
────────────────────────────────────────
重复0次: []
易错场景
8.1 isBlank 不能替代 null 检查
public class IsBlankNullPitfall {
public static void main(String[] args) {
String name = null;
// ❌ NullPointerException:null.isBlank()
// boolean blank = name.isBlank();
// ✅ 完整模式:先判 null 再判 blank
boolean isValid = name != null && !name.isBlank();
// ✅ 或者用 Optional(JDK 8+)
boolean isValid2 = java.util.Optional.ofNullable(name)
.filter(s -> !s.isBlank())
.isPresent();
System.out.println("isValid: " + isValid);
System.out.println("isValid2: " + isValid2);
}
}
8.2 lines() 不会自动去掉尾随空行
import java.util.stream.Collectors;
public class LinesTrailingPitfall {
public static void main(String[] args) {
// 注意最后一行有换行符
String text = "大翔\n白歌\n";
long count = text.lines().count();
System.out.println("行数: " + count); // 2 —— 最后的空行不算
// lines() 的行为:如果以换行符结尾,最后的空字符串不计入
// 但如果不以换行符结尾...
String text2 = "大翔\n白歌"; // 最后没有 \n
long count2 = text2.lines().count();
System.out.println("行数2: " + count2); // 2 —— 完全等价
}
}
面试考点
问题一:"isBlank() 和 isEmpty() 的区别?"
isEmpty()只判断length() == 0(仅空字符串""返回 true);isBlank()判断字符串为空或仅包含空白字符(空格、制表符、换行符、全角空格等 Unicode 空白)。" ".isEmpty()为 false," ".isBlank()为 true。
问题二:"strip() 和 trim() 的区别?为什么建议用 strip?"
trim()定义于 JDK 1.0,只去除 ≤ U+0020 的字符(ASCII 空格、制表、换行等),无法处理中文全角空格(U+3000)、不间断空格(U+00A0)等。strip()基于Character.isWhitespace()定义,覆盖全部 Unicode 空白。在处理国际化文本时必须使用strip()。
问题三:"lines() 返回的 Stream 是惰性的吗?"
是的,
lines()返回一个惰性求值的Stream<String>,按需从字符串中提取每一行。这与split("\\R")不同——split会一次性拆分成数组,消耗额外内存。
问题四:"repeat(0) 和 repeat(-1) 分别返回什么?"
repeat(0)返回空字符串""。repeat(-1)抛出IllegalArgumentException,因为 count 不能为负数。
黄俪(前端)旁听了这场代码评审,会后跟孔蓝说:"原来 JDK 11 的 String 方法这么实用。
isBlank()处理表单验证、strip()处理用户输入清理、repeat()生成前端模板——每个方法都能少写好几行代码。"白歌最后总结:"String 新方法没有一个是'高大上'的特性,但它们解决了 JDK 八年以来积累的最常见的编码痛点。把
trim()换成strip(),把isEmpty()的判断换成isBlank()——这些微小的改变,就是你从 JDK 8 程序员升级到 JDK 11 程序员的第一步。"