Java源码分析(一) String

2023年 10月 9日 92.1k 0

简介

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 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 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 
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 
 3 dup
 4 ldc #3 
 6 invokespecial #4 
 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  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开始进行比较。

startsWith和endWith比较的步骤.png

indexOf

public int indexOf(int ch, int fromIndex) {
    // 当前字符数组长度
    final int max = value.length;
    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  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语句来校验是否指定了切割方式,当没有指定切割方式则会按照指定的元素从头开始切割。

split未指定切割方式.png

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

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

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

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

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

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论