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

结合场景再看三个关注点

  1. 不可变trim() 得到新串,必须接住返回值。
  2. 长度口径:业务“显示宽度”、码点、UTF-8 字节是三件事。
  3. 比较内容用 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. 拼接与性能

  • + 在编译期对常量可优化;循环内大量 + 应用 StringBuilder
  • StringBuilder 非线程安全;并发构建用 StringBuffer 或外层同步(更常避免共享 builder)

5. 常用 API 地图

API用途
isBlank/isEmpty空白判断(版本注意)
substring/split/replace变换(注意 split 正则)
formatted / String.format格式化
intern字符串池(高级、慎用)

边界情况与反直觉行为

  1. split(".") 按正则,. 匹配任意字符——要转义。
  2. substring 在旧版本与当前版本的实现细节不同,不要依赖“共享底层”传闻写逻辑。
  3. null"null" 字符串不同;拼接 null 会变成 "null" 文本。
  4. 区域相关大小写(土耳其 locale 的 i)会坑。

常见误区

[!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 创建新对象,与字面量池中的串不一定同一引用。
  2. 默认编码随 OS/JVM 配置变化,显式 UTF-8 保证跨环境一致。

延伸阅读与资料来源

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