String

String 源码分析

String 用不可变性换来三样东西:可以安全共享(常量池、拷贝构造直接共享数组)、可以缓存 hashCode、天然线程安全,所以它敢当 HashMap 的 key。
JDK 9 起存储从 char[] 换成 byte[] + coder 标记(Compact Strings),纯 Latin1 内容每字符只占 1 字节,字符串占大头的堆直接省近一半内存。

// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.lang.String
public final class String                       // final:不可变的第一道锁,杜绝子类重写方法伪装成"可变字符串"
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    @Stable                                     // 告诉 JIT:此字段一旦非默认值就永不再变,可参与常量折叠
    private final byte[] value;                 // 真正的存储。final + private + 从不外泄引用 = 不可变的根基

    private final byte coder;                   // value 的解读方式:LATIN1 每字符 1 字节,UTF16 每字符 2 字节

    @Stable
    private int hash; // Default to 0           // hashCode 缓存。敢缓存的前提是内容不可变

    private boolean hashIsZero; // Default to false;    // 补丁位:内容恰好算出 hash==0 时靠它记住"已算过"

    // ...

    static final boolean COMPACT_STRINGS;       // 压缩总开关。特意做成静态常量:JIT 折叠后 coder 分支可整体消失

    static {
        COMPACT_STRINGS = true;                 // 实际值由 JVM 注入(-XX:-CompactStrings 可关),这里只是占位
    }

    // ...

    public String(String original) {            // 拷贝构造直接共享 value 数组:不可变对象无需防御性复制
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;              // 连算好的 hash 一起带走
        this.hashIsZero = original.hashIsZero;
    }

    // ...

    public int length() {
        return value.length >> coder();         // 字节数 >> coder 得字符数:LATIN1 右移 0 位,UTF16 右移 1 位
    }

    public char charAt(int index) {
        if (isLatin1()) {                       // 几乎所有方法都是这个套路:按 coder 分发给两套实现之一
            return StringLatin1.charAt(value, index);
        } else {
            return StringUTF16.charAt(value, index);
        }
    }

    // ...

    String(byte[] value, byte coder) {          // 包内构造:直接持有传入数组不复制,JDK 内部专用的零拷贝通道
        this.value = value;
        this.coder = coder;
    }

    byte coder() {
        return COMPACT_STRINGS ? coder : UTF16; // 先读静态常量再读实例字段,让 JIT 能把整个判断折叠掉
    }

    boolean isLatin1() {
        return COMPACT_STRINGS && coder == LATIN1;
    }

    @Native static final byte LATIN1 = 0;
    @Native static final byte UTF16  = 1;
}
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.lang.String
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;                            // 同一对象直接短路,intern 过或来自常量池的字符串常走这条
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)    // 编码不同即内容必不同:构造时保证了"能压缩必压缩"
            && StringLatin1.equals(value, aString.value);           // coder 相同则字节布局相同,UTF16 串也走这个逐字节比较
}

// 源码注释点明:hash / hashIsZero 存在良性数据竞争(benign data race)。多线程可能各算一遍,
// 但计算幂等、输入不可变、每个实例只会写这两个字段之一,所以不加锁不加 volatile 结果也正确
public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {                // 两个条件都不成立才真正计算
        h = isLatin1() ? StringLatin1.hashCode(value)
                       : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;                  // 结果恰好是 0:记进标记位,本对象下次免算
        } else {
            hash = h;
        }
    }
    return h;
}
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.lang.String
public String substring(int beginIndex) {
    return substring(beginIndex, length());     // 单参版只是转发,越界检查和复制都在双参版里
}

public String substring(int beginIndex, int endIndex) {
    int length = length();
    checkBoundsBeginEnd(beginIndex, endIndex, length);
    if (beginIndex == 0 && endIndex == length) {
        return this;                            // 截整串直接复用自身:不可变让这种复用永远安全
    }
    int subLen = endIndex - beginIndex;
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)   // 复制出新数组。JDK 7u6 起不再共享原数组:
                      : StringUTF16.newString(value, beginIndex, subLen);   // 老实现截 1 个字符也会钉住整个大数组不放
}

public String concat(String str) {
    if (str.isEmpty()) {
        return this;                            // 拼空串免分配
    }
    return StringConcatHelper.doConcat(this, str);      // 每次 concat 都产出全新 String——循环拼接必须换 StringBuilder
}

public native String intern();                  // 查 JVM 的 StringTable(native 哈希表):池里有等值串就返回池内那个,没有就登记自己

原理串讲

以最常见的 map.put(key, value)(key 是 String)走一遍完整链路。
HashMap 先调 key.hashCode():第一次调用时 hash 是 0 且 hashIsZero 为 false,于是按 isLatin1() 分发给 StringLatin1.hashCode(value)StringUTF16.hashCode(value),逐字符累积 h = 31 * h + c,算完写回 hash 字段;之后同一个 key 再怎么查询,hashCode() 都只剩一次字段读。
为什么敢这样无锁缓存?因为写入的值由不可变的 value 唯一决定,两个线程抢着算也只会写入同一个数,是典型的良性竞争,省掉 volatile 和锁反而是这段代码的精髓。
为什么乘数选 31?奇素数分布好,且 31 * i == (i << 5) - i,JIT 能把乘法优化成移位减法。
hashIsZero 这个看似多余的 boolean 是 JDK 13 补的坑:内容恰好散列为 0 的字符串(比如 "\u0000" 或某些精心构造的串)在老实现里每次调用都全量重算,可被拿来做拒绝服务攻击的放大器。

哈希定位到桶后发生碰撞,HashMap 转调 key.equals(k)。它的三级短路很讲究:先 == 比引用(字面量和 intern 串在这里就结束了);再 instanceof 带模式变量一步完成类型判断加转型;然后比 coder
为什么 coder 不同能直接判不等?因为构造路径保证了编码是内容的规范函数:能用 Latin1 表示的内容一定被压成 LATIN1,所以同内容必同 coder,编码不同等价于内容不同。
coder 相同后,两个 value 的字节布局完全一致,直接 StringLatin1.equals 逐字节比即可,UTF16 串也复用这一个方法。

再看两个高频操作。substring 自 JDK 7u6 起用 StringLatin1/StringUTF16.newString 复制出独立数组,把 O(1) 共享改成了 O(n) 复制,代价是性能,换来的是从几 MB 日志里截出的 8 字符 ID 不会把整个大数组钉在堆里。
+ 拼接从 JDK 9 起由编译器翻译成 invokedynamic,运行期挂到 StringConcatHelper 的具体策略上一次性算好总长度、分配一个终态数组直接填充,比链式 concat 少建所有中间对象;但这套优化只对单个表达式生效,循环体里每轮 s += x 仍是每轮一个新 String,这正是”循环内用 StringBuilder”这条规矩的源码依据(StringBuilder 复用一个可变 byte[],不够就倍增扩容,StringBuffer 只是给同样的方法加了 synchronized)。

设计取舍

  • byte[] + coder 替代 char[]:省一半内存,代价是每个方法多一次编码分发;用 COMPACT_STRINGS 静态常量前置判断,让 JIT 把分支折叠成零开销。
  • hash 缓存接受重复计算、拒绝加锁:多算一次的代价远小于每次读 hash 都过内存屏障。
  • substring 用 O(n) 复制换掉 O(1) 共享:牺牲截取性能,根治”小串钉住大数组”的内存泄漏。
  • intern 能给海量重复串去重,但 StringTable 容量有限且查表有开销;大规模去重更该考虑 G1 的字符串去重或业务层缓存。
  • 拷贝构造 new String(s) 共享数组几乎零成本,但也几乎没有使用场景——它只会让 <mark> 失效,除了演示 陷阱之外别用。

延伸阅读