一起学JDK源码 -- AbstractStringBuilder类

查看所有目录
前一篇查看了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();

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 查看所有目录String类是我们日常开发中使用最频繁的类之一,曾今有人说String类用的好坏能评判你是否是一个合...
    Kinsanity阅读 5,707评论 0 3
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,257评论 0 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 看别人做的挺好看的,自己试了一下,第一次,手残党啊,感觉脏兮兮的。 其实看实图是很漂亮绯红色,还有指甲油亮片也是很...
    浅浅岁月荒度余生阅读 411评论 4 11
  • 沉迷工具安利,买了新笔,水自闲家的如一。 买了北斗斋的水彩书。 就差颜料了,不知道买什么好。 时间紧凑,挨个上图吧...
    敬千帆阅读 246评论 2 3