JDK源码阅读-String类

408 阅读7分钟

概述

String类无疑是日常开发中的使用最频繁的类之一。其被final修饰:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence 

因此可知String类是一个不可变类,且实现了Serializable,ComparableCharSequence接口。

String类类图

hash

hash阈来用缓存字符串的hash code,默认为0。

/** Cache the hash code for the string */
private int hash; // Default to 0

serialVersionUID

String类实现了Serializable接口,因此支持序列化和反序列化。serialVersionUID字段则是用来在运行时判断类版本的一致性。

value[]

String类用一个char数组来存储真实字符序列,就好比Integer中的字段value表示Integer对象的实际值。并且这个数组还被final修饰了,这正是我们所熟知的:String的内容一旦被初始化后,其值就不能再被修改了。当然,通过反射还是可以修改String内容的,不过没啥意义。

/**The value is usesed for character storage.*/
private final char value[];

构造函数

在jdk8中有14中不同的String类构造函数,下面记录一些常用的。

public String()

很明显,String类显示的声明了一个空构造函数。

public String() {
    this.value = "".value;
}

即使是空的构造函数,但还是会创建一个String对象,因此,下面这种字符串的声明方式,不仅鸡肋而且浪费恐怖,不提倡:

String str = new String();
str = "realString";

public Sting(String)

入参为一个字符串的构造函数。其直接将入参字符串的value域和hash域直接赋值给目标String。且String类是不可变类,因此不会对原有的入参字符串产生副作用。

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

在实际开发中,我们应该直接采用字面量赋值的方式来初始化String对象(记为方法一),而不是通过上述这个构造函数new一个String对象(记为方法二)。这是因为:

  • 方法一构造一个新的字符串时,编译器会优先从常量池中(堆中的一块专门区域)查找是否已经存在这个字符串字面量。如果存在则直接返回该对象地址,否则构造一个新对象并保存一份在常量池中。这实现了一种类似缓存的机制,可以避免不必要的开销。
  • 方法二new一个String对象时,即使常量池在存在该对象的字面量,但还是会要求编译器在堆中开辟一个新的内存空间来保存新new出来的对象。
public static void main(String[] args){
    String a = "string";
    String b = "string";
    String c = new String("string");
    String d = new String("string");
    
    System.out.println("a==b: " +(a == b));
    System.out.println("a==c: " +(a == c));
    System.out.println("c==d: " +(c == d));
}

---------------output------------
a==b: true // a和b都是常量池中同一个String对象
a==c: false // a是常量池中的对象,但c是堆内存中新申请的一块空间
c==d: false // 虽然在常量池中存在字面量"string",但c和d都是堆内存中新建立的独立对象

public String(char[])

入参是一个char数组,构造器调用Arrays.copyOf()方法将入参的字符序列依次复制成一个新的String对象,且不会对入参有副作用。

public String(char value[]) {
    this.value = Arrays.copyof(value, vlaue.length);
}

方法

equals(Object)方法

该方法重写了Object的equals方法,用来判断两个字符串是否包含相同的字符序列,最大时间复杂度为O(n),其中n字符串长度。

public boolean equals(Object anObject) {
    // 1. 引用同一对象,判定true
    if (this == anObject) {
        return true;
    }
    
    // 2. 入参不是String类型,判定false
    if (anObject instancof String) {
        String anotherString = (String) anObject;
        int n = value.lenth;
        // 3. 长度不匹配,判定false
        if (n == anotherString.value.length) {
            char v1[] = value; // 写成char[] v1 = value,更规范点
            char v2[] = anotherString.value;
            int i = 0;
            // 4. 逐字符比较,如不等,判定false
            while (n-- != 0){
                if (v1[i] != v2[i]) {
                    return false;
                }
                i++;
            }
        }
        // 5. 否则,判断true
        return true;
    }
    return false;
}

compareTo(String)方法

该方法实现了Comparable接口的compareTo方法,用来按字典顺序比较两个字符串。该比较是基于字符串中各个字符的Unicode值。

字典序

因为该方法比较两个字符串的大小,采用的是字典序,因此有必要了解一下字典序。
字典序定义:如果这两个字符串不同,那么它们要么在某个索引处的字符不同(该索引对二者均为有效索引),要么长度不同,或者同时具备这两种情况。如果它们在一个或多个索引位置上的字符不同,假设 k 是这类索引的最小值;则在位置k上具有较小值的那个字符串(使用 < 运算符确定),其字典顺序在其他字符串之前。在这种情况下,compareTo返回这两个字符串在位置k处两个char值得差,即值:

this.charAt(k) - anotherString.charAt(k);

如果没有字符不同的索引位置,则较短字符串的字典顺序在较长字符串之前。在这种情况下,compareTo返回这两个字符串长度的差,即值:

this.length() - anotherString.length();

入参

anotherString: String,要比较的字符串

返回值

  • 正数:按字典序,调用者位于入参字符串之后
  • 负数:按字典序,调用者位于入参字符串之前
  • 0:两者相等,此时若调用equals方法肯定是返回true

源码分析

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = min(len1, len2); // 取两字符串长度的最小值
    char v1[] = value;
    char v2[] = anotherString.value;
    
    int k = 0;
    while (k < lim) { // 只遍历min(len1, len2)次
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) { // 某个索引处字符不同,则返回字符的差值
            return c1 - c2;
        }
        k++;
    }
    // 如果前个字符都相同,则返回长度差
    return len1 - len2;
}

这个方法写的很巧妙。从初始索引开始,如果两个对象能比较字符的地方都比较完了还相等,就直接返回长度差。此时如果两个字符串长度相等,则返回的刚好就是0,巧妙地判断了返回值的三种情况。且是的最大时间复杂度为T=O(n),其中n为两个字符串的长度最小值。
可能有人好奇,为什么没有在方法体中看到对入参为null的防护?这是因为Comparable接口中已经定了,当入参为null时,compareTo方法会直接抛出NullPointException异常

startsWith方法

startsWith(才发现原来中间还有个s,代码补全的锅!)用来判断字符串是否以指定的前缀开始。其包含两种重载方法:

startsWith(String, int)

入参
  • String prefix: 前缀
  • int toffset: 在此字符串中开始查找的位置
返回值
  • true: 字符序列是此对象从索引 toffset 处开始的子字符串前缀
  • false: toffset为负或大于此String对象的长度或字符序列不是此对象从索引 toffset 处开始的子字符串前缀
源码分析
public boolean startsWith(String prefix, int toffset) {
    char ta[] = value; // 变量名为啥用ta,难道是target?
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    //如果起始地址小于0或者大于自身对象长度,返回false
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    // 从尾开始比较
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}

startsWith(String)

这个方法直接调用了startsWith(String, 0):

public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}

endsWith(String)

相比startsWith,endsWith只有一种方法签名。而且,endsWith的实现方式很巧妙,直接借助了startsWith(String, int)方法:

public boolean endsWith(String suffix) {
    // 将判断是否存在指定后缀转换为去判断是否是指定前缀
    return startsWith(suffix, value.length - suffix.value.length); 
    }

replace方法

replace包含两种重载形式,一种是替换字符串中指定字符,另一种则是替换指定字符序列。

replace(char, char)

直接上源码,就是逐字符校验,讨巧之处在于先定位到旧值出现的位置,才去new一个char数组保存目标字符串。

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
                int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        // 找到oldChar最开始出现的位置
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        // 从那个位置开始依次遍历,用新值代替出现的旧值
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) { // 直接for循环拷贝
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}

待续...updated 0115