004_java.lang.String源码解析

java.lang.String

继承结构与类声明

image.png

public final class String implements java.io.Serializable, 
    Comparable<String>, CharSequence{
    private static final long serialVersionUID = -6849794470754667710L;
}

可以看到 String类是final类,不可以被继承的,并且他的成员方法默认是final方法。

重要属性

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

/** The offset is the first index of the storage that is used. */
private final int offset;

/** The count is the number of characters in the String. */
private final int count;

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

可以看到String底层实际上是使用char的数组来承载数据的,使用private修饰,不提供直接修正value的方法。所有对String的数据产生影响的方法都会重新产生一个新的对象。
其中,offset表示char从哪个下标开始是有效的,count则记录当前有效的字符长度。

创建字符串实例

字面常量创建

@Test
public void test001(){
    String a = "123";
    System.out.println(a);
}

查看其字节码:

 0 ldc #2 <123>
 2 astore_1
 3 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
 6 aload_1
 7 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
10 return

ldc指令:将常量值(如字符串、数字、类引用等)加载到操作数栈上。

可以看到"123"的数据直接来自于字节码常量池,它的背后执行逻辑是先会从常量池中判断是否存在"123"对象,如果不存在,则会在常量池中创建该对象,并且返回常量池中"123"对象的引用给到操作数栈;如果之前常量池存在"123"的话,则直接返回常量池中"123"的对象引用。

使用byte[]初始化

使用byte[]数组初始化相关的构造器如下,这里还有offset,length,Charset的组合。这里只需要看最后一个方法就可以。

public String(byte bytes[]) {
    this(bytes, 0, bytes.length);
}
public String(byte bytes[], int offset, int length) {
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}
public String(byte bytes[], Charset charset) {
    this(bytes, 0, bytes.length, charset);
}
public String(byte bytes[], String charsetName)
        throws UnsupportedEncodingException {
    this(bytes, 0, bytes.length, charsetName);
}

public String(byte bytes[], int offset, int length, Charset charset) {
    if (charset == null)
        throw new NullPointerException("charset");
    checkBounds(bytes, offset, length);
    this.value =  StringCoding.decode(charset, bytes, offset, length);
}

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

可以看到其逻辑是:
(1)针对字节编码参数校验
(2)针对offset与length进行数组越界校验
(3)委托StringCoding进行编码,将字节数组转换为字符数组

使用char[]初始化

使用char[]数组作为String的入参,由于String底层使用的就是char[],因此中间并未产生转换逻辑。仅做了针对offset与length进行数组越界校验。

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

使用int[]初始化

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[]
    int n = count;
    for (int i = offset; i < end; i++) {
        int c = codePoints[i];
        if (Character.isBmpCodePoint(c))
            continue;
        else if (Character.isValidCodePoint(c))
            n++;
        else throw new IllegalArgumentException(Integer.toString(c));
    }

    // Pass 2: Allocate and fill in char[]
    final char[] v = new char[n];

    for (int i = offset, j = 0; i < end; i++, j++) {
        int c = codePoints[i];
        if (Character.isBmpCodePoint(c))
            v[j] = (char)c;
        else
            Character.toSurrogates(c, v, j++);
    }

    this.value = v;
}

这里先需要理解什么是codePoint:

@Test
public void test003(){
    String a = new String("12😄4");

    // 打印5
    System.out.println(a.length());
    // 打印4
    System.out.println(a.codePoints().count());

}

初始化了一个字符串:“12😄4”,这个字符串混入一个奇怪的表情。而这个表情得用4个字节才能表达。因此我们debug之后,会出现这样的情况:
image.png
可以看到,字符数组第3-4号位置由于截断的关系产生了乱码,而3-4号的字节加在一起才能完整表示一个表情。因此String#length实际上会返回char[]的长度,而codePoint才真正反映了其字数。一般我们不用关心这个,但是如果你的数据里面会出现类似表情需要4个字节进行编码的场合就需要使用codePoint进行处理。
回到上面的源码,处理codePoint核心是使用Character的一些方法:
Character#isBmpCodePoint 是判断其int值是否高16位是空值,如果是则认定其需要4个字节进行编码。

public static boolean isBmpCodePoint(int codePoint) {
    return codePoint >>> 16 == 0;
}

判定需要是四个字节进行编码的时候,j会++两次实现跳索引,并会用toSurrogates方法进行填充

for (int i = offset, j = 0; i < end; i++, j++) {
    int c = codePoints[i];
    if (Character.isBmpCodePoint(c))
        v[j] = (char)c;
    else
        Character.toSurrogates(c, v, j++);
}

// Character#toSurrogates
static void toSurrogates(int codePoint, char[] dst, int index) {
    // We write elements "backwards" to guarantee all-or-nothing
    dst[index+1] = lowSurrogate(codePoint);
    dst[index] = highSurrogate(codePoint);
}

使用对象初始化

例如下面的例子:

String a = new String("123");

查看其字节码:

 0 new #5 <java/lang/String>
 3 dup
 4 ldc #2 <123>
 6 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
10 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
13 aload_1
14 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
17 return

可以看到先创建一个字符串实例,然后从常量池内获取"123"字符串,然后调用字符串实例的构造方法。
除了使用String对象作为入参,也可以使用StringBuffer与StringBuilder作为数据承载体进行初始化。


public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}

public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

其中StringBuffer是非并发安全的,因此做了一下锁。

常用方法

字符串基本信息方法

我们知道String底层就是个char[],那么封装之后是需要探知其信息的,因此会有如下方法:

  • char charAt (int index) 返回index所指定的字符
  • int length() 返回字符串的长度
  • char[] toCharArray 将字符串转换成字符数组
public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

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

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

字符串测试方法

equals 方法
public boolean equals(Object anObject) {
    // 对象引用相同直接返回 true
    if (this == anObject) {
        return true;
    }
    // 判断需要对比的值是否为 String 类型,如果不是则直接返回 false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            // 把两个字符串都转换为 char 数组对比
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 循环比对两个字符串的每一个字符
            while (n-- != 0) {
                // 如果其中有一个字符不相等就 true false,否则继续对比
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
compareTo 方法

compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值,源码如下:

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    // 获取到两个字符串长度最短的那个 int 值
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
    int k = 0;
    // 对比每一个字符
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            // 有字符不相等就返回差值
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}

从源码中可以看出,compareTo() 方法会循环对比所有的字符,当两个字符串中有任意一个字符不相同时,则 return char1-char2。比如,两个字符串分别存储的是 1 和 2,返回的值是 -1;如果存储的是 1 和 1,则返回的值是 0 ,如果存储的是 2 和 1,则返回的值是 1。
还有一个和 compareTo() 比较类似的方法 compareToIgnoreCase(),用于忽略大小写后比较两个字符串。

private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();

public int compareToIgnoreCase(String str) {
    return CASE_INSENSITIVE_ORDER.compare(this, str);
}
contains 方法

查询字符串中是否包含另一个字符串,可以看到内部调用的是indexOf方法。

public boolean contains(CharSequence s) {
    return indexOf(s.toString()) > -1;
}
endWith 与 startWith
public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}

public boolean endsWith(String suffix) {
    return startsWith(suffix, value.length - suffix.value.length);
}
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;
}

可以看到startWith与endWith都调用的同一个底层方法,逻辑简单,就是从toffeset开始进行遍历看是否可以将prefix完整遍历完。

字符串查找方法

indexOf是用来查找入参在字符串内的位置的,如果没有找到则返回-1。

indexOf有多个重载方法:

public int indexOf(int ch) {}
public int indexOf(int ch, int fromIndex) {}

public int indexOf(String str) {}
public int indexOf(String str, int fromIndex) {}

static int indexOf(char[] source, 
                   int sourceOffset, 
                   int sourceCount,
                   String target, 
                   int fromIndex) {}
static int indexOf(char[] source, 
                   int sourceOffset, 
                   int sourceCount,
                   char[] target, 
                   int targetOffset, 
                   int targetCount,
                   int fromIndex){}

先来看第一组,使用int作为入参进行查找的方法:

public int indexOf(int ch) {
    return indexOf(ch, 0);
}

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

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;
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == hi && value[i + 1] == lo) {
                return i;
            }
        }
    }
    return -1;
}

String底层使用char,但是这里使用int进行匹配,为的就是兼容码点数据。判断是否为码点核心就是这句:

if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
    ...
}

// Character.MIN_SUPPLEMENTARY_CODE_POINT
public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;

可以看到,只要ch的确只能超过2个字节存储的场合下就会判定为码点。从而使用indexOfSupplementary进行查找。indexOfSupplementary的代码也很简单,相当于将int拆分为高位低位然后从0开始到value.length-1进行遍历,每次比较两个字节。
我们再来看看第二组,使用字符串进行匹配的逻辑,可以看到实际上内部使用的是static方法,也就是上面的第三套查找方式。

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

static int indexOf(char[] source, int sourceOffset, int sourceCount,
                   char[] target, int targetOffset, int targetCount,
                   int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }

        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

可以看到,先是做了边界值判定,然后取出第一个char,将目标字符串窗口往后移动,直到第一个char匹配后,则开始目标字符串剩下部分的匹配。如果没有匹配上则重复将目标字符串窗口往后移动。
而lastIndexOf方法和indexOf的实现大同小异,lastIndexOf是从尾部开始遍历的。

public int lastIndexOf(int ch) {
    return lastIndexOf(ch, value.length - 1);
}
public int lastIndexOf(int ch, int fromIndex) {
    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
        // handle most cases here (ch is a BMP code point or a
        // negative value (invalid code point))
        final char[] value = this.value;
        int i = Math.min(fromIndex, value.length - 1);
        for (; i >= 0; i--) {
            if (value[i] == ch) {
                return i;
            }
        }
        return -1;
    } else {
        return lastIndexOfSupplementary(ch, fromIndex);
    }
}

private int lastIndexOfSupplementary(int ch, int fromIndex) {
    if (Character.isValidCodePoint(ch)) {
        final char[] value = this.value;
        char hi = Character.highSurrogate(ch);
        char lo = Character.lowSurrogate(ch);
        int i = Math.min(fromIndex, value.length - 2);
        for (; i >= 0; i--) {
            if (value[i] == hi && value[i + 1] == lo) {
                return i;
            }
        }
    }
    return -1;
}


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

static int lastIndexOf(char[] source, int sourceOffset, int sourceCount,
                       char[] target, int targetOffset, int targetCount,
                       int fromIndex) {
        /*
         * Check arguments; return immediately where possible. For
         * consistency, don't check for null str.
         */
        int rightIndex = sourceCount - targetCount;
        if (fromIndex < 0) {
            return -1;
        }
        if (fromIndex > rightIndex) {
            fromIndex = rightIndex;
        }
        /* Empty string always matches. */
        if (targetCount == 0) {
            return fromIndex;
        }

        int strLastIndex = targetOffset + targetCount - 1;
        char strLastChar = target[strLastIndex];
        int min = sourceOffset + targetCount - 1;
        int i = min + fromIndex;

    startSearchForLastChar:
        while (true) {
            while (i >= min && source[i] != strLastChar) {
                i--;
            }
            if (i < min) {
                return -1;
            }
            int j = i - 1;
            int start = j - (targetCount - 1);
            int k = strLastIndex - 1;

            while (j > start) {
                if (source[j--] != target[k--]) {
                    i--;
                    continue startSearchForLastChar;
                }
            }
            return start - sourceOffset + 1;
        }
    }

字符串转换方法

toLowerCase 与 toUpperCase 方法

这两个方法分别执行把字符串全部转换成小写/大写,这里只选取toLowerCase做分析

public String toLowerCase() {
    return toLowerCase(Locale.getDefault());
}

public String toLowerCase(Locale locale) {
        if (locale == null) {
            throw new NullPointerException();
        }

        int firstUpper;
        final int len = value.length;

        /* Now check if there are any characters that need to be changed. */
        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);
                    if (supplChar != Character.toLowerCase(supplChar)) {
                        break scan;
                    }
                    firstUpper += Character.charCount(supplChar);
                } else {
                    if (c != Character.toLowerCase(c)) {
                        break scan;
                    }
                    firstUpper++;
                }
            }
            return this;
        }

        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. */
        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);
    }

Locale表示的是地区语言,在这个方法内作用很小,默认传入的是en_CN。我们暂且忽略这部分逻辑。
首先进入scan循环,查看是否存在需要转换的字符,其中存在char域的判定:

public static final char MIN_HIGH_SURROGATE = '\uD800';
public static final char MAX_HIGH_SURROGATE = '\uDBFF';

上面两个值分别代表,Unicode 编码中高代理代码单元的最小值 / Unicode 编码中高代理代码单元的最大值。
如果遍历之后没有发现需要转换字符则返回当前。否则进入真正的转换逻辑。

trim 方法

剔除首尾的空字符。代码中首尾个进行一次循环。

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;
    }
replace 方法
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;
}

    public String replace(CharSequence target, CharSequence replacement) {
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
    }

    public String replaceFirst(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
    }

    public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }

replace方法总共有两套,一个是基于char进行替换,一个是使用字符串进行替换。char替换就是中规中矩的循环匹配产生新的字符序列的过程,字符串替换则委托给了正则表达式处理工具,这个工具类将在很后面涉及到编译原理的时候再详细介绍。

split 方法

split方法支持正则或者普通字符串序列,如果是普通字符串序列则使用indexOf进行查找,找到则截取字符串加入到ArrayList,最终转化为字符串数组。

public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((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 {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            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);
    }
join 方法

join方法可以将数组使用分隔符进行重新拼凑:

String.join(":",Arrays.as("1","2",""3)) => "1:2:3";

源码如下:

public static String join(CharSequence delimiter, CharSequence... elements) {
    Objects.requireNonNull(delimiter);
    Objects.requireNonNull(elements);
    // Number of elements not likely worth Arrays.stream overhead.
    StringJoiner joiner = new StringJoiner(delimiter);
    for (CharSequence cs: elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

// StringJoiner
public StringJoiner add(CharSequence newElement) {
    prepareBuilder().append(newElement);
    return this;
}

private StringBuilder prepareBuilder() {
    if (value != null) {
        value.append(delimiter);
    } else {
        value = new StringBuilder().append(prefix);
    }
    return value;
}

委托StringJoiner进行操作,其底层是StringBuilder提供存储,每次add的时候都先会拼上分隔符再拼接上元素值。

String.format 方法

String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。熟悉C语言的同学应该记得C语言的sprintf()方法,两者有类似之处。format()方法有两种重载形式。其背后都委托Formatter类进行转换。

// 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。
format(String format, Object... args){}
// 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。
format(Locale locale, String format, Object... args) 

显示不同转换符实现不同数据类型到字符串的转换,如下表所示:

转 换 符说  明
%s字符串类型
%c字符类型
%b布尔类型
%d整数类型(十进制)
%x整数类型(十六进制)
%o整数类型(八进制)
%f浮点类型
%a十六进制浮点类型
%e指数类型
%g通用浮点类型(f和e类型中较短的)
%h散列码
%%百分比类型
%n换行符
%tx日期与时间类型(x代表不同的日期与时间转换符

这里写个列子:

@Test
public void test007(){
    String str = String.format("Hi,%s", "早安");
    System.out.println(str);
    System.out.printf("字母a的大写是:%c %n", 'A');
    System.out.printf("3>7的结果是:%b %n", 3>7);
    System.out.printf("100的一半是:%d %n", 100/2);
    System.out.printf("100的16进制数是:%x %n", 100);
    System.out.printf("100的8进制数是:%o %n", 100);
    System.out.printf("50元的书打8.5折扣是:%f 元%n", 50*0.85);
    System.out.printf("上面价格的16进制数是:%a %n", 50*0.85);
    System.out.printf("上面价格的指数表示:%e %n", 50*0.85);
    System.out.printf("上面价格的指数和浮点数结果的长度较短的是:%g %n", 50*0.85);
    System.out.printf("上面的折扣是%d%% %n", 85);
    System.out.printf("字母A的散列码是:%h %n", 'A');
}

得到的输出是:

Hi,早安
字母a的大写是:A 
3>7的结果是:false 
100的一半是:50 
10016进制数是:64 
1008进制数是:144 
50元的书打8.5折扣是:42.500000 元
上面价格的16进制数是:0x1.54p5 
上面价格的指数表示:4.250000e+01 
上面价格的指数和浮点数结果的长度较短的是:42.5000 
上面的折扣是85% 
字母A的散列码是:41 
substring方法

方法较为简单,部分截取字符串内内容成为一个新的对象即可。

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

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

public CharSequence subSequence(int beginIndex, int endIndex) {
    return this.substring(beginIndex, endIndex);
}

concat 方法

可以看到,其内部委托了Arrays的拷贝方法,创建了一个新的字符串实例。

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

intern 方法

intern()方法比较特殊,这个方法会和常量池沟通。其逻辑是先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用。

String s0 = "1";
String s1 = new String("1");

// false,
System.out.println(s0 == s1);
// true
System.out.println(s0.intern() == s0);
// false
System.out.println(s1.intern() == s1);

s0的引用是字符串常量池引用,s1是堆上引用。因此第一个判断是false,s0.intern()之后返回常量池引用,自然与s0相等,而第三个判断则是将堆上引用和常量池引用进行判断,因此返回false。

String s0 = "1" + "2";
String s1 = "1" + new String("2");
String s2 = new String("1") + new String("2");

// true
System.out.println("12" == s0);
// false
System.out.println("12" == s1);
// false
System.out.println("12" == s2);

这里的s0将会直接编译为"12",因此第一个判断为true。
第二行与第三行,由于是字符串对象与字面常量(或者字符串对象)拼接,底层会转换为StringBuffer#append操作,因此s1,s2会产生非字符串常量对象(方法内会分配在栈上),因此返回是false。

字符串常见问题

对象的地址问题

可以看上文中intern方法详解

字符串长度限制

当我们使用字符串字面量直接定义String的时候,是会把字符串在常量池中存储一份的。常量池中的每一种数据项也有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。
CONSTANTUtf8info是一个CONSTANTUtf8类型的常量池数据项,它存储的是一个常量字符串。常量池中的所有字面量几乎都是通过CONSTANTUtf8info描述的。CONSTANTUtf8_info的定义如下:

CONSTANT_Utf8_info {
     u1 tag;
     u2 length;
     u1 bytes[length];
}

u2是无符号的16位整数,因此理论上允许的的最大长度是2^16=65536。而 java class 文件是使用一种变体UTF-8格式来存放字符的,null 值使用两个字节来表示,因此只剩下 65536- 2 = 65534个字节。
因此String字面常量超过65534的时候将会报错:

String s1 = "a...a";// 共65535个a处编译失败

同样的,String在运行期也有限制,这个值约等于4G,在运行期,如果String的长度超过这个范围,就可能会抛出异常。

字符串常量池

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
Java中的常量池,实际上分为两种形态:静态常量池运行时常量池
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

AbstractStringBuilder

Java中, AbstractStringBuilder是 StringBuilder 和 StringBuffer 的父类. 所以了解StringBuilder和StringBuffer前, 有必要先了解一下这个抽象父类。AbstractStringBuilder继承关系如下:
image.png
从接口上看,继承关系和String如出一辙。其字段也和String差不多,同样适用char[]进行存储

char[] value;

int count;

而由于AbstractStringBuilder目标是提供可动态调整字符串的能力,其方法层面增加了位置信息,动态扩容能力

动态调整容量能力

ensureCapacity� 方法

public void ensureCapacity(int minimumCapacity) {
    if (minimumCapacity > 0)
        ensureCapacityInternal(minimumCapacity);
}
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

private int hugeCapacity(int minCapacity) {
    if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
        throw new OutOfMemoryError();
    }
    return (minCapacity > MAX_ARRAY_SIZE)
        ? minCapacity : MAX_ARRAY_SIZE;
}

可以看到,传入的minimumCapacity必须大于0,否则报错,如果minimumCapacity 小于既有的长度则不执行扩充。扩充之后的大小 = 原大小 * 2 + 2,再之后针对扩充后的值进行溢出判定。

trimToSize�方法

由于存在扩充能力,因此有可能出现char[]数组内存在浪费的空间,trimToSize可以将这部分浪费空间剔除。

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

setLength�方法

public void setLength(int newLength) {

    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    
    ensureCapacityInternal(newLength);
    
    if (count < newLength) {
        Arrays.fill(value, count, newLength, '\0');
    }
    
    count = newLength;
}

// Arrays
public static void fill(char[] a, int fromIndex, int toIndex, char val) {
    rangeCheck(a.length, fromIndex, toIndex);
    for (int i = fromIndex; i < toIndex; i++)
        a[i] = val;
}

setLength是重新设置了一下长度,newLength先走判定,小于0的抛出异常,在进行扩容判定,最终在count与newLength之间填充’\0’。

指定尾部追加字符

append方法帮助我们在字节数组尾部追加,append方法存在大量重载方法,这里以append一个字符串做案例。

public AbstractStringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    // 扩容
    ensureCapacityInternal(count + len);
    // 填充数据到value
    str.getChars(0, len, value, count);
    // 修改count为最新的有效值
    count += len;
    return this;
}

指定位置追加字符

在指定位置追加字符使用的是insert方法,拥有大量重载方法。这里选择在指定位置追加字符串的方法。

public AbstractStringBuilder insert(int offset, String str) {
    // 判断指定位置的合法性
    if ((offset < 0) || (offset > length()))
        throw new StringIndexOutOfBoundsException(offset);
    // 字符串为null,则参数变更为"null"字符串
    if (str == null)
        str = "null";
    int len = str.length();
    // 尝试扩充
    ensureCapacityInternal(count + len);
    // 字符串拷贝插入
    System.arraycopy(value, offset, value, offset + len, count - offset);
    str.getChars(value, offset);
    count += len;
    return this;
}

可以看到, 先是判断了下入参是否合法,再之后进行尝试扩充容量长度,扩充之后使用System.arrayCopy方法进行拷贝字符串操作。

指定位置替换字符

public AbstractStringBuilder replace(int start, int end, String str) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (start > count)
            throw new StringIndexOutOfBoundsException("start > length()");
        if (start > end)
            throw new StringIndexOutOfBoundsException("start > end");

        if (end > count)
            end = count;
        int len = str.length();
        int newCount = count + len - (end - start);
        ensureCapacityInternal(newCount);

        System.arraycopy(value, end, value, start + len, count - end);
        str.getChars(value, start);
        count = newCount;
        return this;
    }

这里的核心还是使用System.arraycopy对char[]进行操作

指定位置删除字符

public AbstractStringBuilder deleteCharAt(int index) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    System.arraycopy(value, index+1, value, index, count-index-1);
    count--;
    return this;
}

使用System.arraycopy将index + 1部分朝前覆盖拷贝

指定范围切割字符串

public String substring(int start) {
    return substring(start, count);
}

public CharSequence subSequence(int start, int end) {
    return substring(start, end);
}

public String substring(int start, int end) {
    if (start < 0)
        throw new StringIndexOutOfBoundsException(start);
    if (end > count)
        throw new StringIndexOutOfBoundsException(end);
    if (start > end)
        throw new StringIndexOutOfBoundsException(end - start);
    return new String(value, start, end - start);
}


切割字符串,将使用String的默认构造函数产生一个新的字符串对象,其内部将会产生一个新的char[]对象,而非引用当前value对象进行共享。

码点操作

指定位置追加码点

public AbstractStringBuilder appendCodePoint(int codePoint) {
    final int count = this.count;

    if (Character.isBmpCodePoint(codePoint)) {
        ensureCapacityInternal(count + 1);
        value[count] = (char) codePoint;
        this.count = count + 1;
    } else if (Character.isValidCodePoint(codePoint)) {
        ensureCapacityInternal(count + 2);
        Character.toSurrogates(codePoint, value, count);
        this.count = count + 2;
    } else {
        throw new IllegalArgumentException();
    }
    return this;
}

操作码点的时候,使用int标识两个char信息,其内部将开始判定是否符合码点条件,进而进行拷贝填充。

定位码点操作

public int codePointAt(int index) {
    if ((index < 0) || (index >= count)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return Character.codePointAtImpl(value, index, count);
}

指定位置前置码点

public int codePointBefore(int index) {
    int i = index - 1;
    if ((i < 0) || (i >= count)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return Character.codePointBeforeImpl(value, index, 0);
}

统计范围码点操作

public int codePointCount(int beginIndex, int endIndex) {
    if (beginIndex < 0 || endIndex > count || beginIndex > endIndex) {
        throw new IndexOutOfBoundsException();
    }
    return Character.codePointCountImpl(value, beginIndex, endIndex-beginIndex);
}

定位范围码点

public int offsetByCodePoints(int index, int codePointOffset) {
    if (index < 0 || index > count) {
        throw new IndexOutOfBoundsException();
    }
    return Character.offsetByCodePointsImpl(value, 0, count,
                                            index, codePointOffset);
}

反转字符串操作

public AbstractStringBuilder reverse() {
    boolean hasSurrogates = false;
    int n = count - 1;
    for (int j = (n-1) >> 1; j >= 0; j--) {
        int k = n - j;
        char cj = value[j];
        char ck = value[k];
        value[j] = ck;
        value[k] = cj;
        if (Character.isSurrogate(cj) ||
            Character.isSurrogate(ck)) {
            hasSurrogates = true;
        }
    }
    if (hasSurrogates) {
        reverseAllValidSurrogatePairs();
    }
    return this;
}

private void reverseAllValidSurrogatePairs() {
    for (int i = 0; i < count - 1; i++) {
        char c2 = value[i];
        if (Character.isLowSurrogate(c2)) {
            char c1 = value[i + 1];
            if (Character.isHighSurrogate(c1)) {
                value[i++] = c1;
                value[i] = c2;
            }
        }
    }
}

java.lang.StringBuilder

StringBuffer继承了AbstractBuilder,几乎所有的代码都重写了父类,并简单的调用了一下。除此之外,StringBuilder重写了序列化方法,自行控制了序列化操作。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    s.defaultWriteObject();
    s.writeInt(count);
    s.writeObject(value);
}

/**
 * readObject is called to restore the state of the StringBuffer from
 * a stream.
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    count = s.readInt();
    value = (char[]) s.readObject();
}

java.lang.StringBuffer

StringBuffer继承了AbstractBuilder,几乎所有的代码都重写了父类,并简单的调用了一下。对比StringBuilder与AbstractBuilder,这里会有几个变化:
a)所有的方法都会增加synchronized�关键字,确保线程安全

@Override
public synchronized StringBuffer reverse() {
    toStringCache = null;
    super.reverse();
    return this;
}

b)实现内增加toStringCache,在执行toString的时候,直接使用toStringCache进行构造,当任何变更当前char[]数组的时候将会把toStringCache置为null

private transient char[] toStringCache;

public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

c)StringBuffer重写了序列化方法,自行控制了序列化操作

private synchronized void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    java.io.ObjectOutputStream.PutField fields = s.putFields();
    fields.put("value", value);
    fields.put("count", count);
    fields.put("shared", false);
    s.writeFields();
}
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    java.io.ObjectInputStream.GetField fields = s.readFields();
    value = (char[])fields.get("value", null);
    count = fields.get("count", 0);
}
  • 28
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值