String源码阅读(三)

常用的几个方法

length()

public int length() {
    return value.length >> coder();
}
byte coder() {
    //这里根据压缩标识,来返回byte值,其中UTF16的值是1,coder是不确定的
    //但是可以确定的是,coder的值跟对应编码方式下一个字符所占的byte数组长度有关
    //例如:UTF16类型的数据,一个字符会占据两个byte数组位,所以计算长度时需要将byte数组长度缩小2倍
    return COMPACT_STRINGS ? coder : UTF16;
}

它返回的是字符串的长度,length的数值其实是字符串中Unicode的code units,即:编码单元数目。

isEmpty()

public boolean isEmpty() {
    return value.length == 0;
}

char charAt(int index)

       返回字符串中指定下标index所在的那个字符,index的取值方位必须是0到length-1的方位,并且字符串的第一个字符的下标是0,也就是说是从0开始计数。

public char charAt(int index) {
    //判断是否是压缩格式版的字符串
    if (isLatin1()) {
        //压缩版:放弃高八位
        return StringLatin1.charAt(value, index);
    } else {
        //同时保留高八位和低八位
        return StringUTF16.charAt(value, index);
    }
}
private boolean isLatin1() {
    return COMPACT_STRINGS && coder == LATIN1;
}
//StringLatin1.charAt
public static char charAt(byte[] value, int index) {
    if (index < 0 || index >= value.length) {
        throw new StringIndexOutOfBoundsException(index);
    }
    //这里使用了0xff,它就是十进制的255,二进制就是11111111
    //对于Java中,byte类型转成int,高位会随机填充值,通过它来保证低位可靠性,并且放弃高位数
    //因为&操作中,超过0xff的部分,全部都会变成0,而对于0xff以内的数据,它不会影响原来的值
    return (char)(value[index] & 0xff);
}
//StringUTF16.charAt
public static char charAt(byte[] value, int index) {
    checkIndex(index, value);
    return getChar(value, index);
}
static char getChar(byte[] val, int index) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;
    //HI_BYTE_SHIFT和LO_BYTE_SHIFT跟当前系统环境有关,存在一个大端还是小端的判定isBigEndian()
    //所谓大小端:大端就是高字节存储在低位,低字节存储在高位,小端正好与之想反
    return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                  ((val[index]   & 0xff) << LO_BYTE_SHIFT));
}
static {
    if (isBigEndian()) {
        HI_BYTE_SHIFT = 8;
        LO_BYTE_SHIFT = 0;
    } else {
        HI_BYTE_SHIFT = 0;
        LO_BYTE_SHIFT = 8;
    }
}

       仔细考虑上面的StringUTF16.charAt方法,它有一个index<<=1的操作,之所以会这样,也是跟byte存储有关,因为StringUTF16不会丢弃任何一位,所以它的byte数组相邻两个位置一个存储高八位,一个存储低八位。所以任何传入的index,如果换算成byte数组中的位置需要将其值扩展成二倍作为index的开始,并且byte数组存储这些数据的时候,将高八位放在数组前一个位置,低八位放在数组的后一个位置,形成<hight, low>类型的数据。再根据大端小端确认到底是高八位需要移动还是低八位需要移动。

boolean equals(Object anObject)

       String重写了equals方法,当且仅当传入的参数是一个String类型,并且其中的字符序列与调用对象中的字符序列相同才会返回true。

public boolean equals(Object anObject) {
    //首先用==比较,如果相等,说明就是同一个对象,肯定是相等的
    if (this == anObject) {
        return true;
    }
    //前置判断:必须是String类型,否则肯定不相等
    if (anObject instanceof String) {
        String aString = (String)anObject;
        //coder方法其实就是获取字符串采用的编码方式,如果编码方式都不一样,肯定结果为false
        if (coder() == aString.coder()) {
            //根据数据是否是压缩数据,采用不同的比较方式
            //数据压缩弃了高八位,一个八位就只占据一个byte数组位
            //如果是非压缩版,一个字符对应两个byte数组位
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}
byte coder() {
    return COMPACT_STRINGS ? coder : UTF16;
}
private boolean isLatin1() {
    return COMPACT_STRINGS && coder == LATIN1;
}
//StringLatin1.equals
public static boolean equals(byte[] value, byte[] other) {
    //首先需要保证byte数组长度必定相同
    if (value.length == other.length) {
        //逐个比对两个数组内部相同下标上的内容
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
}
//StringUTF16.equals
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        int len = value.length >> 1;
        for (int i = 0; i < len; i++) {
            //这里使用的getChar方法就是前面charAt方法中的那个getChar方法
            //其实就是根据index和byte获取高位和低位拼接后得到的字符,然后比较char是否不同
            if (getChar(value, i) != getChar(other, i)) {
                return false;
            }
        }
        return true;
    }
    return false;
}

indexOf 方法

       indexOf方法有很多重载的方法,该方法的作用是可以获取一个字符在字符串中的位置,不同的重载方法只是针对这个功能做了不同的业务划分,下面逐个讨论。

//这个是经常使用的一种,传入一个字符串,获取字符串在源字符串中第一次出现的位置
//注意,这里有个情况,就是字符串可以是多个字符,而不单单只是一个字符的字符串
public int indexOf(String str) {
    if (coder() == str.coder()) {
        return isLatin1() ? StringLatin1.indexOf(value, str.value)
            : StringUTF16.indexOf(value, str.value);
    }
    if (coder() == LATIN1) {  // str.coder == UTF16
        return -1;
    }
    return StringUTF16.indexOfLatin1(value, str.value);
}
//StringLatin1.indexOf
public static int indexOf(byte[] value, byte[] str) {
    //如果传入的是一个空字符串,默认直接返回0
    if (str.length == 0) {
        return 0;
    }
    //如果源字符串本身是一个空字符串,返回-1
    if (value.length == 0) {
        return -1;
    }
    return indexOf(value, value.length, str, str.length, 0);
}
public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) {
    byte first = str[0];
    //这里的max计算可以这么理解:
    //源byte长度减去待比较byte数组长度,得到的值是可以完全包含待比较数组的最大index
    //换句话说,如果超出了这个值,那么剩余的byte位数与待比较的byte数组长度就不一致了,肯定不具有可比性了
    int max = (valueCount - strCount);
    for (int i = fromIndex; i <= max; i++) {
        //查询第一个源byte数组的开始位字符内容是否与待比较数据的首位是否相等
        if (value[i] != first) {
            //如果不相等就继续寻找源数组的下一位,直至找到与待比较数组的首位内容相等的位置
            while (++i <= max && value[i] != first);
        }
        // 到此处,说明已经发现了一位相等的数据,或者i的值已经超出max
        if (i <= max) {
            //排除i超出max的可能,剩下的就是在已经找到的首字符的基础上,继续查询余下的字符
            //所以这里的j是从i所示的位置的下一位开始,结束的index为开始位+待比较数组长度-1
            int j = i + 1;
            int end = j + strCount - 1;
            //一个没有循环体的循环,为的就是比较待比较数组中剩余的内容与源数组i之后的内容,
            //连续strCount位之内的数据是否相等,如果完全相等,i就是需要的值
            //中间若存在不同的情况,说明待比较数组不是源数组的一个子数组,继续循环直至结束返回-1
            for (int k = 1; j < end && value[j] == str[k]; j++, k++);
            if (j == end) {
                // Found whole string.
                return i;
            }
        }
    }
    return -1;
}

上面这种是经常使用的一种,此外还有一些不常使用的方式:

public int indexOf(String str, int fromIndex) {
    return indexOf(value, coder(), length(), str, fromIndex);
}
static int indexOf(byte[] src, byte srcCoder, int srcCount,
                       String tgtStr, int fromIndex) {
    byte[] tgt    = tgtStr.value;
    byte tgtCoder = tgtStr.coder();
    int tgtCount  = tgtStr.length();
    if (fromIndex >= srcCount) {
        //指定的开始查找的位置不小于源字符序列的最大长度,
        return (tgtCount == 0 ? srcCount : -1);
    }
    //开始位置为负数,其实就是从头开始查找
    if (fromIndex < 0) {
        fromIndex = 0;
    }
    //待查询的字符串是一个空串,直接返回查询的起始位置
    if (tgtCount == 0) {
        return fromIndex;
    }
    if (tgtCount > srcCount) {
        return -1;
    }
    if (srcCoder == tgtCoder) {
        //这里用到的indexOf方法就是上一段代码中展示的那个indexOf方法
        return srcCoder == LATIN1
            ? StringLatin1.indexOf(src, srcCount, tgt, tgtCount, fromIndex)
            : StringUTF16.indexOf(src, srcCount, tgt, tgtCount, fromIndex);
    }
    if (srcCoder == LATIN1) {    //  && tgtCoder == UTF16
        return -1;
    }
    // srcCoder == UTF16 && tgtCoder == LATIN1) {
    return StringUTF16.indexOfLatin1(src, srcCount, tgt, tgtCount, fromIndex);
}

lastIndexOf(byte[] src, byte srcCoder, int srcCount,String tgtStr, int fromIndex)

       这个方法是在常用的lastIndexOf((String str, int fromIndex)方法的底层调用方法,作用就是为了获取从fromIndex位置开始,tgtStr字符串在src字符串中最后一次出现的数组下标。

static int lastIndexOf(byte[] src, byte srcCoder, int srcCount,
                       String tgtStr, int fromIndex) {
    //将待查找的字符串转换成byte数组
    byte[] tgt = tgtStr.value;
    //获取字符编码格式
    byte tgtCoder = tgtStr.coder();
    //待查找字符串的长度(字符串长度,非byte数组长度)
    int tgtCount = tgtStr.length();
    //获取字符下标能够达到的最右端值,超过该值src中剩余的字符串长度就比tgtStr长度短了
    int rightIndex = srcCount - tgtCount;
    //这里不像indexOf,一旦超过了,强制将其变成能够达到的最大下标值
    if (fromIndex > rightIndex) {
        fromIndex = rightIndex;
    }
    //如果传入的开始查询的位置为负数,直接返回-1
    if (fromIndex < 0) {
        return -1;
    }
    //如果待查找的字符串是一个空串,始终都能查到,返回值就是传入的fromIndex值
    if (tgtCount == 0) {
        return fromIndex;
    }
    if (srcCoder == tgtCoder) {
        //这里StringLatin1.lastIndexOf方法下面已经贴出来了
        //至于StringUTF16.lastIndexOf就不贴了,因为代码逻辑基本一致,只是对于tgt数组长度有了扩展
        return srcCoder == LATIN1
            ? StringLatin1.lastIndexOf(src, srcCount, tgt, tgtCount, fromIndex)
            : StringUTF16.lastIndexOf(src, srcCount, tgt, tgtCount, fromIndex);
    }
    if (srcCoder == LATIN1) {    // && tgtCoder == UTF16
        return -1;
    }
    // srcCoder == UTF16 && tgtCoder == LATIN1
    return StringUTF16.lastIndexOfLatin1(src, srcCount, tgt, tgtCount, fromIndex);
}
//StringLatin1.lastIndexOf
public static int lastIndexOf(byte[] src, int srcCount,
                                  byte[] tgt, int tgtCount, int fromIndex) {
    int min = tgtCount - 1;
    //计算src数组下标查询上限值
    int i = min + fromIndex;
    //strLastIndex用于后面获取tgt数组中最后一个字符的内容
    int strLastIndex = tgtCount - 1;
    //因为是StringLatin1,所以直接可以将对应byte数组中的内容强制转换成char
    //& 0xff操作则保证了转换成的char高位能够正确,确保结果的准确性
    char strLastChar = (char)(tgt[strLastIndex] & 0xff);
    startSearchForLastChar:
    while (true) {
        //不停在rc数组中从后往前找字符与tgt数组的最后一个字符进行对比
        while (i >= min && (src[i] & 0xff) != strLastChar) {
            i--;
        }
        //若退出上面的while循环,则只有两种可能:
        //i < min:查找失败
        //(src[i] & 0xff) == strLastChar:正好找到
        if (i < min) {
            return -1;
        }
        //到了此处,说明已经找到了匹配的字符
        //后面的就是不停地继续在src中倒序查询tgt数组中倒数第二个字符,第三个字符。。以此类推
        int j = i - 1;
        int start = j - strLastIndex;
        //用于获取tgt数组在上一个字符位置基础上前移一位的字符
        int k = strLastIndex - 1;
        while (j > start) {
            if ((src[j--] & 0xff) != (tgt[k--] & 0xff)) {
                i--;
                continue startSearchForLastChar;
            }
        }
        return start + 1;
    }
}

substring(int beginIndex)

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = length() - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    if (beginIndex == 0) {
        return this;
    }
    //下面这句是求取子串的核心逻辑
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                      : StringUTF16.newString(value, beginIndex, subLen);
}

       针对上面方法的最后return语句,因为涉及到是否丢弃高八位的情况,所以采用了不同的方式进行子串的拷贝工作,这两种方式虽然代码有些不同,但最终调用的都是Arrays工具类中的copyOfRange方法,这个方法最终会调用System.arraycopy方法,它是一个native方法,效率很高。

       生成子串的大致逻辑就是:将源字符串对应的byte数组按照beginIndex开始位置和需要截取的长度,让后将返回的byte数组通过String构造器构造一个新的字符串对象。这个过程因为会涉及到编码的问题,所以StringLatin1和StringUTF16的处理会有所不同,StringLatin1就是正常的截取逻辑然后构建,但是StringUTF16在构造子串的时候,如果压缩标识位为true,就需要对子串进行压缩操作,其实就是将其高八位丢弃,然后就是跟StringLatin1操作类似就行了。如果压缩标识位为false,在截取的时候需要对截取的初始位置以及长度都要扩大一倍,即左移一位,然后再进行拷贝,在拷贝的时候,需要额外注意,每个字符占两个byte数组位。

hashCode()

       我电脑上当前的JDK版本中,hash码的计算分了两种情况:StringLatin1计算和StringUTF16计算,即:分为压缩情况下的hash计算和非压缩的hash计算。

在JDK的注释中,提出了一个hash的计算公式:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
//其实上面的也可以看成是:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]*31^0

       这里s就代表字符串内部的字符数组,n代表字符串的长度,这个是整体的计算思路,但是JDK中字符串底层使用的是byte数组,所以字符还牵扯到具体的存储格式问题,存在格式压缩的情况。这里使用31数字,网上有选用这个数计算的各种原因,可以查阅相关博客了解具体内容,这里只要记住一点:31这个数是奇素数,它可以保证hashCode尽可能大的基础上,虚拟机在计算的时候还会有一定的优化,要知道:31 * n == (n << 5) - n这个等式的结果是true,它是成立的。

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}
//StringLatin1.hashCode
//对于压缩版的数据,它的字符长度其实与value的长度是一致的,直接套用上面的公式就可以了
public static int hashCode(byte[] value) {
    int h = 0;
    //for循环结束之后,就可以得到一个赋值公式:
    //value[0]*31^(n-1) + ... + value[n - 1]
    for (byte v : value) {
        h = 31 * h + (v & 0xff);
    }
    return h;
}
//StringUTF16.hashCode
//大致的算法没变,只是需要处理以下value数组中内容,数组中连续两个位置合在一起才表示一个字符
//getChar方法的作用就是根据i值获取i以及i+1位置上的两个byte数据,并且合并成一个char返回
public static int hashCode(byte[] value) {
    int h = 0;
    int length = value.length >> 1;
    for (int i = 0; i < length; i++) {
        h = 31 * h + getChar(value, i);
    }
    return h;
}    

compareTo(String anotherString)

public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    if (coder() == anotherString.coder()) {
        return isLatin1() ? StringLatin1.compareTo(v1, v2)
                          : StringUTF16.compareTo(v1, v2);
    }
    return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                      : StringUTF16.compareToLatin1(v1, v2);
 }

       compareTo是String实现的Comparable接口中的方法,逻辑比较简单,就是将两个字符串对应的byte数组进行比较,逐个byte位置进行比较,不同的是StringUTF16是两两byte位置进行比较,所以会有一个转换操作,将连续两个byte数组中的内容组合起来转成char类型再进行比较,但是最终的逻辑都是一样,逐个比较String里面的字符。

intern()

       它是一个native方法,它会返回一个规范化表示的字符串对象。字符串常量池是由String自行维护的,它初始时是空的。如果intern方法被调用,此时若常量池中已经存在一个字符串对象与调用方法的对象equals相等,那么常量池中的字符串对象将被返回,否则该对象将被添加到常量池中并返回在常量池中的引用。

       任意两个字符串s和t,如果s.intern() == t.intern()的结果为true,那么s.equals(t)的结果必然也是true。所有的字符串字面量和字符串常量表达式都会在常量池中。

       因为篇幅有限,其他方法就不再一一介绍了,在前面介绍的基础上,基本看看源码都能大致了解的差不多。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 一、Java 简介 Java是由Sun Microsystems公司于1995年5月推出的Java面向对象程序设计...
    子非鱼_t_阅读 4,082评论 1 44
  • 前言 最先接触编程的知识是在大学里面,大学里面学了一些基础的知识,c语言,java语言,单片机的汇编语言等;大学毕...
    oceanfive阅读 2,988评论 0 7
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,334评论 0 4
  • 第一天. “妈没啥文化,给你取名叫路颜,也不像其他人那么高级的名字,就希望你的人生路走得顺顺当当,也希望你能有五彩...
    清汤圆子er阅读 417评论 0 1
  • 当夜,主站在保罗旁边,说:“放心吧!你怎样在夜耶路撒冷为我做见证,也必怎样在罗马为我做见证。”
    小王瑞阅读 730评论 0 0