vlambda博客
学习文章列表

我的jdk源码(三):AbstractStringBuilder类

一、概述

    我们在jdk1.8的源码中可以看到,StringBuilder类和StringBuffer类都是继承了AbstractStringBuilder类的,并且很多方法都是直接使用的父类AbstractStringBuilder的方法,所以在学习StringBuilder类和StringBuffer类之前,先来研究一下AbstractStringBuilder的源码。

二、源码分析

    (1)类的申明,源码如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence

        类名用abstract修饰说明是一个抽象类,只能被继承,不能直接创建对象。查了里面的方法你会发现它就一个抽象方法,toString方法。 同时可以看到AbstractStringBuilder实现了Appendable和CharSequence接口,我们来看一下他们的作用:

        * 实现了 Appendable能够被追加 char 序列和值的对象,实现的几个方法如下:

        a.  append(CharSequence csq) throws IOException:如何添加一个字符序列

        b. append(CharSequence csq, int start, int end) throws IOException:如何添加一个字符序列的一部分

        c. append(char c) throws IOException:如何添加一个字符

        * 实现CharSequence字符序列的接口,代表该类,或其子类是一个字符序列

        a. 规定了需要实现该字符序列的长度:length();

        b. 可以取得下标为index的的字符:charAt(int index);

        c. 可以得到该字符序列的一个子字符序列:subSequence(int start, int end);

        d. 规定了该字符序列的String版本(重写了父类Object的toString()):toString();

    (2) 成员变量源码如下:

 //定义存放内容的字符数组,数组的长度即为容量 char[] value; //定义内容实际的长度count<=value.length int count;

    (3)  构造函数源码如下:

 AbstractStringBuilder() { }
//重新new一个长度为capacity的字符数组 AbstractStringBuilder(int capacity) { value = new char[capacity]; }

    (4) 重载CharSequence的length方法,返回当前实际存储数据长度,源码如下:

 @Override public int length() { return count; }

    (5) capacity()方法返回当前容器的容量

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

    (6) ensureCapacity()扩容方法已经内部调用的方法源码如下:

 public void ensureCapacity(int minimumCapacity) { //判断扩充的目标容量值是否大于0 if (minimumCapacity > 0) ensureCapacityInternal(minimumCapacity); }
private void ensureCapacityInternal(int minimumCapacity) { //如果目标容量大于原本容量,则调用Arrays.copyOf,长度则由newCapacity(minimumCapacity)方法得出 if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } //先定义字符数组的最大存储容量,减去8是因为数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8个字节,数组的对象头信息相较于其他Object,多了一个表示数组长度的信息。 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //这个方法是计算容量的核心逻辑 private int newCapacity(int minCapacity) { //这里表示每次扩容都是2n+2,就有可能出现int越界,导致newCapacity从正数变为了负数 int newCapacity = (value.length << 1) + 2; //这里判断相比较于newCapacity < minCapacity的写法,规避了newCapacity越界,让newCapacity 也能够被赋予一个正值。 //而正常情况的时候,就是newCapacity扩容后还是没有目标容量minCapacity大,就直接设置扩容后的容量为目标容量minCapacity。 if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } //如果newCapacity是正常的正数,直接返回;如果不是,则进入hugeCapacity()方法 return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; } //如果目标容量大于了数组最大容量MAX_ARRAY_SIZE,则返回目标容量,小于则返回数组最大容量MAX_ARRAY_SIZE private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }

        注意:看到很多判断都是采用 a - b < 0 而不是直接用 a < b 。是因为本身与0比较大小运算更快一些,还能有效的规避一些int值越界的问题。

    (7) trimToSize()方法用来存放实际用的容量的值,没有值的容量被释放,执行后count = value.length 。源码如下:

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

        值得注意的是, 原来的数组由于没有被任何引用所指向,之后会被gc回收。

    (8) setLength()方法用来扩大实际容量,就是让count变大。源码如下:

 public void setLength(int newLength) { //如果要扩充的容量小于0,则抛出异常 if (newLength < 0) throw new StringIndexOutOfBoundsException(newLength); //调用扩容方法,源码及注释如上。每次判断需要扩容,就扩大到2n+2的容量 ensureCapacityInternal(newLength); //如果容量大于实际存储量,则在空的数组位置上存放'\0'(ASCII码中的null)来初始化 if (count < newLength) { Arrays.fill(value, count, newLength, '\0'); } //经过上一步操作后,就被填满了,所以实际存储量等于容量 count = newLength; }

    (9)  charAt(int index)方法是重写了CharSequence的charAt()方法,用于获取字符数组value指定位置的元素,源码如下:

 @Override public char charAt(int index) { //数组下标小于0或者大于数组长度,则抛出数组下标越界异常 if ((index < 0) || (index >= count)) throw new StringIndexOutOfBoundsException(index); return value[index]; }

    (10) codePointAt(int index) 等系列方法,用于获取字符对应代码点相关内容。源码如下:

 //获取字符序列中指定位置的字符,所对应的代码点,即ascii码。 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); } //返回此字符序列中从给定的index处偏移codePointOffset个代码点的索引。 public int offsetByCodePoints(int index, int codePointOffset) { if (index < 0 || index > count) { throw new IndexOutOfBoundsException(); } return Character.offsetByCodePointsImpl(value, 0, count, index, codePointOffset); }

         在java中一个char类型为16个二进制位,原本用于表示一个字符。但后来发现,16位已经不够表示所有的字符,所以后来发展出了代码点表示字符的方法。代码点(code point)是指编码字符集中,字符所对应的数字。有效范围从U+0000到U+10FFFF。其中U+0000到U+FFFF为基本字符,U+10000到U+10FFFF为增补字符。 代码单元(code unit):对代码点进行编码得到的1或2个16位序列。其中基本字符的代码点直接用一个相同值的代码单元表示,增补字符的代码点用两个代码单元的进行编码,这个范围内没有数字用于表示字符,因此程序可以识别出当前字符是单单元的基本字符,还是双单元的增补字符。一个代码单元为16位二进制,一个代码点为一个或两个16位二进制。即一个代码点可表示为一个代码单元或两个代码单元。

    (11) getChars()方法将value[]的[srcBegin, srcEnd)拷贝到dst[]数组的desBegin开始处,源码如下:

 public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) { if (srcBegin < 0) throw new StringIndexOutOfBoundsException(srcBegin); if ((srcEnd < 0) || (srcEnd > count)) throw new StringIndexOutOfBoundsException(srcEnd); if (srcBegin > srcEnd) throw new StringIndexOutOfBoundsException("srcBegin > srcEnd"); System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }

    (12) setCharAt(int index, char ch)方法设置数组index位置上的字符为ch,源码如下:

 public void setCharAt(int index, char ch) { if ((index < 0) || (index >= count)) throw new StringIndexOutOfBoundsException(index); value[index] = ch; }

    (13) append()方法有很多的同名不同参的方法,是AbstractStringBuilder类及其子类最常用最重要的方法。常用的append(String str)源码如下:

 public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }

        调用原理如下:

        a. 首先判断所传参数是否为null,如果为null则调用appendNull方法,实际上就是在原字符序列后加上"null"序列。

        b. 如果不为null则进行扩容操作,最小值为count+len,这一步可能增加容量也可能不增加,当count+len小于或等于capacity就不用进行扩容。

        c. 然后再将参数的字符串序列添加到value中。

        d. 最后返回this,注意这里返回的是this,即AbstractStringBuilder对象本身,也就意味,可以在一条语句中多次调用append方法。

    (14) delete(int start, int end)方法源码如下:

 public AbstractStringBuilder delete(int start, int end) { if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) end = count; if (start > end) throw new StringIndexOutOfBoundsException(); int len = end - start; if (len > 0) { System.arraycopy(value, start+len, value, start, count-end); count -= len; } return this; }

        此方法可以删掉value数组的[start,end)部分,并将end后面的数据移到start位置。 删除字符序列指定区间的内容后不改变原序列的容量。

    (15) deleteCharAt(int codePoint)方法可以删除下标为index的数据,并将后面的数据前移一位。源码如下:

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

    (16)  replace()方法,源码如下:

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

         此方法将原字符序列指定区间start到end区间内的内容替换为str,替换过程中序列长度会改变,所以需要进行扩容和修改count的操作。

    (17) subSequence ()方法,源码如下:

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

        值得注意的是,此方法调用了String的构造函数,返回了一个new的String对象。

    (19)insert()方法也有很多的同名不同参的方法。常用的insert(String str)源码如下:

 public AbstractStringBuilder insert(int offset, String str) { if ((offset < 0) || (offset > length())) throw new StringIndexOutOfBoundsException(offset); 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; }

         insert系列方法作用是将给定定对象所对应的字符串插入到原序列的指定位置。 调用原理如下:

        a. 对待插入的位置offset进行检查,必须在容量内。

        b. 如果传入对象为null则插入"null"字符串。

        c. 对value数组进行扩容。

        d. 通过System.arraycopy对数组进行复制。

        e. 将str的内容复制到value中。

    (20) indexOf()方法在也是调用的String类的indexOf()方法,具体可以查看《 》,源码如下:

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

    (21) lastIndexOf()是获取字符串最后出现的位置,也是调用的String类的lastIndexOf()方法实现,源码如下:

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

    (22) reverse()方法源码如下:

 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; }
/** Outlined helper method for reverse() */ 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; } } } }

         reverse()方法用于将字符序列反转,具体逻辑如下:

        a. hasSurrogates用于判断字符序列中是否包含surrogates pair ( Surrogate Pair是UTF-16中用于扩展字符而使用的编码方式,是一种采用四个字节(两个UTF-16编码)来表示一个字符。 )

        b. 将字符反转,count为数组长度,因为是从0开始的所以这里需要减1。具体转换是第一个字符与最后一个字符对调,第二个字符与倒数第二个字符对调,依次类推 。

        c. 实际上上述操作只需要循环(n-1) /2 + 1次(判断条件j>=0所以要+1次,源码中>>1就是除以2)就可以了,如数组长度为9则需要循环 (9-1-1)/2 +1 = 4次,9个字符对调次,第5个位置的字符不用换,如果长度为10需要循环(10-1-1)/2 +1 = 5次 。

        d. 剩下的工作就是两个位置的元素互换。

        e. 如果序列中包含surrogates pair 则执行reverseAllValidSurrogatePairs方法(char在java中是16位的,刚好是一个UTF-16编码。而字符串中可能含有Surrogate Pair,但他们是一个单一完整的字符,只不过是用两个char来表示而已,因此在反转字符串的过程中Surrogate Pairs 是不应该被反转的。而reverseAllValidSurrogatePairs方法就是对Surrogate Pair进行处理 )。

    (23) toString()方法源码如下:

 @Override public abstract String toString();

         这是这个抽象类中唯一的一个抽象方法,需要子类StringBuilder和StringBuffer自己实现。

    (24) getValue()方法源码如下:

 final char[] getValue() { return value; }

        直接返回字符数组value。

三、总结

    学习AbstractStringBuilder类主要是对后面深入学习StringBuilder类和StringBuffer类打下坚实的基础,那么敬请期待《我的jdk源码(四):StringBuffer 线程安全可多次修改》和《我的jdk源码(五):StringBuilder 类》。