likes
comments
collection
share

Java源码分析(一) -- String

作者站长头像
站长
· 阅读数 19

简介

String是一个引用数据类型,被final修饰,不可继承,不可改变原有字符的内容,当对原有字符进行改变操作的时候都会返回一个新的String对象,在jdk1.8String会根据不同的创建方式会存放在堆中或字符串常量池中。

常量

/** 用于存储字符串的字符数组 */
private final char value[];

/** 缓存字符串的hash值 */
private int hash;
  • value[]:用于存放String对象中的每一个字符。
  • hash:用于表示该字符串的hash值。

构造方法

/**
 * 构建一个空的字符串
 */
public String() {
    this.value = "".value;
}

/**
 * 根据指定的字符串值来构建一个字符串对象
 */
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

/**
 * 根据指定的char数组来构建字符串对象
 */
public String(char value[]) {
    // 将char数组中的值拷贝到一个新的char数组中
    // 并将新的char数组赋值给当前字符串对象中存储字符串的char数组
    this.value = Arrays.copyOf(value, value.length);
}

/**
 *
 * @param value 指定的char数组
 * @param offset 指定从char数组中的起始位置
 * @param count 从char数组中需要获取的长度
 */
public String(char value[], int offset, int count) {
    if (offset < 0) {
        // 起始位置小于0则抛出越界异常
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            // 获取的字符长度小于0则抛出越界异常
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            // 获取的字符长度等于0,起始位置小于char数组的长度则给与空字符串
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        // 在char数组中从offset的位置开始获取count数量的字符,已经超出了char数组剩余的数量
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    // 从char数组中的offset位置开始拷贝,拷贝结束位置为offset+count
    // 将拷贝的数据放到一个新的char数组中,并将新的char数组赋予当前字符对象中的char数组
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

/**
 * 将int数组中指定起始位置开始的元素转换为char
 * 如果int数组中的值超出了65535,那就需要在char数组中占据两个索引位置
 * @param codePoints
 * @param offset
 * @param count
 */
public String(int[] codePoints, int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= codePoints.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > codePoints.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    // 结束位置
    final int end = offset + count;

    // Pass 1: Compute precise size of char[]
    // 默认的char数组长度为所指定的获取元素的个数
    int n = count;
    for (int i = offset; i < end; i++) {
        int c = codePoints[i];
        // 校验是否是常用字符集
        if (Character.isBmpCodePoint(c))
            continue;
        // 校验是否是增补字符集
        else if (Character.isValidCodePoint(c))
            // 如果是增补字符集,那就需要在char数组中占据两个索引位置
            n++;
        else throw new IllegalArgumentException(Integer.toString(c));
    }

    // Pass 2: Allocate and fill in char[]
    // 初始化char数组
    final char[] v = new char[n];

    for (int i = offset, j = 0; i < end; i++, j++) {
        // 从int数组中获取指定索引位置的元素
        int c = codePoints[i];
        // 校验该元素是否是常用字符集,如果是常用字符集,就直接添加到新创建的char数组中去
        if (Character.isBmpCodePoint(c))
            v[j] = (char)c;
        else
            // 如果不是常用字符集,那就是增补字符集
            // 增补字符集就需要占据两个索引位置
            // 调用toSurrogates方法将增补字符集分别放在索引j和j+1的位置上
            // j的位置放置高位,j+1的位置放置低位
            Character.toSurrogates(c, v, j++);
    }

    this.value = v;
}

String的构造方法比较多,选择几个来看看就可以了。

  • String():创建一个空字符串的对象。

  • String(String original):根据指定的字符串对象来创建一个新的字符串对象,相当于拷贝了一个字符串对象。

  • String(char value[]):根据指定的char字符数组来构建一个新的字符串对象,最终通过Arrays的拷贝方法来将char数组中的字符拷贝到一个新的char字符数组,并将这个新的char字符数组赋给当前的String对象。

  • String(char value[], int offset, int count):从指定的char字符数组中的起始位置开始拷贝指定数量的字符给新的char字符数组,并将这个新的char字符数组赋给当前的String对象。

  • String(int[] codePoints, int offset, int count):从int数组中的指定起始索引位置开始拷贝指定数量的元素到新的char数组中去,我们先看该方法中的第一个for循环语句,end则是要拷贝的元素结束的索引位置,但不包括end索引位置,n则是所有拷贝的元素在新数组中所占用的索引位置个数,首先第一个for循环语句则会校验当前遍历到的索引位置上的元素是否是常用字符集,如果是常用字符集则占用一个索引位置,如果是增补字符集则会占用两个索引位置,第二个for循环则是将元素添加到char字符数组中去,如果是常用字符集则正常添加,如果是增补字符集则会将增补字符集的高低位分别放在索引jj++的位置上。

java中创建String的方式有两种,一个是用new String的方式来创建,一个是用字面量的方式直接给予String对象赋值,在jdk1.8中字符串常量池已经被移动到了堆中,用字面量的方式创建String对象则会在字符串常量池中生成一个字符串常量,如果使用的是new String的方式来创建则会在堆中创建一个String对象并且会将该字符串保存一份到字符串常量池中去。

public static void main(String[] args) {
    String str = "lihechuan";
}

我们在idea中可以通过jclasslib插件来看到上面代码的具体字节指令。

0 ldc #2 <lihechuan>
2 astore_1
3 return

首先通过ldc指令将常量池中#2所指向的常量lihechuan加载到操作数栈中,然后通过astore_1指令将操作数栈中的值存储到局部变量表中的索引1的位置上。

我们再来看一下用new String的方式创建的字符串。

public static void main(String[] args) {
    String str = new String("lihechuan");
}
 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <lihechuan>
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return

首先会通过new的指令在堆中创建一个String对象并放置操作数栈中,再通过dup的指令将创建的String对象拷贝一份并放置到操作数栈的栈顶,然后通过ldc指令将#3所指向的常量lihechuan加载到操作数栈中,然后通过invokespecial指令对常量和String对象进行初始化,在执行完invokespecial指令的时候常量lihechuan和拷贝的String对象都会出栈,初始化完成之后通过astore_1将最开始的String对象存放在局部变量表中的索引1的位置上。

equals

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数组
            char v1[] = value;
            // 传递进来的字符串所存放的char数组
            char v2[] = anotherString.value;
            int i = 0;
            // 依次比较两个char数组中的元素是否相同
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    // 当传递的参数不是一个字符串对象或者说当前字符串对象的长度与传递进来的字符串对象传递不一致
    return false;
}

equals方法用来比较两个字符串是否相同,我们来了解一下它是怎么进行比较的,首先会通过==符号来比较两个字符串的引用地址是否是同一个,当引用地址是同一个则说明两个字符串是相同的,当引用地址不是同一个的话则会校验传递的参数对象是否是一个字符串对象,如果不是一个字符串对象,那就没有必要进行比较了,如果是一个字符串对象就需要比较两个字符串对象的长度是否相同,只有两个字符串对象的长度相同才有继续比较的必要,然后依次遍历两个字符串对象中的char数组中的字符进行一个个的比较。

startsWith

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.
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    // 循环比较当前字符数组是否以指定的前缀开始的
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}

startsWith方法用来比较当前字符串对象是否以指定的字符串为前缀,该方法则是依次比较两个字符串中的所有字符,最大比较次数则是指定字符串的长度,如果其中有一个字符不相同则返回false,当所有的字符都比较完之后则说明是以指定的字符前缀开头。

endWith

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

我们可以看到endWith方法最终还是调用的startsWith方法,只不过传递的第二个参数不一样,使用当前字符串的长度减去指定字符串的长度,获取到从当前字符串开始比较的起始位置。

假设当前字符串的长度为5,指定字符串的长度为2,那就会从当前字符串的索引位置3开始进行比较。

Java源码分析(一)   --  String

indexOf

public int indexOf(int ch, int fromIndex) {
    // 当前字符数组长度
    final int max = value.length;
    if (fromIndex < 0) {
        fromIndex = 0;
    } else if (fromIndex >= max) {
        // Note: fromIndex might be near -1>>>1.
        return -1;
    }

    // 校验是否是常用字符集
    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
        // handle most cases here (ch is a BMP code point or a
        // negative value (invalid code point))
        // 如果是常用字符集,那就从字符数组依次比较
        // 如果存在则返回索引,如果不存在则返回-1
        final char[] value = this.value;
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == ch) {
                return i;
            }
        }
        return -1;
    } else {
        // 增补字符集
        return indexOfSupplementary(ch, fromIndex);
    }
}

/**
 * Handles (rare) calls of indexOf with a supplementary character.
 */
private int indexOfSupplementary(int ch, int fromIndex) {
    // 校验是否是一个有效的字符
    if (Character.isValidCodePoint(ch)) {
        // 获取当前字符数组
        final char[] value = this.value;
        // 获取高位字符
        final char hi = Character.highSurrogate(ch);
        // 获取低位字符
        final char lo = Character.lowSurrogate(ch);
        // 获取最大索引长度
        final int max = value.length - 1;
        // 遍历字符数组,依次校验i以及i+1索引位置上的元素是否与高位和低位相同
        // 相同则返回索引位置i
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == hi && value[i + 1] == lo) {
                return i;
            }
        }
    }
    return -1;
}

StringindexOf方法有多个,区别在于接收的参数类型不同,一个是int一个是String,能通过String类型的参数获取元素的索引位置很合理,但是为什么能通过int类型的参数获取到指定元素的索引位置呢?因为String中最终存放字符元素的是一个char数组,而char数组中存放的元素最终是与字符相对应的ASCLL码。

我们先来看一下带int类型参数的indexOf方法,带int类型参数的indexOf方法有两个,当前这个方法多了一个指定的起始索引位置,而没有带指定起始索引位置的indexOf方法则是调用了当前的这个方法,传递的起始索引位置为0,我们只需要看当前带了起始索引位置的方法即可,首先会校验指定的起始索引位置是否在有效范围内,只有指定的起始索引位置在有效范围内才会继续后续的操作,当在有效的范围内则会去校验该int类型的参数是否是一个常用的字符集,常用字符集的范围为0~65535,大部分字符都可以用这个范围中的数值来表示,当参数是一个常用字符集则会从指定起始索引位置开始遍历并比较,如果存在则返回该字符所在的索引位置,反之则返回-1

如果说传递的参数不是一个常用字符集,则说明该字符是一个增补字符集,此时就需要调用indexOfSupplementary方法来获取索引位置,我们来看一下该方法,首先会校验一下是否是一个有效的字符,而该有效字符的范围为0~1114111,当是一个有效的字符时则会通过方法获取该字符的高位和低位字符,然后从指定的起始索引位置开始比较ii+1的索引位置上的元素是否与高位和低位相同,相同则返回高位所在的索引位置。

public int indexOf(String str) {
    return indexOf(str, 0);
}

public int indexOf(String str, int fromIndex) {
    return indexOf(value, 0, value.length, str.value, 0, str.value.length, fromIndex);
}

/**
 * Code shared by String and StringBuffer to do searches. The
 * source is the character array being searched, and the target
 * is the string being searched for.
 *
 * @param   source       源字符数组
 * @param   sourceOffset offset of the source string.
 * @param   sourceCount  count of the source string.
 * @param   target       目标字符数组
 * @param   targetOffset offset of the target string.
 * @param   targetCount  count of the target string.
 * @param   fromIndex    起始索引位置
 */
static int indexOf(char[] source, int sourceOffset, int sourceCount,
        char[] target, int targetOffset, int targetCount, int fromIndex) {

    // 校验起始索引位置是否大于等于源字符数组的长度
    if (fromIndex >= sourceCount) {
        // 目标字符数组长度等于0则返回源字符数组的长度
        // 反之返回-1
        return (targetCount == 0 ? sourceCount : -1);
    }
    if (fromIndex < 0) {
        // 如果起始索引位置小于0则默认从0开始
        fromIndex = 0;
    }
    if (targetCount == 0) {
        // 目标字符数组的长度为0则返回起始索引位置
        return fromIndex;
    }
    // 获取目标数组起始索引位置上的元素
    char first = target[targetOffset];
    // 源数组长度减去目标数组长度
    // 如果为正数则是截至的索引位置元素
    // 如果为负数则说明目标数组长度大于源数组长度
    int max = sourceOffset + (sourceCount - targetCount);

    for (int i = sourceOffset + fromIndex; i <= max; i++) {
        /* 查找目标数组中的第一个字符是否在源数组中存在 */
        if (source[i] != first) {
            // 如果不存在则将源数组索引推进依次查找第一个字符
            while (++i <= max && source[i] != first);
        }

        // 校验i是否大于max,如果大于则说明源数组中不存在目标数组的字符
        if (i <= max) {
            // 进入该语句则说明查找到了第一个字符
            // i + 1 开始查找后续的字符
            int j = i + 1;
            // 计算索引比较的结束位置
            int end = j + targetCount - 1;
            // k 目标数组下一个字符的索引位置
            // j 源数组下一个字符的索引位置
            // 如果j的索引位置没有超出结束的索引位置时就比较源数组中j索引位置与目标数组k索引位置上的元素
            // 如果两个元素相同则推进,如果不相同则说明源数组中不包含目标数组的字符
            for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++);

            if (j == end) {
                // 如果j的索引位置等于结束位置则说明已经在源数组中找到了目标数组的字符
                // 返回目标数组中的字符在源数组中的起始位置
                return i - sourceOffset;
            }
        }
    }
    return -1;
}

我们看一下String类型参数的indexOf方法,可以看到最终调用的是一个私有的方法,首先该方法会先校验一下一些参数是否符合正常的逻辑,当符合正常的逻辑的时候才会继续执行,通过target[targetOffset]获取到从目标字符数组起始的索引位置上的元素,而targetOffset默认为0,其实就是从目标字符数组的头元素开始,然后再通过soruceOffset + ( sourceCount -targetCount )计算出第一个元素比较的截至的索引位置,而sourceOffset也是默认为0,sourceCount -targetCount则是源字符数组的长度减去目标字符数组的长度,假设sourceCount5,targetCount3,那计算出来的截至的索引位置为2,如果说计算出来的索引位置是一个负数,则说明目标字符数组中的元素比源字符数组的元素多,此时源字符数组中肯定是不会存在着与目标字符数组相同的元素。

再通过循环来依次比较,首先会查找目标字符数组中的第一个字符是否在源字符数组中存在,如果说目标字符数组中的第一个元素与源字符数组比较的索引位置超出了计算出来的截至的索引位置,则已经没有继续比较的必要了,因为源字符数组的剩余长度已经不够目标字符数组的长度了,只有找到了目标字符数组中的第一个字符且没有超出截至的索引位置则会继续执行后续的查找,j则是下一个要查找的索引位置,而end则是查找结束的索引位置,因为目标数组中的第一个字符已经在源字符数组中查找到了,此时就需要在源字符数组中找到目标字符数组中后续连续的字符,然后通过循环依次推进两个字符数组的索引位置进行元素的比较,最终j已经等于end则说明已经在源字符数组中找到了目标字符数组的元素,此时就会返回目标字符数组中的字符在源字符数组中的起始索引位置。

replace

public String replace(char oldChar, char newChar) {
    // 先校验替换的字符串是否跟被替换的字符串相同
    if (oldChar != newChar) {
        // 获取当前字符串的长度
        int len = value.length;
        int i = -1;
        // 获取当前字符数组
        char[] val = value;
        // 将当前字符数组中的每一个字符与被替换的字符比较是否相同
        // 如果相同则退出比较,执行后续的替换操作
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        // 校验索引i是否小于字符数组的长度
        // 如果小于字符数组的长度则说明已经找到了匹配的字符
        if (i < len) {
            // 创建一个len长度的新的字符数组
            char buf[] = new char[len];
            // 循环将旧字符数组中i之前的字符都添加到新的字符数组中
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                // 获取旧字符数组上索引i位置上的字符
                char c = val[i];
                // 如果旧字符数组i的索引位置上的字符是要被替换的字符
                // 就需要使用新的字符填入到新的字符数组中i的索引位置上
                // 反之就用原有的字符填入
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            // 将新创建的字符数组封装到字符对象中并返回
            return new String(buf, true);
        }
    }
    return this;
}

replace方法用来替换字符串中指定的字符,该方法首先会校验两个传递的参数是否相同,如果相同就没有必要进行替换了,只有不相同才会执行后续的操作,然后通过while循环从索引位置0开始依次比较当前字符数组中是否包含指定的被替换的字符,如果包含被替换的字符则记录下来该元素的索引位置(即首个需要被替换的元素索引位置),通过for循环将该索引位置之前的所有元素都拷贝到新的字符数组中去,再通过while循环将该索引位置上的元素以及后续需要替换的字符进行替换,后续不需要进行替换的字符则拷贝到新的字符数组中去,并将新的字符数组封装到String对象中去。

split

public String[] split(String regex, int limit) {
    char ch = 0;
    // 校验传递的字符长度是否等于1并且是指定的特殊符号
    if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
         // 校验传递的字符长度是否等于2并且是以\开头的
         (regex.length() == 2 && regex.charAt(0) == '\\' &&
          // 校验传递的字符第2位是否是数字或字母
          (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
          ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) &&
          // 校验字符是否不是增补字符集
        (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE))
    {
        int off = 0;
        int next = 0;
        boolean limited = limit > 0;
        ArrayList<String> list = new ArrayList<>();
        // 循环获取下一个需要切割的元素所在的索引位置
        while ((next = indexOf(ch, off)) != -1) {
            // 校验是否指定了切割方式,如果没有指定则将字符数组中所有指定的元素进行截取
            // 如果指定了则校验指定的截取数量是否超出
            if (!limited || list.size() < limit - 1) {
                // 通过索引位置对字符进行截取并将截取后的元素添加到集合中
                list.add(substring(off, next));
                // 下一个元素开始的索引位置
                off = next + 1;
            } else {
                // 对指定了切割方式的进行收尾工作
                list.add(substring(off, value.length));
                off = value.length;
                break;
            }
        }
        // 没有找到切割的元素
        if (off == 0)
            return new String[]{this};

        // 没有指定切割方式的时候
        // 当最后一次截取结束后则会继续通过while循环往后查找元素是否匹配
        // 此时后面的元素并不匹配,只能退出while循环
        // 当退出while循环之后还有一部分不匹配的元素并没有添加到集合中
        // 此时就需要将最后一次匹配到的元素索引位置后面所有的元素添加到集合中
        if (!limited || list.size() < limit)
            list.add(substring(off, value.length));

        // Construct result
        int resultSize = list.size();
        if (limit == 0) {
            // 取集合中有效的长度
            while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                resultSize--;
            }
        }
        // 创建指定长度的字符数组
        String[] result = new String[resultSize];
        // 截取有效元素的长度并返回一个新的子集,并将子集转换为字符数组
        return list.subList(0, resultSize).toArray(result);
    }
    // 通过正则来切割
    return Pattern.compile(regex).split(this, limit);
}

split方法用来对字符串进行切割,可以按指定的切割份数来进行切割,首先该方法有一个大的if判断,用来校验regex是正则表达式还是一个切割的字符,如果是正则表达式则会调用Pattern的方法来进行正则校验并切割。

我们主要看不是正则的情况下的代码,首先会校验一下limit是否大于0,大于0的情况下则说明已经指定了切割的方式,然后通过while循环寻找开始切割的元素所在的索引位置,再通过if语句来校验是否指定了切割方式,当没有指定切割方式则会按照指定的元素从头开始切割。

Java源码分析(一)   --  String

上面的图片则是未指定切割方式,而红色的则是切割完之后添加到了集合中,可以发现最后的一段元素没有被添加到集合中去。

当指定了切割的方式之后,则会校验集合中的元素是否超出指定的切割数量,如果没有超出则继续执行if里面的语句来对字符进行切割,按照上面的图片的步骤执行操作,当list.size的长度不小于limit-1了则说明是最后一次了,则需要执行else语句里面的操作进行收尾,就是将上面图片中最后的元素添加到集合中去。

再看后续操作if(off==0),当该条件成立则说明上面的while条件是不成立的,此时就需要将整个字符串返回。

如果说if(off==0)的条件不成立则说明已经对字符进行了切割,此时就需要校验一下是不是没有指定切割方式,因为在上面的时候说过没有指定切割方式的时候,会有最后的一段元素是没有被添加到集合中去的,此时就需要通过该校验来确定是否需要添加到集合中去。

当添加元素的操作都执行完毕之后则会通过集合中有效的长度计算出新的字符数组的长度,并截取集合中有效元素的长度并返回一个新的子集,再将子集转换为字符数组并返回。

转载自:https://juejin.cn/post/7287576723008634914
评论
请登录