← [ BACK_TO_NOTES ]

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

结合场景再看三个关注点

  1. 不可变:trim() 不修改原串,需使用返回值;没有变化时可以返回原引用。
  2. 长度口径:业务“显示宽度”、码点、UTF-8 字节是三件事。
  3. 比较内容用 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. 拼接与性能

  • + 在编译期对常量可优化;循环内大量 + 应用 StringBuilder
  • StringBuilder 非线程安全,优先避免共享;StringBuffer 的单个方法同步,不保证由多次调用组成的操作整体原子。

5. 常用 API 地图

API用途
isBlank/isEmptyJava 11+ 的 isBlank 按 Character.isWhitespace 判断;isEmpty 只检查长度为 0
substring/split/replace变换(注意 split 正则)
formatted / String.format格式化
intern字符串池(高级、慎用)

边界情况与反直觉行为

  1. split(".") 按正则,. 匹配任意字符——要转义。
  2. substring 在旧版本与当前版本的实现细节不同,不要依赖“共享底层”传闻写逻辑。
  3. null 与 "null" 字符串不同;拼接 null 会变成 "null" 文本。
  4. 与语言无关的标识符大小写转换可显式用 Locale.ROOT;用户文本则按实际语言规则处理。
  5. trim() 去除首尾不大于 U+0020 的字符;Java 11+ 的 strip() 按 Character.isWhitespace 判断,也不包括所有不换行空格。

常见误区

[!warning] 常见误区:用 == 比较用户输入字符串 内容比较必须用 equals/Objects.equals。字符串字面量和字符串常量表达式的驻留是语言保证,但不能推广到任意用户输入或运行时拼接结果。

[!warning] 常见误区:length 当“可见字符数” emoji、组合字符会使 length 与用户直觉不符;UI 长度规则要单独定义。

工程实践

  1. API 边界统一 UTF-8。
  2. 日志与监控对超长字符串截断,防爆内存。
  3. 密码等敏感串注意驻留与 char[] 清理策略(进阶)。
  4. 国际化文案不要靠硬编码拼接语序。

本节总结

  • String 不可变、内容比较用 equals
  • 分清 code unit / code point / bytes
  • 编码显式化;循环拼接用 builder

自测题

  1. 为什么 new String("a") == "a" 为 false?
  2. 为什么读写文件要写 UTF_8?
参考答案
  1. == 比引用;new 创建不同对象,所以表达式为 false。
  2. 当文件约定 UTF-8 时显式指定可避免默认值差异;不能不顾源文件编码而强制按 UTF-8 解码。

延伸阅读与资料来源

资料类型支撑内容
String (Java SE)API字符串行为
CharsetAPI字符集
JLS String Literals规范字面量与 intern 相关语义
创建于 2026/7/15 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本