深入理解StringBuilder类

        

1.StringBuilder概述

        在学习String类时,API中说字符串缓冲区支持可变的字符串,什么是字符串缓冲区呢?接下来研究下字符串缓冲区。查阅StringBuilder的API,StringBuilder又称为可变字符序列,它是一个类似于 String 的字符串缓冲区,通过某些方法调用可以改变该序列的长度和内容。原来StringBuilder是个字符串的缓冲区,即它是一个容器,容器中可以装很多字符串。并且能够对其中的字符串进行各种操作。

2.StringBuilder构造方法

        既然要使用这个类,肯定要先知道这个类的构造方法,这里展示StringBuilder的两个构造方法,先简单看一下这两个构造方法,public StringBuilder()和public StringBuilder(String str),显然前者是空参构造,后者是单参构造,怎么用到没什么问题,该如何理解呢?那么下面先看一段代码:

public class stringBuilderDemo01 {
    public static void main(String[] args) {

        StringBuilder sb = new StringBuilder();
        System.out.println(sb);
        System.out.println(sb.toString());

        StringBuilder sb02 = new StringBuilder("abcd");
        System.out.println(sb02);
        System.out.println(sb02.toString());
    }
}

         运行结果图:

        按照一般的理解,这里打印输出对象sb,应该是十六进制的数字,而这里第一行和第二行输出的空,首先想到的应该是toString肯定被重写了。其实public StringBuilder()的功能是构造一个初始容量为16(底层用的是字节数组)的容器(下图),就是在创建sb对象的时候底层会最终创建一个空串,直接输出sb,即直接打印容器,相当于将容器中的内容转换成一个字符串,而此时容器为空,所以输出为空,那么sb.toString输出空也就容易理解了,原理也是一样的。

        再说public StringBuilder(String str)也就容易了,也就是构造一个StringBuilder容器,初始化内容为“abcd”(例如上),当直接输出sb02时打印abcd,和sb02.toString一样,因为打印new的对象会默认调用toString。

3.StringBuilder类的成员方法

        这里展示常用的成员方法.

        1.append()方法:不断向容器的末尾追加任意类型的数据

     可以看到append()方法很多,下面简单展示部分:

public class stringBuilderDemo02 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("cde");//初始cde
        char[] chars={'b','g','t'};
        sb.append(chars);
        sb.append(6);//append()方法其实就是向容器中追加数据
        sb.append("abc");
        sb.append('c');
        sb.append(false);
        System.out.println(sb);//cdebgt6abccfalse
    }
}

        总之就是将数据字符追加在后面,最终打印时以字符串的形式打印。
    
        2.int length():返回容器中字符的个数

public class stringBuilderDemo02 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("qaz");//初始cde
        sb.append(789);
        sb.append(true);
        System.out.println(sb.length());//10
    }
}

        显然就是将追加好的字符计算其字符个数,这里不同于数组的length,数组里使用直接打印数组长度,而与数组存的字符个数无关。


        3.String substring(int start, int end):返回一个新的 String,它包含此序列当前所包含字符的子序列。
        4.StringBuilder delete(int start, int end):删除容器中索引[startIndex,endIndex-1)的字符

public class stringBuilderDemo02 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("zxcvbnm");
        sb.delete(2,4);
        System.out.println(sb);//zxbnm   删除的范围[startIndex,endIndex-1)
        //delete直接删除容器中的字符,改变了这个容器中的内容

        String substr = sb.substring(1, 3);
        System.out.println(substr);//xb   截取范围 [startIndex,endIndex-1)
    }
}

        可以看到当删除索引的[2,4)字符,即剩下zxbnm,再去截取索引[1,3)所得到就是xb。


        5.StringBuilder reverse():反转容器中的内容,将此字符序列用其反转形式取代。

public class stringBuilderDemo02 {
    public static void main(String[] args) {
        StringBuilder sb=new StringBuilder("916qpalzm");
        System.out.println(sb.reverse());//mzlapq619
    }
}

4.StringBuilder底层原理

       ①底层原理一:空参构造

        其实StringBuilder的底层使用的是一个字节数组(JDK 8及以前版本使用的是字符数组,而JDK9及以后版本使用字节数组),所有添加到StringBuilder中的内容都会添加到底层的字节数组中,从StringBuilder中取出数据(toString())会把字符数组中的内容转换成一个字符串。

        直接上代码:

        在该代码的第一行加上断点进行Debug,点击红色箭头强制进入(由于该类不是我们写的,即查看需要强制进入):

        这里看到进入了StringBuilder类的空参构造方法,执行的语句是调用父类,那么我们再点击强制进入(或ALT+Shift+F7)进入此方法的父类。

        就上图进入AbstractStringBuilder,下面可以点击F8,进入方法内部,如下图:

        点击CONMPACT_STRINGS和鼠标左键即可进入查看发现是布尔类型且是常量,主要判断是否压缩,而由下图可知该常量的布尔默认值是true,即默认压缩。所谓是否压缩就是根据存储的字符,如果不压缩用UTF-16则默认都是两个字节,比如存一个字母用1个字节就够用(此时用UTF-16就必须2个字节才能存储),而压缩后就用1个字节即可,总之默认就是启用压缩而节约空间。

        点击后面的value上发现它是一个字节数组,开辟了16字节的空间,而下面的coder=0,即记录底层编码,意思是使用单字节时coder为0,使用的双字节时coder为1。再点击F8就执行完上面的方法,再点点F8返回到最初执行的代码了。

        再向下Debug会发现,abc存储在value字节数组中,对应的是97、98、99,也就是abc对应的ASCII表里的字节的十进制。后面追加的数字3和6.6也是对应的ASCII码的十进制,注意6.6是分三部分存储的,6对应的ASCII码十进制是54,而小数点对应的是46,所以后几个54,46,54对应存储的是6.6。count就是计数的,占用几个字节就是计数为几啦。

        最后说打印对象sb,其实默认调用了toString,将上面的字符数组转换为字符串进行打印,最终输出我们所看到的字符串:abc36.6。

        总结一下,new StringBuilder()也就是底层会创建一个默认长度为16的字节数组,即byte[] value = new byte[16],最后该字节数组用来存储数据(相当于底层使用的容器)。
 

        ②底层原理二:有参构造

        直接上代码:

        对sb进行Debug,强制进入,能看到字符串的存储是一个字节数组,如下图:

        再强制进入(ALT+Shift+F7),来到StringBuilder的父类,如下图:

        第一行对字符进行计数长度为3,第二行是一个三目运算,对字符长度进行判断是否小于整形最大值减去16,显然是小于的,那么就执行后面的length+16,容量变为19。第三行点击进入coder能看到下面的代码(byte coder()),即判断是否压缩,“abc”可以当单字符所以肯定压缩存储。第四行是一个赋值操作,第五行进行判断initCoder是LATIN1字符吗( Latin-1是ASCII码表的扩展,不同于UTF-16编码通常占用2个字节的空间,所以和ASCII一样编码也只是占用一个字节的空间),如果是就开辟19字节的空间字节数组,这里的19就是存储的3个字符加上默认的16开辟出来的。

byte coder() {
        return COMPACT_STRINGS ? coder : UTF16;
    }

        最后能够查看到value是19字节空间的字节数组,存储了abc三个字符的ASCII的十进制。

        总结一下:首先初始化容量,当前数据字符个数+默认容量 例如: "abc".length+16 =19。其次判断存入的字符是否是latin-1字符,如果是就用单字节。再就是开辟空间byte[] value = new byte[3+16]。最后将"abc"追加到value数组中。

刚刚上面存的的是英文字母,下面存一下中文试试。

public class stringBuilderDemo02 {
    public static void main(String[] args) {
        StringBuilder sb=new StringBuilder("编程");
    }
}

        还是那段代码,这里换成中文字符,仍旧Debug,强制进入,前面的还和上面将英文的一样,而进入coder判断时就变了,返回的是UTF-16。

byte coder() {
        return COMPACT_STRINGS ? coder : UTF16;
    }

        也就不再进行压缩,当判断不是拉丁字符时就运行后面StringUTF16.newBytesFor(capacity)

这部分,当存英文字符时使用单字节18不变,而这里UTF-16使用双字节,此时就18*2=36,也就是相邻的两个字节表示一个汉字。如下图,22和127表示‘编’这个字,而11和122表示‘程’;

        总结一下,首先初始化容量,当前数据字符个数+默认容量 例如: "编程".length+16 =18。其次判断存入的字符不是纯latin-1字符,如果是就用双字节。然后byte[] value = new byte[(2+16)*2]
最后将"编程"追加到value数组中,每相邻的两个元素(两个字节)代表一个字符。

5.Stringbuilder扩容原理

       先看一段代码:

public class StringBuilderDemo04 {
    public static void main(String[] args) {
        StringBuilder sb=new StringBuilder();
        for (int i = 0; i < 16; i++) {
            sb.append('a');
        }
        sb.append('b');
        System.out.println(sb);
    }
}

       不难看出它的存储是这样的:

       而输出结果:

        由上面所提到的,StringBuilder默认开辟的字节数组是16字节,而上面代码存储16个a后又能正常存储并输出第17个字符b,这是为什么?其实这就是StringBuilder的扩容。

        一图便能看懂:

        下面再从代码看一下扩容思想,还是DeBug对sb.append('b');这一行,强制进入。

        然后进入append的父类(其实StringBuilder的所有append方法都会调用这个父类)如下图,其实这个方法就是判断是否扩容。

        这行代码就是确定本次运行所需要的最小容量为17,即我所存储的16个a加上一个b。再点击强制进入,就如下图:

        这里>>是右移运算符(value.length的二进制表示向右移动coder位),意思是除2的coder次方,coder前面提到过,如果存储的是Latin-1字符(也可以说是ASCII字符)就为0(单字节),非Latin-1时coder就为1(双字节)。也就是16除2的0次方,还是16.(则如果coder是1就减半了,即除2的1次方).再往下执行,判断minimumCapacity - oldCapacity > 0,显然结果为true,进入if语句内部,里面有一个copyof的方法,对应上面画的扩容图。对newCapability(minimumCapacity)<<coder强制进入,如下图:

        这里会计算出一个增长长度growth=1,对于最终开辟多大的空间,图中用到了一个叫newLength的方法,点击强制进入,如下图:

        总之用到minGrowth最小增长长度和prefGrowth首选增长长度,且最终两者取最大值,这是为什么?前者就刚刚的growth=1,而后者preGrowth为18,是由下图newLength中的oldLength+2<<coder计算出来的。

        之所以取二者最大值,是因为当我们存入17个字符时,就新增开辟一个空间,如果我在源代码上再用append追加其他字符,又要再来扩容(再走一遍上面的),就是浪费时间,为了提高效率,所以选取首选的一个增长量,说白了就是足够大。

        所以最终开辟的就是16+18=34个字节空间,同时进行了一个判断prefLength小于整形最大值。最后进行返回prefLength。

        那么再进行copyof方法强制进入时就看到拷贝的新数组。如上图。

        最后看到索引第16个是98,也就是b的ASCII十进制表示,表明b被追加进去了。总的来说,

新字节数组有多长其实也受底层算法的影响,JDK不同可能造的长度也不一样,但是扩容思想是不变的。

6.StringBuilder链式编程

        StringBuilder编程,顾名思义就是像链子一样的编程,下面直接上代码:

public class StringBuilderDemo05 {
    public static void main(String[] args) {
        String str=new StringBuilder().append(7).append("def").append(3.14).toString();//链式编程
        System.out.println(str);//7def3.14    //不想追加就用toString方法结束,最后以字符串输出

        StringBuilder sb=new StringBuilder();
        StringBuilder sb02 = sb.append(6);
        StringBuilder sb03=sb02.append("abc");
        System.out.println(sb==sb02);//true
        System.out.println(sb02==sb03);//true
        System.out.println(sb03==sb); //true     //sb,sb02,sb03都指向一个StringBuilder对象,就是最开始的那个容器
    }
}

        显然链式编程就是使用append方法一直往后追加字符。

7.StringBuilder类与StringBuffer类

        

        查阅API发现还有一个StringBuffer类,它也是字符串缓冲区,StringBuilder与它和StringBuffer的有什么不同呢?两者在用法上完全相同, 它也是一个可变的字符序列。在StringBuffer中有这样一句描述从 JDK 5 版本开始,此类已补充了设计供单线程使用的等效类StringBuilder 。通常应优先使用StringBuilder类,因为它支持所有相同的操作,但速度更快,因为它不执行同步。
        其实StringBuffer使用和StringBuilder基本一致,但是大多数情况下,优先考虑StringBuilder,操作效率更高。想了解更多,可以去查看API。

       

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值