Java源码解读扫盲【String篇】

一.String类的定义

    String类被定义为final类,意味着它不能被继承,它是个不可变类,并发程序最喜欢不可变量了。java

    String类实现了Serializable, Comparable<String>, CharSequence接口。面试

    Comparable接口有compareTo(String s)方法,CharSequence接口有length(),charAt(int index),subSequence(int start,int end)方法。正则表达式

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

二. String的属性

    String类中包含一个不可变的char数组用来存放字符串,一个int型的变量hash用来存放计算后的哈希值。(String没有length属性,只有length方法,面试笔试常考)。    数组

    String的字符串内容用的char数组来保存,而这个char数据变量也是final的,意味着字符串是不可变的,因此一个String一旦被声明定义则是不可变的。更改String内容的本质就是:查看常量池中是否有对应的字符串,若是有则直接把变量引用到这个字符串上,若没有,则新建一个字符串扔到常量池,而后对它进行变量引用。并发

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

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

/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

三.String构造函数

//不含参数的构造函数,通常没什么用,由于value是不可变量
public String() {
    this.value = new char[0];
}

//参数为String类型
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

//参数为char数组,使用java.utils包中的Arrays类复制
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

//从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value,在编码转换中比较经常使用
public String(byte bytes[], int offset, int length, String charsetName)
        throws UnsupportedEncodingException {
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

//调用public String(byte bytes[], int offset, int length, String charsetName)构造函数,在编码转换中比较经常使用
public String(byte bytes[], String charsetName)
        throws UnsupportedEncodingException {
    this(bytes, 0, bytes.length, charsetName);
}

其他的构造函数不经常使用。函数

四. String经常使用方法

    String类的方法不少,但不少都是互相调用的,这里看几个经常使用方法的源码。this

    1.equals

    String重写了Object的equals方法,能够发现比较的内容是字符串。先是看看是否同一个内存地址,而后再比较一下长度,最后再比较内容,很是严谨高效的逻辑。编码

  1. 内存地址相同,则为真。spa

  2. 若是对象类型不是String类型,则为假。不然继续判断。code

  3. 若是对象长度不相等,则为假。不然继续判断。

  4. 从后往前,判断String类中char数组value的单个字符是否相等,有不相等则为假。若是一直相等直到第一个数,则返回真。

public boolean equals(Object anObject) {
    //判断是否同一个内存地址
    if (this == anObject) {
        return 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;
            int i = 0;
            //逐一比较内容
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

    2.length()

    String类中获取字符串的长度经过length()方法,其实调用的是数组的length属性,因此String没有length属性。

public int length() {
    return value.length;
}

    3.substring

    substring返回的也是一个新的String对象,由于从0开始计算,其中不包括endIndex位置的字符,几乎全部String操做都会涉及new一个String对象,在有些场合记得从新引用,原来字符串的是不会改变的,因此能够想象常量池里面的内容是多么的庞大,特别是大型的企业级项目,若是不注意合理使用String类的话,GC是很是频繁的。

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);
}

    4.trim()

    String是如何去掉先后的空格的?就是截取先后不属于空格内容的部分。

public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */

    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

    5.replace()

    String替换方法replace是在char数组内进行的,先定位到要替换字符的位置,而后把不须要替换的部分复制到一个新的char数组内,把要替换的部分替换成新的字符,而后利用新的char数组生成一个新的String对象返回。这个方法值替换第一个位置,所有替换有replaceAll()方法,而这是用正则表达式匹配替换的。

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

        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                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;
}

    6.compareTo

    这个方法写的很巧妙,先从0开始判断字符大小。若是两个对象能比较字符的地方比较完了还相等,就直接返回自身长度减被比较对象长度,若是两个字符串长度相等,则返回的是0,巧妙地判断了三种状况。因此比较字符跟字符的长度没有什么关系,并非字符串长度大的就会返回>0。

public int compareTo(String anotherString) {
    //自身对象字符串长度len1
    int len1 = value.length;
    //被比较对象字符串长度len2
    int len2 = anotherString.value.length;
    //取两个字符串长度的最小值lim
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    //从value的第一个字符开始到最小长度lim处为止,若是字符不相等,返回自身(对象不相等处字符-被比较对象不相等字符)
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    //若是前面都相等,则返回(自身长度-被比较对象长度)
    return len1 - len2;
}

    7.hashCode

    String类重写了hashCode方法,Object中的hashCode方法是一个Native调用。String类的hash采用多项式计算得来,咱们彻底能够经过不相同的字符串得出一样的hash,因此两个String对象的hashCode相同,并不表明两个String是同样的。

public int hashCode() {
    int h = hash;
    //若是hash没有被计算过,而且字符串不为空,则进行hashCode计算
    if (h == 0 && value.length > 0) {
        char val[] = value;

        //计算过程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash赋值
        hash = h;
    }
    return h;
}

    8.startsWith

    起始比较和末尾比较都是比较常常用获得的方法,例如在判断一个字符串是否是http协议的,或者初步判断一个文件是否是mp3文件,均可以采用这个方法进行比较。

public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    //若是起始地址小于0或者(起始地址+所比较对象长度)大于自身对象长度,返回假
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    //从所比较对象的末尾开始比较
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}

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

public boolean endsWith(String suffix) {
    return startsWith(suffix, value.length - suffix.value.length);
}

 

总结

    String对象是不可变类型,返回类型为String的String方法每次返回的都是新的String对象,除了某些方法的某些特定条件返回自身。

    String对象的三种比较方式:

    ==内存比较:直接对比两个引用所指向的内存值,精确简洁直接明了。

    equals字符串值比较:比较两个引用所指对象字面值是否相等。

    hashCode字符串数值化比较:将字符串数值化。两个引用的hashCode相同,不保证内存必定相同,不保证字面值必定相同。

但愿看一遍源码,之后少在String中踩一下坑吧。

相关文章
相关标签/搜索