【Java源码阅读系列4】深度解析Java String源码

一、String类的核心设计:不可变性

从源码可以看到,Java String类的定义为:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash; // Default to 0
    // ...其他代码...
}
  • 两大不可变保障

    1. 类被final修饰String类无法被继承,避免子类修改其行为。
    2. 字符数组valuefinal修饰:虽然final数组的引用不可变,但数组内容理论上可修改(如反射),但String的所有方法均未暴露value数组的修改入口(如无setter),且构造时通过Arrays.copyOf复制输入数组(除包私有构造外),确保内部字符序列不可变。
  • 不可变性的优势

    • 线程安全:多线程共享时无需同步。
    • 哈希缓存:hash值仅计算一次(private int hash),适合作为HashMap的键。
    • 字符串常量池:JVM可缓存重复字符串,节省内存(如"abc"字面量会进入常量池)。

二、构造方法:从输入到字符数组的转换

String提供了10余种构造方法(如基于char[]byte[]StringBuffer等),但所有构造方法的核心逻辑都是创建新的字符数组副本,确保内部value不被外部修改。

1. 基于char数组的构造(最基础)

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
  • 关键点:若直接使用this.value = value,则外部修改原始数组会影响String内容。通过Arrays.copyOf创建新数组,彻底隔离外部修改。

2. 包私有构造(性能优化)

String(char[] value, boolean share) {
    this.value = value;
}
  • 设计意图:当明确输入数组不会被外部修改时(如StringBuffertoString()),直接共享数组,减少拷贝开销(StringBuffertoString()调用此构造)。

3. 基于byte数组的构造(编码处理)

public String(byte bytes[], String charsetName) throws UnsupportedEncodingException {
    this(bytes, 0, bytes.length, charsetName);
}
  • 底层逻辑:调用StringCoding.decode(charsetName, bytes, offset, length)将字节数组按指定编码解码为字符数组。若编码不支持(如"GBK"传成"UTF-9"),抛出UnsupportedEncodingException

三、核心方法解析

1. equals:值相等的判断逻辑

public boolean equals(Object anObject) {
    if (this == anObject) return true; // 地址相同直接返回true
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) { // 长度相同才比较内容
            char v1[] = value;
            char v2[] = anotherString.value;
            for (int i = 0; i < n; i++) { // 逐个字符比较
                if (v1[i] != v2[i]) return false;
            }
            return true;
        }
    }
    return false;
}
  • 设计要点

    • 先判断对象地址(优化性能),再检查类型,最后比较长度和字符内容。
    • 重写equals时必须同时重写hashCode(String已重写,保证哈希一致性)。
  • 性能优化:长度不同时直接返回false,避免无意义的字符比较。

2. hashCode:缓存的哈希计算

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) { // 缓存机制:仅计算一次
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; // 公式:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        }
        hash = h;
    }
    return h;
}
  • 选择31的原因

    • 31是质数,减少哈希冲突。
    • 31的乘法可优化为位运算(31 * i = (i << 5) - i),提升计算效率。
  • 缓存机制hash字段存储已计算的哈希值,避免重复计算(适用于多次调用hashCode的场景)。

3. substring:子串的创建

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) throw new StringIndexOutOfBoundsException(beginIndex);
    if (endIndex > value.length) throw new StringIndexOutOfBoundsException(endIndex);
    int subLen = endIndex - beginIndex;
    if (subLen < 0) throw new StringIndexOutOfBoundsException(subLen);
    return ((beginIndex == 0) && (endIndex == value.length)) ? this 
        : new String(value, beginIndex, subLen); // 调用构造方法创建新对象
}
  • 注意:Java 7之前substring会共享原数组(通过偏移量),可能导致内存泄漏(如长字符串截取短子串后,原长数组无法被GC);Java 7后改为复制子数组,避免此问题。

4. indexOf:字符查找

public int indexOf(int ch) {
    return indexOf(ch, 0);
}

public int indexOf(int ch, int fromIndex) {
    final int max = value.length;
    if (fromIndex < 0) fromIndex = 0;
    if (fromIndex >= max) return -1;

    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // BMP字符(0-0xFFFF)
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == ch) return i;
        }
    } else { // 补充字符(需要代理对)
        return indexOfSupplementary(ch, fromIndex);
    }
    return -1;
}
  • BMP与补充字符Unicode中,大部分常用字符属于BMP(基本多文种平面),使用单个char存储;补充字符(如某些emoji)需要两个char(高代理和低代理)。indexOf针对两种情况分别处理。

四、扩展特性:字符串拼接与性能

1. "+"操作符的底层实现

Java编译器会将"a" + "b"优化为new StringBuilder().append("a").append("b").toString()。但循环中使用"+"会频繁创建StringBuilder,推荐显式使用StringBuilder

2. concat方法

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this; // 空字符串直接返回当前对象
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen); // 新建数组,长度为原长度+拼接长度
    str.getChars(buf, len); // 将str的内容拷贝到buf的末尾
    return new String(buf, true); // 基于新数组创建String
}
  • 性能对比concat适合拼接1-2个字符串(直接数组复制),但拼接多个时仍不如StringBuilderStringBuilder通过扩容减少复制次数)。

五、设计思想与最佳实践

1. 不可变性的权衡

  • 优势:线程安全、哈希稳定、避免脏数据。
  • 代价:拼接、修改操作需新建对象,可能导致内存浪费(因此推荐StringBuilder用于频繁修改场景)。

2. 性能优化细节

  • 缓存哈希值hashCode方法通过hash字段缓存结果,避免重复计算。
  • 避免不必要的拷贝:如substring在JDK 7后改为新建数组,而不是共享原数组(避免内存泄漏)。
  • 快速失败equals方法先检查长度,indexOf先检查索引范围,减少无效操作。

3. 扩展思考:为什么String不支持直接修改?

  • Java设计者选择不可变性,本质是用空间换时间和安全性。在分布式、高并发场景中,不可变对象是实现线程安全的最简单方式。若允许修改,需额外同步机制,增加复杂度。

总结

Java String的源码设计体现了“不可变性”这一核心思想,通过final classprivate final char[] value和构造方法中的深拷贝实现。其关键方法(如equalshashCode)在保证正确性的同时,通过缓存、快速失败等优化提升性能。理解String的源码有助于写出更高效、安全的Java代码,尤其是在处理字符串拼接、哈希存储等场景时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值