一、概述与历史背景
string 是 java 中被使用最多的类,其设计经历了多次重要演进:
| 时期 | 关键变化 |
|---|---|
| jdk 1.0(1996) | string 基于 char[],stringbuffer 提供可变字符串 |
| jdk 1.5(2004) | stringbuilder(非同步,性能更优)、string.format |
| jdk 1.7(2011) | 字符串常量池从永久代移入堆;substring 不再共享底层数组 |
| jdk 1.9(2017) | 紧凑字符串(jep 254):byte[] + coder 标志 |
| jdk 1.11(2018) | isblank/lines/strip/repeat 等实用方法 |
| jdk 1.15(2020) | 文本块(text blocks)正式 |
| jdk 1.18(2021) | 默认字符集改为 utf-8(jep 400) |
| jdk 21/23 | 字符串模板预览后撤回重新设计 |
二、string 不可变性原理
2.1 源码层面的保障
// jdk 9+ 的 string 核心结构(简化)
public final class string implements charsequence {
// jdk8及以前:private final char[] value;
// jdk9+:紧凑字符串,latin1用1字节/字符,其余用2字节
@stable
private final byte[] value;
private final byte coder; // 0=latin1, 1=utf16
private int hash; // 缓存的hashcode,默认0
// 没有任何方法修改value的内容
// 所有"修改"操作都返回新string:
public string touppercase() { /* 返回新对象 */ }
public string replace(char a, char b) { /* 返回新对象 */ }
}三重保障:
- 类
final:防止子类覆写方法破坏不可变性; value数组private final:引用不可变,外部拿不到数组;- 不提供任何修改数组的方法:所有变更操作返回新对象。
2.2 不可变的收益演示
import java.util.hashmap;
import java.util.map;
public class immutabilitydemo {
public static void main(string[] args) {
// 收益1:线程安全 —— 无需同步即可跨线程共享
string shared = "线程安全";
new thread(() -> system.out.println(shared)).start();
// 收益2:hashcode可缓存 —— hashmap键性能好
string key = "java";
key.hashcode(); // 第一次计算并缓存
map<string, string> map = new hashmap<>();
map.put(key, "语言");
system.out.println(map.get("java")); // 语言(哈希缓存加速)
// 收益3:"修改"返回新对象,原对象不变
string s = "hello";
string upper = s.touppercase();
system.out.println(s); // hello(原串不变)
system.out.println(upper); // hello(新对象)
}
}三、字符串常量池与 intern()
3.1 字面量、new 与常量池
public class stringpooldemo {
public static void main(string[] args) {
// 字面量:编译期入池,运行时直接引用池中对象
string a = "abc";
string b = "abc";
system.out.println(a == b); // true(同一池中对象)
// new:强制在堆上创建新对象(其内部仍引用池中char/byte数组)
string c = new string("abc");
system.out.println(a == c); // false(堆对象≠池对象)
system.out.println(a.equals(c)); // true(内容相同)
// 常量折叠:编译期 "a"+"b" 直接折叠为 "ab" 并入池
string d = "a" + "b";
system.out.println(a == d); // true
// 变量参与则运行期拼接,结果不入池
string x = "a";
string e = x + "b";
system.out.println(a == e); // false
// final变量仍视为常量
final string fx = "a";
string f = fx + "b";
system.out.println(a == f); // true
}
}3.2 intern() 的行为差异(jdk 6 vs jdk 7+)
public class interndemo {
public static void main(string[] args) {
// 场景1:池中已有 "java"(字面量类名等会提前入池)
string s1 = new string("ja") + new string("va"); // 堆中"java",不入池
string s2 = s1.intern(); // 池无则入池(jdk7+存引用),返回池中串
string s3 = "java"; // 字面量引用池中对象
system.out.println(s1 == s2); // jdk7+: true(池中存的就是s1的引用)
system.out.println(s1 == s3); // jdk7+: true
// 场景2:对比 "j" 开头但池中没有的串(用动态构造避免提前入池)
string t1 = new stringbuilder("go").append("od").tostring();
system.out.println(t1.intern() == t1); // jdk7+: true
string t2 = new stringbuilder("ja").append("va").tostring();
system.out.println(t2.intern() == t2); // false("java"已被池化)
}
}要点:
- jdk 6:
intern()在永久代池中复制一份; - jdk 7+:池移入堆,池中无该串时直接保存堆对象引用,省去复制;
- 实际用途:海量重复字符串场景(如爬虫去重)手动驻留可省内存,但需谨慎评估。
四、stringbuilder 与 stringbuffer
4.1 对比与选型
| 维度 | stringbuilder(jdk5) | stringbuffer(jdk1.0) |
|---|---|---|
| 线程安全 | 否 | 是(方法级 synchronized) |
| 性能 | 高 | 低(锁开销) |
| 适用 | 单线程拼接(绝大多数场景) | 跨线程共享构建器(极少见) |
4.2 完整使用示例
public class builderdemo {
public static void main(string[] args) {
// 预分配容量,避免扩容(默认16,扩容公式:旧容量*2+2)
stringbuilder sb = new stringbuilder(64);
sb.append("select ").append("id, name ") // 链式追加
.append("from users ")
.insert(0, "-- 查询语句:\n") // 插入
.append("where age > ").append(18);
system.out.println(sb);
// -- 查询语句:
// select id, name from users where age > 18
sb.delete(0, 3); // 删除区间 [0,3)
sb.replace(0, 6, "query"); // 替换区间
sb.reverse(); // 反转
system.out.println(sb);
string result = sb.tostring(); // 每次调用都复制新数组
system.out.println(result.length());
}
}4.3 扩容机制
底层数组默认 16 个元素;append 超出时按 (旧容量 << 1) + 2 扩容并 arrays.copyof 复制。若能预估最终长度,构造时指定容量可避免多次复制。
五、字符串拼接性能演进
5.1 编译器处理方式的变迁
public class concatdemo {
public static void main(string[] args) {
string name = "java";
int version = 21;
// jdk5~8:编译为 new stringbuilder().append(name).append(version).tostring()
// jdk9+:编译为 invokedynamic 调用 stringconcatfactory
string msg = "语言" + name + "版本" + version;
system.out.println(msg); // 语言java版本21
}
}5.2 循环拼接的经典陷阱
public class loopconcatdemo {
public static void main(string[] args) {
int n = 100000;
// 反例:循环内+拼接
// 每次循环都新建stringbuilder并复制已有内容 → o(n^2)
long t1 = system.currenttimemillis();
string bad = "";
for (int i = 0; i < n; i++) {
bad = bad + i; // 编译器只在每轮循环内建一个builder
}
long t2 = system.currenttimemillis();
system.out.println("+拼接耗时: " + (t2 - t1) + "ms");
// 正例:循环外创建一个builder → o(n)
long t3 = system.currenttimemillis();
stringbuilder sb = new stringbuilder(n * 4);
for (int i = 0; i < n; i++) {
sb.append(i);
}
string good = sb.tostring();
long t4 = system.currenttimemillis();
system.out.println("builder耗时: " + (t4 - t3) + "ms");
}
}5.3 jdk 8 集合拼接工具
import java.util.list;
import java.util.stringjoiner;
import java.util.stream.collectors;
public class joindemo {
public static void main(string[] args) {
list<string> langs = list.of("java", "kotlin", "scala");
// 方式1:string.join(jdk8)
system.out.println(string.join(", ", langs)); // java, kotlin, scala
// 方式2:stringjoiner,带前后缀
stringjoiner joiner = new stringjoiner(", ", "[", "]");
langs.foreach(joiner::add);
system.out.println(joiner); // [java, kotlin, scala]
// 方式3:stream收集器
string csv = langs.stream().collect(collectors.joining(" | "));
system.out.println(csv); // java | kotlin | scala
}
}六、字符串比较
public class comparedemo {
public static void main(string[] args) {
string a = "abc";
string b = new string("abc");
string c = "abc";
// 1. ==:比较引用(地址),不是内容!
system.out.println(a == b); // false
system.out.println(a == c); // true(同一池对象)
// 2. equals:比较内容(推荐,且能处理null判断在左)
system.out.println(a.equals(b)); // true
system.out.println("abc".equals(b)); // 常量在左,避免npe
// 3. compareto:字典序比较,返回差值,可用于排序
system.out.println("apple".compareto("banana")); // 负数
system.out.println("abc".compareto("abc")); // 0
// 4. equalsignorecase / comparetoignorecase
system.out.println("abc".equalsignorecase("abc")); // true
// 5. contentequals:与charsequence比较(含stringbuilder)
system.out.println("abc".contentequals(new stringbuilder("abc"))); // true
}
}七、文本块(text block)
jdk 13 首次预览,jdk 15 正式(jep 378)。
public class textblockdemo {
public static void main(string[] args) {
// 传统写法:转义+拼接,可读性差
string oldjson = "{\n" +
" \"name\": \"java\",\n" +
" \"version\": 21\n" +
"}";
// 文本块:三个引号,起始定界符后必须换行
string json = """
{
"name": "java",
"version": 21
}
""";
system.out.println(json);
system.out.println(json.equals(oldjson)); // true
// 缩进处理:结束定界符的位置决定"偶然缩进"去除量
// 上例中结束"""与内容对齐,内容保留2空格缩进
// sql示例:天然保留换行与缩进
string sql = """
select id, name, email
from users
where status = 'active'
order by created_at desc
""";
system.out.println(sql);
// 转义技巧
string escaped = """
第一行 \
接上一行 \
最终一行
"""; // \ 在行尾 → 去除换行(续行)
system.out.println(escaped);
string trailing = """
保留尾部空格 \s
下一行"""; // \s 显式保留空格
system.out.println(trailing.length());
// 配合formatted(jdk15)填充变量
int age = 21;
string info = """
{
"name": "%s",
"age": %d
}
""".formatted("java", age);
system.out.println(info);
}
}关键规则:
- 起始
"""后必须紧跟换行; - 编译器找到所有行(含结束定界符行)的最小缩进,统一去除(偶然缩进);
- 结束定界符的位置可控制保留多少缩进;
- 行尾空白默认剥离,
\s可强制保留。
八、字符串模板(预览)
字符串模板(string templates)曾在 jdk 21(jep 430)与 jdk 22(jep 459)作为预览特性出现,但因 api 设计争议在 jdk 23 被撤回,重新设计中。语法形态回顾:
// 注意:以下代码仅在jdk21/22预览模式下可编译
// 编译参数:--enable-preview --source 21
// string name = "java";
// int version = 21;
// string msg = str."语言\{name}版本\{version}";
// 输出:语言java版本21
// 多行模板:
// string json = str."""
// {
// "name": "\{name}",
// "version": \{version}
// }
// """;当前生产建议:不要依赖字符串模板;占位符拼接使用 string.format、messageformat 或模板引擎(freemarker/thymeleaf)。
九、字符编码与 charset
9.1 编码基础与 java 默认编码变迁
- jdk 17 及以前:
charset.defaultcharset()依赖操作系统(windows 中文环境常为 gbk),跨平台乱码的根源; - jdk 18+(jep 400):默认字符集强制为 utf-8,
file.encoding仅影响system.out/err。
9.2 string 与 byte[] 互转
import java.nio.charset.charset;
import java.nio.charset.standardcharsets;
import java.util.arrays;
public class charsetdemo {
public static void main(string[] args) throws exception {
string text = "java字符串";
// 编码:string → byte[],必须显式指定字符集!
byte[] utf8 = text.getbytes(standardcharsets.utf_8);
byte[] gbk = text.getbytes(charset.forname("gbk"));
system.out.println("utf-8字节数: " + utf8.length); // 13
system.out.println("gbk字节数: " + gbk.length); // 9
// 解码:byte[] → string,字符集必须与编码时一致
system.out.println(new string(utf8, standardcharsets.utf_8)); // 正常
system.out.println(new string(gbk, standardcharsets.utf_8)); // 乱码!
// jdk18+ 默认即utf-8
system.out.println("默认字符集: " + charset.defaultcharset());
// 判断是否支持某编码
system.out.println(charset.issupported("gb2312")); // true
// 列出所有可用编码
system.out.println("可用编码数量: " + charset.availablecharsets().size());
}
}要点:getbytes() / new string(bytes) 无参版本使用平台默认编码,是历史乱码 bug 的高发区——永远显式传 standardcharsets.utf_8。
十、格式化:format 与 printf
import java.util.date;
import java.util.locale;
public class formatdemo {
public static void main(string[] args) {
string name = "java";
double score = 98.5678;
date now = new date();
// %s字符串 %d整数 %f浮点 %n平台换行 %%百分号
string s1 = string.format("语言:%s, 评分:%.2f%n", name, score);
system.out.print(s1); // 语言:java, 评分:98.57
// 索引参数:复用同一参数
string s2 = string.format("%1$s 与 %1$s 重复引用", name);
system.out.println(s2); // java 与 java 重复引用
// 日期格式:%t 前缀
string s3 = string.format("今天: %1$tf %1$tt", now);
system.out.println(s3); // 今天: 2025-01-01 12:00:00
// locale敏感:千分位
string s4 = string.format(locale.us, "%,d", 1234567);
system.out.println(s4); // 1,234,567
// printf直接输出到控制台(内部同一套formatter)
system.out.printf("保留1位小数: %.1f, 十六进制: %#x, 布尔: %b%n",
score, 255, true);
// 对齐与宽度:%-10s左对齐占10位,%05d补零5位
system.out.printf("|%-10s|%05d|%n", "左对齐", 42);
}
}性能提示:string.format 每次解析格式串,高频循环中慎用;messageformat 可复用已编译模式;极致性能场景用 stringbuilder 手工拼接。
十一、实际应用场景
- sql/json 模板:文本块 +
formatted()生成测试夹具或日志报文; - 大文本组装:日志聚合、csv 导出 →
stringbuilder预估容量或collectors.joining; - 内存敏感的海量短串:
intern()驻留(如爬虫 url 去重),需配合基准测试; - 网络/文件传输:显式
standardcharsets.utf_8编解码; - 用户可见文案:
string.format+locale或messageformat做国际化。
十二、最佳实践与常见陷阱
12.1 最佳实践
- 比较内容用
equals,常量放左边防 npe; - 循环拼接用
stringbuilder,且预估容量; - 编解码永远显式指定
standardcharsets.utf_8; - 多行文本用文本块,不要再手拼
\n; - 不要为了"省内存"滥用
substring(jdk 7+ 已不共享数组,无收益); - 格式化热路径考虑复用
messageformat或改手工拼接。
12.2 常见陷阱
public class stringtrap {
public static void main(string[] args) {
// 陷阱1:+拼接与==
string a = "a" + new string("b"); // new参与,运行期拼接,不入池
system.out.println(a == "ab"); // false
// 陷阱2:无参getbytes依赖平台编码 → 跨平台乱码
// byte[] bad = "中文".getbytes(); // 禁用
// 陷阱3:string.format 的 % 与 locale
// string.format("%f", 1.5) 在德语locale下用逗号小数点
// 陷阱4:stringbuilder.tostring()后继续修改
stringbuilder sb = new stringbuilder("abc");
string s = sb.tostring();
sb.append("d");
system.out.println(s); // abc(tostring已复制,不受影响)
// 陷阱5:空串判断
string empty = "";
system.out.println(empty.isempty()); // true
system.out.println(" ".isblank()); // true(jdk11,含空白字符)
}
}十三、版本演进说明
| 版本 | 特性 | 备注 |
|---|---|---|
| jdk 1.0 | string(char[])、stringbuffer、常量池 | 奠基 |
| jdk 1.4 | charsequence、正则支持 | 接口抽象 |
| jdk 1.5 | stringbuilder、string.format、可变参数 | 性能与便利 |
| jdk 1.7 | 常量池入堆、substring 独立复制 | 内存模型调整 |
| jdk 1.8 | stringjoiner、collectors.joining | 集合拼接 |
| jdk 1.9 | 紧凑字符串(jep 254,byte[]+coder) | 内存减半 |
| jdk 1.10 | (无重大字符串特性) | — |
| jdk 1.11 | isblank、lines、strip、stripleading/trailing、repeat | 实用工具 |
| jdk 1.12 | indent、transform | 文本处理 |
| jdk 1.13~14 | 文本块预览(两轮) | — |
| jdk 1.15 | 文本块正式(jep 378) | 多行字符串 |
| jdk 1.18 | 默认字符集 utf-8(jep 400) | 终结乱码根源 |
| jdk 21 | 字符串模板预览(jep 430)、formatted 增强 | 预览 |
| jdk 22 | 字符串模板二次预览(jep 459) | — |
| jdk 23 | 字符串模板撤回重设计 | 生产勿依赖 |
总结:java 字符串体系的主线是"不可变保安全、构建器保性能、文本块保可读、utf-8 保互通"。掌握常量池机制与拼接性能模型,能解决绝大多数面试与工程问题;文本块与 formatted() 则是现代 java 文本处理的标准姿势。
到此这篇关于java字符串与文本处理详解的文章就介绍到这了,更多相关java字符串与文本处理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论