String源码

作者: xiaolyuh | 来源:发表于2018-04-10 16:07 被阅读6次

    总览

    微信截图_20180409112659.png

    在这里我们可以看出String主要实现了Serializable, Comparable和CharSequence接口。

    • Serializable:主要作用是实现序列化
    • Comparable:主要作用是String对象之间的比较
    • CharSequence:主要作用是为许多不同类型的字符序列(如String,StringBuffer和StringBuilder等)提供统一的只读访问(如获取长度的length方法,获取指定位置的字符charAt方法等)。

    属性

    我们可以看到String有两个比较总要的属性char[] value和int hash;

    • value:在这里我们可以看到String底层实现其实就是一个final修饰char数组。对Spring的一些操作也是基于这个value数组的操作。
    • hash:对应的String的hash值,默认是0 。

    String的不可变性

    String不可变性的原因:

    • 源码中String的本质是一个final类型的char数组,既然是final类型,那个该数组引用value就不允许再指向其他对象了,因此只从类的设计角度讲:如果jdk源码中并没有提供对value本身的修改,那么理论上来讲String是不可变的
    • 字符串池(String pool)的需求 在Java中,当初始化一个字符串变量时,如果字符串已经存在,就不会创建一个新的字符串变量,而是返回存在字符串的引用。 例如: String string1=”abcd”; String string2=”abcd”; 这两行代码在堆中只会创建一个字符串对象。如果字符串是可变的,改变另一个字符串变量,就会使另一个字符串变量指向错误的值。
    • 缓存字符串hashcode码的需要 字符串的hashcode是经常被使用的,字符串的不变性确保了hashcode的值一直是一样的,在需要hashcode时,就不需要每次都计算,这样会很高效。
    • 出于安全性考虑 字符串经常作为网络连接、数据库连接等参数,不可变就可以保证连接的安全性。

    方法分析

    compareTo 比较两个字符串大小

    这个方法的作用是按字母顺序比较两个字符串的大小。先是基于字符串中每个字符的Unicode值进行比较,如果前面部分一样那么就基于字符串长度进行比较。

    源码:

    public int compareTo(String anotherString) {
            int len1 = value.length;
            int len2 = anotherString.value.length;
            // (1) 获取两个字符串里较短的一个长度来作为循环的最大值
            int lim = Math.min(len1, len2);
            // (2) 将两个比较的字符串赋值给一个局部变量
            char v1[] = value;
            char v2[] = anotherString.value;
    
            int k = 0;
            while (k < lim) {
                char c1 = v1[k];
                char c2 = v2[k];
                if (c1 != c2) {
                    // 循环比较,如果在相同的下标位置两个字符不一样,直接比较字符的Unicode值
                    return c1 - c2;
                }
                k++;
            }
            // 如果两个字符串前面lim个字符都是一样的,那么直接比较两个字符串的长度
            return len1 - len2;
        }
    

    说明:

    1. 获取两个字符串里较短的一个长度来作为循环的最大值:这个的主要目的是防止在下面循环里较小的那个字符数组出现数组下标越界的异常。
    2. 将两个比较的字符串赋值给两个局部变量:这个的主要目的是减少getfield操作,提高性能。具体可以参考这篇文章

    流程图:

    String-compareTo.jpg

    hashCode 计算String对应的hash值

    hash的计算公式是: s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

    源码:

    public int hashCode() {
        // 将全局的hash值赋值给一个局部变量h,减少getfield操作
        int h = hash;
        // (1)根据hash值和value长度计算是否需要去计算hash值
        if (h == 0 && value.length > 0) {
            // (2) 将全局的value值赋值给一个局部变量val,减少getfield操作
            char val[] = value;
            // 根据公式s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]计算hash值
            for (int i = 0; i < value.length; i++) {
                 // (3) 这里为什么是31?为啥不是21或者3等等
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    

    说明:

    1. 根据hash值和value长度计算是否需要去计算hash值:如果以前已近计算过hash值那么以后的请求就不用再去计算一次了。
    2. 将全局的value值赋值给一个局部变量val,减少getfield操作:这个一步为啥不在if判断条件的上面,这样还可以减少一次getfield。我想主要目的是为了减少去创建局部变量val的开销,只有需要计算hash值的时候才回去创建局部变量val。
    3. 为什么需要用到31这个值来计算hash值:因为它是个奇素数,而且31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。
    • hashCode就是我们所说的散列码,使用hashCode算法可以帮助我们进行高效率的查找,例如HashMap。
    • 那为什么这里用31,而不是其它数呢?《Effective Java》是这样说的:之所以选择31,是因为它是个奇素数,如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的 好处并不是很明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。
    • 字符串hash函数,不仅要减少冲突,而且要注意相同前缀的字符串生成的hash值要相邻。

    流程图:

    String-hashCode.jpg

    equals 比较两个字符串是否相等

    当且仅当参数不为null并且是表示与此对象相同字符序列的String对象时,结果为true。

    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) {
                    // 判断同一下标位置上的char是否相等
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    

    流程图:

    String-equals.jpg

    replace 替换字符的方法

    该方法主要作用是返回由newChar替换此字符串中出现的所有oldChar所产生的字符串。返回的是一个新的串,替换之前的字符串不会改变。基于这个目的才后有后面的两个循环。

    源码:

    public String replace(char oldChar, char newChar) {
            // 如果新老字符一样就直接返回
            if (oldChar != newChar) {
                int len = value.length;
                int i = -1;
                char[] val = value; /* (1) 减少getfield操作,提升新能 */
                // (2) 遍历字符数组,找出第一个需要需要替换的char的下标
                while (++i < len) {
                    if (val[i] == oldChar) {
                        break;
                    }
                }
              
                if (i < len) {
                    // 如果有需要替换的字符出现才去新建一个新的buf[]
                    char buf[] = new char[len];
                    // (3) 将老字符串前i个char放到新的字符数组中
                    for (int j = 0; j < i; j++) {
                        buf[j] = val[j];
                    }
                    // 遍历第i个元素到最后一个元素的char数组
                    while (i < len) {
                        // (4)减少寻址次数
                        char c = val[i];
                        // 比较并替换char
                        buf[i] = (c == oldChar) ? newChar : c;
                        i++;
                    }
                    // 返回一个全新的串,最后结果对老串没有任何影响
                    return new String(buf, true);
                }
            }
            return this;
        }
    

    说明:

    1. 减少getfield操作:这个的主要目的是减少getfield操作,提高性能。具体可以参考这篇文章
    2. 遍历字符数组,找出第一个需要需要替换的char下标:主要目的是最大限度的减少比较并替换的开销,如果最后没有找到需要替换的char,我们后面的两个循环,还有新建一个全新的buf[]数组和全新的String操作都不会有。
    3. 将老字符串前i-1个char放到新的字符数组中:在上一个循环已经比较过前i-1个char了所以可以去掉比较哪一步,直接放到全新的一个buf[]中。
    4. 减少寻址次数:在下面的操作中有两次用到这个char,如果不建立一个局部变量的话就会有两次查询操作。

    流程图:

    String-replace.jpg

    concat 将两个字符串加起来

    将指定的字符串连接到此字符串的末尾。如果指定字符串长度为0,那么直接返回当前字符串。如果大于了则返回一个全新的字符串。

    public String concat(String str) {
        int otherLen = str.length();
        // 如果需要连接的字符串str长度等于零就直接返回当前String,不做后续操作
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        // 新建一个全新的char[] buf长度就是两个字符串的长度之和,并将当前value复制到新的字符数组。最后我们可以看到底层还是掉的System.arraycopy方法。
        char buf[] = Arrays.copyOf(value, len + otherLen);
        // 调用该方法将需要连接的字符串str加到新创建的buf数组中
        str.getChars(buf, len);
        // 返回一个全新的String对象
        return new String(buf, true);
    }
    
    /**
     * 将此字符串中的字符从dstBegin开始复制到dst中。 此方法不执行任何范围检查。
     */
    void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
    }
    

    lastIndexOf

    从fromIndex位置开始,倒序查找ch第一次出现的位置。

    先说一些基本的定义:

    Unicode

    Unicode是目前绝大多数程序使用的字符编码,定义也很简单,用一个码点(code point)映射一个字符。码点值的范围是从U+0000到U+10FFFF,可以表示超过110万个符号。下面是一些符号与它们的码点:

    • A的码点 U+0041
    • a的码点 U+0061
    • ©的码点 U+00A9
    • 的码点 U+2603
    • 💩的码点 U+1F4A9

    对于每个码点,Unicode还会配上一小段文字说明,可以在codepoints.net查到,比如 💩的码点说明

    Unicode最前面的65536个字符位,称为基本平面(BMP-—Basic Multilingual Plane),它的码点范围是从U+0000到U+FFFF。最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。 剩下的字符都放在补充平面(Supplementary Plane),码点范围从U+010000一直到U+10FFFF,共16个。

    UTF

    UTF(Unicode transformation format)Unicode转换格式,是服务于Unicode的,用于将一个Unicode码点转换为特定的字节序列。常见的UTF有

    UTF-8 可变字节序列,用1到4个字节表示一个码点
    UTF-16 可变字节序列,用2或4个字节表示一个码点
    UTF-32 固定字节序列,用4个字节表示一个码点

    UTF-8对ASCⅡ编码是兼容的,都是一个字节,超过U+07FF的部分则用了复杂的转换方式来映射Unicode,具体不再详述。

    UTF-16对于BMP的码点,采用2个字节进行编码,而BMP之外的码点,用4个字节组成代理对(surrogate pair)来表示。其中前两个字节范围是U+D800到U+DBFF,后两个字节范围是U+DC00到U+DFFF,通过以下公式完成映射(H:高字节 L:低字节 c:码点)
    H = Math.floor((c-0x10000) / 0x400)+0xD800
    L = (c – 0x10000) % 0x400 + 0xDC00

    比如💩用UTF-16表示就是”\uD83D\uDCA9″, 详情参考

    源码:

    public int lastIndexOf(int ch, int fromIndex) {
        // 判断ch是在基本平面BMP(大多数情况都在)
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // 大部分情况是没有超过的
            final char[] value = this.value;
            // 获取遍历数组的起始位置i
            int i = Math.min(fromIndex, value.length - 1);
            // 倒序遍历
            for (; i >= 0; i--) {
                // 找到对应的char直接返回
                if (value[i] == ch) {
                    return i;
                }
            }
            // 没找到返回 -1
            return -1;
        } else {
            // 到补充平面查找
            return lastIndexOfSupplementary(ch, fromIndex);
        }
    }
    

    toLowerCase

    通过给定的本地化规则(Locale)将String中的所有字符转换为小写。

    UTF-16描述

    Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符. Unicode的编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。 详情参考

    源码:

    public String toLowerCase(Locale locale) {
        if (locale == null) {
            throw new NullPointerException();
        }
        
        int firstUpper;
        final int len = value.length;
    
        // 先检查一下是否有需要替换的字符,这里的scan是一个标签,用于跳出循环。
        scan: {
            for (firstUpper = 0 ; firstUpper < len; ) {
                char c = value[firstUpper];
                 // 判断字符是否在辅助平面(大部分是不在的)
                if ((c >= Character.MIN_HIGH_SURROGATE)
                        && (c <= Character.MAX_HIGH_SURROGATE)) {
                    int supplChar = codePointAt(firstUpper);
                    // 使用UnicodeData文件中的大小写映射信息将字符supplChar(Unicode代码点)转换为小写字母,并比较转换后和转换前是否相等。
                    if (supplChar != Character.toLowerCase(supplChar)) {
                        // 如果不想等就直接中断循环,说明有字符需要进行转换。
                        break scan;
                    }
                    // Character.charCount(supplChar):表示指定字符(Unicode代码点)所需的char值的数量。
                    firstUpper += Character.charCount(supplChar);
                } else {
                    // 属于基本平面,每个字符站一个char
                    if (c != Character.toLowerCase(c)) {
                        break scan;
                    }
                    firstUpper++;
                }
            }
            return this;
        }
        // 查找到有需要替换的char所以新建一个result数组来存放替换后的char
        char[] result = new char[len];
        int resultOffset = 0;  /* result may grow, so i+resultOffset
                                * is the write location in result */
    
        /* Just copy the first few lowerCase characters. */
        // 将value数组前firstUpper部分复制到新的result中
        System.arraycopy(value, 0, result, 0, firstUpper);
    
        String lang = locale.getLanguage();
        boolean localeDependent =
                (lang == "tr" || lang == "az" || lang == "lt");
        char[] lowerCharArray;
        int lowerChar;
        int srcChar;
        int srcCount;
        // 开始真正的做转换操作
        for (int i = firstUpper; i < len; i += srcCount) {
            srcChar = (int)value[i];
            if ((char)srcChar >= Character.MIN_HIGH_SURROGATE
                    && (char)srcChar <= Character.MAX_HIGH_SURROGATE) {
                srcChar = codePointAt(i);
                srcCount = Character.charCount(srcChar);
            } else {
                srcCount = 1;
            }
            if (localeDependent ||
                    srcChar == '\u03A3' || // GREEK CAPITAL LETTER SIGMA
                    srcChar == '\u0130') { // LATIN CAPITAL LETTER I WITH DOT ABOVE
                lowerChar = ConditionalSpecialCasing.toLowerCaseEx(this, i, locale);
            } else {
                lowerChar = Character.toLowerCase(srcChar);
            }
            if ((lowerChar == Character.ERROR)
                    || (lowerChar >= Character.MIN_SUPPLEMENTARY_CODE_POINT)) {
                if (lowerChar == Character.ERROR) {
                    lowerCharArray =
                            ConditionalSpecialCasing.toLowerCaseCharArray(this, i, locale);
                } else if (srcCount == 2) {
                    resultOffset += Character.toChars(lowerChar, result, i + resultOffset) - srcCount;
                    continue;
                } else {
                    lowerCharArray = Character.toChars(lowerChar);
                }
    
                /* Grow result if needed */
                int mapLen = lowerCharArray.length;
                if (mapLen > srcCount) {
                    char[] result2 = new char[result.length + mapLen - srcCount];
                    System.arraycopy(result, 0, result2, 0, i + resultOffset);
                    result = result2;
                }
                for (int x = 0; x < mapLen; ++x) {
                    result[i + resultOffset + x] = lowerCharArray[x];
                }
                resultOffset += (mapLen - srcCount);
            } else {
                result[i + resultOffset] = (char)lowerChar;
            }
        }
        return new String(result, 0, len + resultOffset);
    }
    

    trim

    去掉字符串头尾的空字符

    源码:

    public String trim() {
        // 获取字符串长度
        int len = value.length;
        int st = 0;
        // 减少getfield操作,提升性能
        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;
    }
    

    参考

    http://www.cnblogs.com/MUMO/p/5663947.html
    http://www.alloyteam.com/2016/12/javascript-has-a-unicode-sinkhole/
    https://blog.csdn.net/mazhimazh/article/details/17708001
    https://www.cnblogs.com/lanelim/p/4964947.html
    http://youngxhui.github.io/2016/09/11/String%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/

    相关文章

      网友评论

        本文标题:String源码

        本文链接:https://www.haomeiwen.com/subject/vkgbhftx.html