美文网首页一起学JDK源码
一起学JDK源码 -- AbstractStringBuilde

一起学JDK源码 -- AbstractStringBuilde

作者: Kinsanity | 来源:发表于2018-10-17 15:30 被阅读1次

    查看所有目录
    前一篇查看了String类的源码,发现String类中有不少地方使用了StringBuffer和StringBuilder类,而这两个类都是继承自AbstractStringBuilder类,里面的很多实现都是直接使用父类的,所以就看一下AbstractStringBuilder类的源码。

    类的申明:

    abstract class AbstractStringBuilder implements Appendable, CharSequence {}
    

    1.默认访问控制修饰符,说明只能在包内使用,即只能在JDK内部使用,可能有人会问我创建一个java.lang包然后里面的类就可以使用AbstractStringBuilder类了,想法不错,但jkd不允许,会报SecurityException : Prohibited package name: java.lang。故这个类只是给StringBuffer和StringBuilder类使用的。
    2.类名用abstract修饰说明是一个抽象类,只能被继承,不能直接创建对象。查了里面的方法你会发现它就一个抽象方法,toString方法。
    3.实现了Appendable接口,Appendable能够被追加 char 序列和值的对象。如果某个类的实例打算接收来自 Formatter 的格式化输出,那么该类必须实现 Appendable 接口。
    4.实现了Charsequence接口,代表该类,或其子类是一个字符序列。

    成员变量:

        char[] value;
        int count;
    

    value用于承装字符序列,count数组中实际存储字符的数量。这里的value同String类中的value不同,String类中的value是final的不可被修改,这里的value是动态的,并且可提供给外部直接操作。

    构造函数:

        AbstractStringBuilder() {
        }
        AbstractStringBuilder(int capacity) {
            value = new char[capacity];
        }
    

    AbstractStringBuilder提供两个构造函数,一个是无参构造函数。一个是传一个capacity(代表数组容量)的构造,这个构造函数用于指定类中value数组的初始大小,数组大小后面还可动态改变。

    其它方法:

    length:

        public int length() {
            return count;
        }
    

    返回已经存储字符序列的实际长度,即count的值。

    capacity:

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

    返回当前value可以存储的字符容量,即在下一次重新申请内存之前能存储字符序列的长度。新添加元素的时候,可能会对数组进行扩容。

    ensureCapacity:

        public void ensureCapacity(int minimumCapacity) {
            if (minimumCapacity > 0)
                ensureCapacityInternal(minimumCapacity);
        }    
    

    该方法是用来确保容量至少等于指定的最小值,是该类的核心也是其两个实现类StringBuffer和StringBuilder的核心。通过这种方式来实现数组的动态扩容。下面来看下其具体逻辑。
    1.判断入参minimumCapacity是否有效,即是否大于0,大于0执行ensureCapacityInternal方法,小于等于0则忽略。

        private void ensureCapacityInternal(int minimumCapacity) {
            if (minimumCapacity - value.length > 0) {
                value = Arrays.copyOf(value,
                        newCapacity(minimumCapacity));
            }
        }
    

    2.判断入参容量值是否比原容量大,如果大于原容量,执行扩容操作,实际上就是创建一个新容量的数组,然后再将原数组中的内容拷贝到新数组中,如果小于或等于原容量则忽略。

        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;    
        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;
        }
    

    3.计算新数组的容量大小,新容量取原容量的2倍加2和入参minCapacity中较大者。然后再进行一些范围校验。新容量必需在int所支持的范围内,之所以有<=0判断是因为,在执行 (value.length << 1) + 2操作后,可能会出现int溢出的情况。如果溢出或是大于所支持的最大容量(MAX_ARRAY_SIZE为int所支持的最大值减8),则进行hugeCapacity计算,否则取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;
        }
    

    4.这一步先进行范围检查,必须在int所支持的最大范围内。然后在minCapacity与MAX_ARRAY_SIZE之间取较大者,此方法取的范围是Integer.MAX_VALUE - 8到Integer.MAX_VALUE之间的范围。
    5.总结:
    1.通过value = Arrays.copyOf(value,newCapacity(minimumCapacity));进行扩容
    2.新容量取 minCapacity,原容量乘以2再加上2中较大的,但不能大于int所支持的最大范围。
    3.在实际环境中在容量远没达到MAX_ARRAY_SIZE的时候就报OutOfMemoryError异常了,其实就是在复制的时候创建了数组char[] copy = new char[newLength];这里支持不了那么大的内存消耗,可以通过 -Xms256M -Xmx768M设置最大内存。

    trimToSize:

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

    减少字符序列的使用空间,比如申请了100字符长度的空间,但是现在只用了60个,那剩下的40个无用的空间放在那里占内存,可以调用此方法释放掉未用到的内存。原理很简单,只申请一个count大小的数组把原数组中的内容复制到新数组中,原来的数组由于没有被任何引用所指向,之后会被gc回收。

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

    用空字符填充未使用的空间。首先对数组进行扩容,然后将剩余未使用的空间全部填充为'0'字符。

    charAt:

        public char charAt(int index) {
            if ((index < 0) || (index >= count))
                throw new StringIndexOutOfBoundsException(index);
            return value[index];
        }
    

    获取字符序列中指定位置的字符,范围为0到count,超出范围抛StringIndexOutOfBoundsException异常。

    codePointAt:

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

    获取字符序列中指定位置的字符,所对应的代码点,即ascii码。

    codePointBefore:

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

    获取字符序列中指定位置的前一个位置的字符,所对应的代码点。

    codePointCount:

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

    获取字符串代码点个数,是实际上的字符个数。不清楚代码点可查看java中代码点与代码单元的区别

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

    返回此字符序列中从给定的index处偏移codePointOffset个代码点的索引。不清楚代码点的可以查看java中代码点与代码单元的区别

    getChars:

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

    将字符序列中指定区间srcBegin到srcEnd内的字符拷贝到dst字符数组中从dstBegin开始往后的位置中。

    setCharAt:

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

    设置字符序列中指定索引index位置的字符为ch。

    append系列:

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

    AbstractStringBuilder类中有一系列append方法,作用是在原字符序列后添加给定的对象或元素所对应的字符序列。这里挑一个代表讲解,其它方法原理类似。
    1.首先判断所传参数是否为null,如果为null则调用appendNull方法,实际上就是在原字符序列后加上"null"序列。
    2如果不为null则进行扩容操作,最小值为count+len,这一步可能增加容量也可能不增加,当count+len小于或等于capacity就不用进行扩容。
    3.然后再将参数的字符串序列添加到value中。
    4.最后返回this,注意这里返回的是this,也就意味者,可以在一条语句中多次调用append方法,即大家所知的方法调用链。原理简单,但思想值得借鉴。asb.append("hello").append("world");

    appendCodePoint:

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

    添加代码点,将入参转换为对应的代码点后,添加到原字符序列结尾。不清楚代码点的可以查看java中代码点与代码单元的区别

    delete:

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

    删除字符序列指定区间的内容。这个操作不改变原序列的容量。

    deleteCharAt:

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

    删除字符序列中指定索引index位置的字符。

    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的操作。

    substring:

        public String substring(int start) {
            return substring(start, count);
        }
        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);
        }
    

    切割原字符序列指定区间start到end内的内容,返回字符串形式。

    insert系列:

        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系列作用是将给定定对象所对应的字符串插入到原序列的指定位置。insert系列同append系列类似,只不过append是在原序列末尾添加元素,insert是在指定位置插入元素。这里也选一个代表进行讲解。
    假设原字符序列为"hello"现调用insert(int 1, “aa");
    1.对待插入的位置offset进行检查,必须在容量内
    2.如果传入对象为null则插入"null"字符串
    3.对value数组进行扩容
    4.通过System.arraycopy对数组进行复制
    arraycopy(Object src,int srcPos,Object dest,int destPos,int length);
    src:源数组; ['h','e','l','l','o','o']
    srcPos:源数组要复制的起始位置;1
    dest:目的数组; ['h','e','l','l','o','w']
    destPos:目的数组放置的起始位置; 1+2=3
    length:复制的长度。 6-1=5
    则执行完这句后value中的内容为['h','e','l','e','l','l','o','o']
    可以看到是将位置1到结尾的内容后移了两个长度,因为需要插入的字符串"bb"的长度为2
    5.将str的内容复制到value中
    str.getChars(value, offset);//将str的内容复制到value中从offset 1位置开始复制
    复制完成后['h','b','b','e','l','l','o','o'],即我们最终想要达到的效果"hbbellow"

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

    查询给定字符串在原字符序列中第一次出现的位置。调用的其实是String类的indexOf方法,具体可查看一起学JDK源码 -- String类

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

    该方法用于将字符序列反转,如"hellow"执行reverse后变成"wolleh"。
    1.hasSurrogates用于判断字符序列中是否包含surrogates pair
    2.将字符反转,count为数组长度,因为是从0开始的所以这里需要减1。具体转换是第一个字符与最后一个字符对调,第二个字符与倒数第二个字符对调,依次类推
    3.实际上上述操作只需要循环(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次
    4.剩下的工作就是两个位置的元素互换。
    5.如果序列中包含surrogates pair 则执行reverseAllValidSurrogatePairs方法

    reverseAllValidSurrogatePairs:

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

    Surrogate Pair是UTF-16中用于扩展字符而使用的编码方式,是一种采用四个字节(两个UTF-16编码)来表示一个字符。
    char在java中是16位的,刚好是一个UTF-16编码。而字符串中可能含有Surrogate Pair,但他们是一个单一完整的字符,只不过是用两个char来表示而已,因此在反转字符串的过程中Surrogate Pairs 是不应该被反转的。而reverseAllValidSurrogatePairs方法就是对Surrogate Pair进行处理。

    toString:

    public abstract String toString();
    

    这是这个抽象类中唯一的一个抽象方法,需要子类去实现。
    查看所有目录

    相关文章

      网友评论

        本文标题:一起学JDK源码 -- AbstractStringBuilde

        本文链接:https://www.haomeiwen.com/subject/ptrrzftx.html