9.2 字符串操作与构造
前置知识:字符串基础、数组、循环
预计时间:30 分钟
运行环境:Java 17
完成标志:能按 API 契约截取、分割、清理、替换和构造文本
订单行看着只是一段文字
一行订单包含日期、客户、物品和数量。能否正确拆分并重新构造它,取决于 API 契约:端点是否包含、分隔符是否按正则表达式解释、尾部空字段是否保留。片段里的 Arrays、Pattern、Matcher 等类型需要导入,完整示例列出了所有 import;各片段使用同名变量时应分开运行。
1. substring 使用左闭右开区间
String order = "2026-07-15|一名开发者|铁锤|2";
String date = order.substring(0, 10);
String details = order.substring(11);
System.out.println(date);
System.out.println(details);输出:
2026-07-15
一名开发者|铁锤|2substring(begin, end) 包含 begin,不包含 end,有效范围满足:
0 <= begin <= end <= text.length()越界或 begin > end 按 API 契约会抛 IndexOutOfBoundsException(常见 JDK 的具体异常是其子类 StringIndexOutOfBoundsException)。索引以 UTF-16 代码单元计数;若切点落在代理项中间,就会得到不完整 Unicode 字符。按码点定位时先用 offsetByCodePoints 计算索引。
substring 承诺返回指定区间的文本,不承诺底层数组如何分配或共享;截取整个字符串时还可能返回原对象。若排查内存占用,应检查目标 JDK 的实现与实际保留对象,不能只从子串长度推断分配量。
2. split 的第一个参数是正则
点号在默认正则模式中匹配行终止符以外的任意字符,所以不能直接写:
String ip = "192.168.1.1";
System.out.println(Arrays.toString(ip.split(".")));
System.out.println(Arrays.toString(ip.split("\\.")));输出:
[]
[192, 168, 1, 1]分隔符来自配置或用户输入时,不要自己猜需要转义哪些字符:
String delimiter = ".";
String[] parts = ip.split(Pattern.quote(delimiter));第二个参数决定切分次数与尾部空项
String row = "铁锤,2,,";
System.out.println(Arrays.toString(row.split(",")));
System.out.println(Arrays.toString(row.split(",", -1)));
System.out.println(Arrays.toString(row.split(",", 3)));输出:
[铁锤, 2]
[铁锤, 2, , ]
[铁锤, 2, ,]- 不传 limit 或传
0:丢弃末尾空字符串; - 负数:保留全部字段,包括末尾空字段;
- 正数
n:最多切成n段,最后一段保留尚未切分的内容。
CSV 有引号、转义和嵌入换行规则,不能靠一个 split(",") 正确解析。生产代码应使用符合目标 CSV 方言的库。
3. trim 与 strip 的空白定义不同
String padded = " \u2003Hello\u2003 ";
System.out.println("'" + padded.trim() + "'");
System.out.println("'" + padded.strip() + "'");输出:
' Hello '
'Hello'trim() 移除首尾代码值不大于 U+0020 的字符。strip() 从 Java 11 起按 Character.isWhitespace 判断,更适合多语言文本,但“Unicode 中看起来像空白的字符”不一定都被该方法认作 whitespace。例如 "\u00A0".strip().isEmpty() 为 false:不换行空格 U+00A0 不属于 Character.isWhitespace 的集合。选择方法时按输入协议,而不是笼统地说 strip 清除所有 Unicode 空白。
相关方法还有:
text.stripLeading();
text.stripTrailing();
text.isBlank();isBlank() 接受空字符串或全部由 Character.isWhitespace 字符组成的字符串;isEmpty() 只检查长度是否为 0。
4. replace 处理字面文本,replaceAll 处理正则
String notice = "Cats and cats";
System.out.println(notice.replace("cats", "dogs"));
System.out.println(notice.replaceAll("(?i)cats", "dogs"));输出:
Cats and dogs
dogs and dogsreplace(CharSequence, CharSequence) 把目标当作字面文本。replaceAll(regex, replacement) 的第一个参数是正则,第二个参数也有自己的替换语法:$1 引用捕获组,反斜杠用于转义。
替换文本来自外部输入时,用 Matcher.quoteReplacement:
String price = "price=10";
String safe = price.replaceAll("\\d+", Matcher.quoteReplacement("$5"));
System.out.println(safe); // price=$5若只需替换字面内容,优先用 replace,既直白也避开两层正则规则。
5. 查找与大小写转换
| 方法 | 结果 | 边界 |
|---|---|---|
indexOf(text) | 首次出现位置,找不到为 -1 | 可指定起始索引 |
lastIndexOf(text) | 最后出现位置 | 找不到为 -1 |
contains(text) | 是否包含字面子串 | 参数不能为 null |
startsWith / endsWith | 是否匹配前缀或后缀 | 区分大小写 |
toUpperCase(locale) | 按指定地区转换 | 面向用户时不要依赖默认地区 |
协议允许这种转换时,可用 Locale.ROOT 获得不依赖机器语言环境的大小写结果;允许哪些字符、是否区分大小写,仍由协议本身决定:
String normalized = command.toUpperCase(Locale.ROOT);自然语言搜索、排序和大小写折叠更复杂,不能假设一次 toLowerCase() 就解决所有 Unicode 等价问题。
6. 选择合适的构造方式
少量固定片段:使用 +
String label = "订单 #" + orderId + ":" + itemName;源码清楚,编译器会负责运行时拼接策略。不要为了两个片段手写 Builder。
循环或分支中逐步追加:使用 StringBuilder
StringBuilder report = new StringBuilder();
for (OrderLine line : lines) {
report.append(line.item())
.append(" x ")
.append(line.quantity())
.append(System.lineSeparator());
}
String text = report.toString();容量会自动增长。只有在有可靠估算且分析显示扩容值得避免时,才传入初始容量;随手写一个巨大的容量可能浪费内存。
已有元素集合:使用 join
String summary = String.join(" | ", "铁锤", "钉子", "木板");
String commaSeparated = names.stream().collect(Collectors.joining(","));这里仅把名称用逗号连接,并没有转义字段内的逗号、引号或换行,因此不构成完整 CSV 编码。需要前缀和后缀时,Collectors.joining(", ", "[", "]") 比手动处理最后一个分隔符更稳妥。
模板文本:格式化或文本块
String receipt = "订单 %s,共 %d 件".formatted(orderId, quantity);
String template = """
客户:%s
物品:%s
""".formatted(customer, item);格式字符串也有类型契约,外部输入不应直接充当可信模板。
7. StringBuilder 与 StringBuffer
| 类型 | 并发契约 | 常见用途 |
|---|---|---|
StringBuilder | 不提供线程安全保证 | 方法内部或单线程拥有的可变缓冲区 |
StringBuffer | 在必要操作上同步,支持单次操作的线程安全 | 必须兼容要求该类型的旧 API |
StringBuffer 在必要操作上同步,但“先检查再追加”这样的多个调用仍不是一个原子事务。向它追加另一个可变 CharSequence 时,对目标缓冲区加锁也不会自动保护源对象。让多个线程共享一个可变文本缓冲区通常会把顺序、锁和错误处理缠在一起。更常见的设计是每个任务在本地构造结果,最后由明确的汇总阶段合并。
不要写固定的“慢 2 到 3 倍”或“99% 场景”结论。差异取决于 JDK、竞争程度和工作负载;有性能要求时用 JMH 等工具测量目标代码。
8. 完整可运行示例
先准备临时目录:
chapter9_operations_root=$(mktemp -d)
readonly chapter9_operations_root
mkdir -p "$chapter9_operations_root/out"
cd "$chapter9_operations_root"保存为 StringOperationsDemo.java:
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class StringOperationsDemo {
public static void main(String[] args) {
String order = "2026-07-15|一名开发者|铁锤|2";
checkEquals("2026-07-15", order.substring(0, 10));
checkEquals("一名开发者|铁锤|2", order.substring(11));
String ip = "192.168.1.1";
checkEquals(List.of("192", "168", "1", "1"),
Arrays.asList(ip.split(Pattern.quote("."))));
String row = "铁锤,2,,";
checkEquals(2, row.split(",").length);
checkEquals(4, row.split(",", -1).length);
checkEquals("", row.split(",", -1)[3]);
String padded = " \u2003Hello\u2003 ";
checkEquals("\u2003Hello\u2003", padded.trim());
checkEquals("Hello", padded.strip());
String safePrice = "price=10".replaceAll(
"\\d+", Matcher.quoteReplacement("$5"));
checkEquals("price=$5", safePrice);
List<String> items = List.of("铁锤", "钉子", "木板");
String joined = items.stream().collect(Collectors.joining(" | "));
checkEquals("铁锤 | 钉子 | 木板", joined);
StringBuilder report = new StringBuilder();
for (int index = 0; index < items.size(); index++) {
report.append(index + 1).append(". ").append(items.get(index));
if (index + 1 < items.size()) report.append(System.lineSeparator());
}
System.out.println(order.substring(0, 10));
System.out.println(joined);
System.out.println("REPORT".toLowerCase(Locale.ROOT) + ":");
System.out.println(report);
System.out.println("字符串操作检查通过");
}
static void checkEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + ", actual=" + actual);
}
}
}运行:
javac --release 17 -encoding UTF-8 -Xlint:all -d out StringOperationsDemo.java
java -cp out StringOperationsDemo预期输出:
2026-07-15
铁锤 | 钉子 | 木板
report:
1. 铁锤
2. 钉子
3. 木板
字符串操作检查通过保存需要的实验记录后清理:
cd
ls -ld -- "$chapter9_operations_root"
rm -r -- "$chapter9_operations_root"专业查阅入口
StringAPI:substring、split、strip、formatted、lines;Pattern.quote与Matcher.quoteReplacement的不同用途;StringBuilder的 capacity、length 与setLength契约;- JMH:需要比较拼接方案时使用基准测试框架,避免用一次
nanoTime得出结论。
离开这一章前
为 substring 写出左闭右开边界,为 split 分别测试正则点号、末尾空字段和正数 limit;外部替换文本则用 Matcher.quoteReplacement。再按场景选择少量 +、增量 StringBuilder 或集合 join,并明确 StringBuffer 的单方法同步不等于整段流程原子。
保存这些固定输入和预期字段。下次订单格式变化时,先看解析契约需要怎样调整。下一章用集合管理数量会变化的数据。
→ 下一步:核心集合
API 入口:Java 17 Pattern、Java 17 Matcher、Java 17 StringBuilder。