Java 字符串与字符集
Java String 不可变、UTF-16 代码单元、常见编码陷阱与高效拼接边界。
#concept
#growing
#dev
#java
[!info] related notes
Java 字符串与字符集
这个概念为什么出现
业务里到处是订单号、文案、JSON 文本。Java 用 String 表示不可变文本,API 以 UTF-16 代码单元序列为模型(底层存储是实现细节);一旦把“字符个数、字节长度、编码”混为一谈,就会出现截断乱码、错误校验长度、== 误用等问题。
[!abstract] 一句话理解
String是不可变的字符序列对象;length()计的是 UTF-16 code unit 数,跨边界文本处理要分清码点、字符簇与字节编码(Charset)。
最小可运行示例
场景:用户昵称校验与审计日志
此示例拒绝按 trim() 规则去除首尾字符后为空的昵称,并分别统计 UTF-16 单元、码点和 UTF-8 字节。它不覆盖所有 Unicode 空白,也不计算 UI 显示宽度。
import java.nio.charset.StandardCharsets;
public class NicknameGuard {
// validate:最小业务校验。
//
// 业务意图:拒绝空串及全部由 U+0020 以下字符组成的输入。
// 教学点:trim 不改变原串;可能返回新串或原引用。
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 统计码点数,不是 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=4
utf8Bytes=6
codePoints=4
equals=true refEq=false
结合场景再看三个关注点
- 不可变:
trim()不修改原串,需使用返回值;没有变化时可以返回原引用。 - 长度口径:业务“显示宽度”、码点、UTF-8 字节是三件事。
- 比较内容用
equals,不要默认==。
核心概念与准确模型
1. 不可变
- 字符串变换不修改原对象;可能返回新对象、原对象或既有对象,不保证分配新串,也不保证结果自动驻留池中。
- String 内容不可变,可安全共享并作为 hash 键。
2. UTF-16 与 char
char/length()基于 UTF-16 code unit- 有效的辅助平面码点占两个
char(代理对);String 也可以包含孤立代理项,不能据此假定所有内容都是良构 Unicode 文本。 - 遍历完整码点用
codePoints()或offsetByCodePoints
3. 编码
byte[] raw = s.getBytes(StandardCharsets.UTF_8);
String s2 = new String(raw, StandardCharsets.UTF_8);
- 协议/文件边界显式指定约定的 Charset。JDK 18+ 默认字符集通常为 UTF-8,旧版本和兼容配置仍有差异;控制台编码还应单独确认。
- IO 层同样要指定编码(见 Reader/Writer)
4. 拼接与性能
+在编译期对常量可优化;循环内大量+应用StringBuilderStringBuilder非线程安全,优先避免共享;StringBuffer的单个方法同步,不保证由多次调用组成的操作整体原子。
5. 常用 API 地图
| API | 用途 |
|---|---|
isBlank/isEmpty | Java 11+ 的 isBlank 按 Character.isWhitespace 判断;isEmpty 只检查长度为 0 |
substring/split/replace | 变换(注意 split 正则) |
formatted / String.format | 格式化 |
intern | 字符串池(高级、慎用) |
边界情况与反直觉行为
split(".")按正则,.匹配任意字符——要转义。substring在旧版本与当前版本的实现细节不同,不要依赖“共享底层”传闻写逻辑。null与"null"字符串不同;拼接 null 会变成"null"文本。- 与语言无关的标识符大小写转换可显式用
Locale.ROOT;用户文本则按实际语言规则处理。 trim()去除首尾不大于 U+0020 的字符;Java 11+ 的strip()按 Character.isWhitespace 判断,也不包括所有不换行空格。
常见误区
[!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创建不同对象,所以表达式为 false。- 当文件约定 UTF-8 时显式指定可避免默认值差异;不能不顾源文件编码而强制按 UTF-8 解码。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| String (Java SE) | API | 字符串行为 |
| Charset | API | 字符集 |
| JLS String Literals | 规范 | 字面量与 intern 相关语义 |