Java 字符串与字符集
Java String 不可变、UTF-16 代码单元、常见编码陷阱与高效拼接边界。
#type / concept
#status / growing
#tech / dev
#resource / java
[!info] 关联笔记
Java 字符串与字符集
这个概念为什么出现
业务里到处是订单号、文案、JSON 文本。Java 用 String 表示不可变文本,内部以 UTF-16 代码单元序列为经典模型;一旦把“字符个数、字节长度、编码”混为一谈,就会出现截断乱码、错误校验长度、== 误用等问题。
[!abstract] 一句话理解
String是不可变的字符序列对象;length()计的是 UTF-16 code unit 数,跨边界文本处理要分清码点、字符簇与字节编码(Charset)。
最小可运行示例
场景:用户昵称校验与审计日志
注册服务要:1)拒绝空昵称;2)统计展示长度时意识到 emoji/中文;3)落盘时明确 UTF-8 字节数。
import java.nio.charset.StandardCharsets;
public class NicknameGuard {
// validate:最小业务校验。
//
// 业务意图:空串与纯空白不允许注册。
// 教学点:String 方法不改变原串,返回新串/布尔。
static String normalize(String raw) {
if (raw == null) {
throw new IllegalArgumentException("nickname null");
}
String trimmed = raw.trim(); // 原 raw 不变
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("nickname blank");
}
return trimmed;
}
static void audit(String nickname) {
byte[] utf8 = nickname.getBytes(StandardCharsets.UTF_8);
System.out.println("units=" + nickname.length());
System.out.println("utf8Bytes=" + utf8.length);
// codePointCount:更接近“Unicode 码点个数”的统计(仍非 grapheme cluster)。
System.out.println("codePoints=" + nickname.codePointCount(0, nickname.length()));
}
public static void main(String[] args) {
String n = normalize(" 猫Cat ");
audit(n);
// 内容相等 vs 引用相等
String a = "ORD";
String b = new String("ORD");
System.out.println("equals=" + a.equals(b) + " refEq=" + (a == b));
}
}
建议运行:
javac NicknameGuard.java && java NicknameGuard
期望输出(要点):
units=5
utf8Bytes=7
codePoints=5
equals=true refEq=false
结合场景再看三个关注点
- 不可变:
trim()得到新串,必须接住返回值。 - 长度口径:业务“显示宽度”、码点、UTF-8 字节是三件事。
- 比较内容用
equals,不要默认==。
核心概念与准确模型
1. 不可变
- 任何“修改”都返回新
String(或复用字面量池中的既有串) - 可安全共享、可作 hash 键(在内容稳定前提下)
2. UTF-16 与 char
char/length()基于 UTF-16 code unit- 辅助平面字符可能占两个
char(代理对) - 遍历完整码点用
codePoints()或offsetByCodePoints
3. 编码
byte[] raw = s.getBytes(StandardCharsets.UTF_8);
String s2 = new String(raw, StandardCharsets.UTF_8);
- 永远显式 Charset,避免依赖默认平台编码
- IO 层同样要指定编码(见 Reader/Writer)
4. 拼接与性能
+在编译期对常量可优化;循环内大量+应用StringBuilderStringBuilder非线程安全;并发构建用StringBuffer或外层同步(更常避免共享 builder)
5. 常用 API 地图
| API | 用途 |
|---|---|
isBlank/isEmpty | 空白判断(版本注意) |
substring/split/replace | 变换(注意 split 正则) |
formatted / String.format | 格式化 |
intern | 字符串池(高级、慎用) |
边界情况与反直觉行为
split(".")按正则,.匹配任意字符——要转义。substring在旧版本与当前版本的实现细节不同,不要依赖“共享底层”传闻写逻辑。null与"null"字符串不同;拼接 null 会变成"null"文本。- 区域相关大小写(土耳其 locale 的 i)会坑。
常见误区
[!warning] 常见误区:用 == 比较用户输入字符串 内容比较必须用
equals/Objects.equals。字面量池相等是优化,不是业务契约。
[!warning] 常见误区:length 当“可见字符数” emoji、组合字符会使 length 与用户直觉不符;UI 长度规则要单独定义。
工程实践
- API 边界统一 UTF-8。
- 日志与监控对超长字符串截断,防爆内存。
- 密码等敏感串注意驻留与
char[]清理策略(进阶)。 - 国际化文案不要靠硬编码拼接语序。
本节总结
- String 不可变、内容比较用 equals
- 分清 code unit / code point / bytes
- 编码显式化;循环拼接用 builder
自测题
- 为什么
new String("a") == "a"常为 false? - 为什么读写文件要写
UTF_8?
参考答案
==比引用;new创建新对象,与字面量池中的串不一定同一引用。- 默认编码随 OS/JVM 配置变化,显式 UTF-8 保证跨环境一致。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| String (Java SE) | API | 字符串行为 |
| Charset | API | 字符集 |
| JLS String Literals | 规范 | 字面量与 intern 相关语义 |