java stringbuffer长度限制_参加了这么多面试,还是不懂StringBuffer和StringBuilder的区别?...

StringBuffer

在实际开发中使用 String 类会存在一个问题,String 对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象,即修改了 String 的引用。因为 String 的底层是用数组来存值的,数组长度不可改变这一特性导致了上述问题,所以如果开发中需要对某个字符串进行频繁的修改,使用 String 就不合适了,会造成内存空间的浪费,如何解决这个问题呢?

可以使用 StringBuffer 类来解决,当对字符串对象进行频繁修改时,使用 StringBuffer 可以极大提升程序的效率,我们通过下面这个例子一测便知。

分别定义 String 和 StringBuffer 类型的字符串对象,对它们进行值的累加操作,循环执行 50000 次,然后统计各自的耗时,代码如下所示。

//String

运行结果分别如下图所示。

8060664fe72ad79f04204cfdfdcab296.png

1a25dbdab1ec086964845ad31c63ff44.png

可以看到 String 类型耗时 1345 毫秒,StringBuffer 类型耗时只有 7 毫秒,速度提升了近 200 倍。

接下来我们就来详细学习 StringBuffer 类。

StringBuffer 和 String 类似,底层也是用一个数组来存储字符串的值,并且数组的默认长度为 16,即一个空的 StringBuffer 对象,数组长度为 16,如下图所示。

547494d4672df0086a4fe61365013c2d.png

实例化一个 StringBuffer 对象即创建了一个大小为 16 个字符的字符串缓冲区。

当我们调用有参构造创建一个 StringBuffer 对象时,数组长度就不是 16 了,而是根据当前对象的值来决定数组的长度,“值的长度+16”作为数组的长度,如下图所示。

68bc44bac5f3ce223d6b4d31d6b1a04e.png

创建了一个字符串缓冲区,该缓冲区初始值为指定的字符串。字符串缓冲区的初始容量为字符串参数的长度+16。

我们可以看到带参构造中依次执行了两步操作:super(str.length()+16)、append(str),这也就很清楚的说明了 StringBuffer 的创建过程,先创建一个长度为"str长度+16"的字符串缓冲区,然后把 str 的值追加到此字符串序列中。

所以一个 StringBuffer 创建完成之后,有 16 个字符的空间可以对其值进行修改。如果修改的值范围超出了 16 个字符,则调用 ensureCapacityInternal() 方法检查 StringBuffer 对象的原 char 数组的容量能不能装下新的字符串,如果装不下则对 char 数组进行扩容。

a2537c9471a3f609edc75c6ff573beaf.png

扩容的逻辑就是创建一个新的 char 数组,newCapacity() 方法用于确定新容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小。

e67545b279934961adbdce20f8559cbf.png

扩容完成之后,再调用 Arrays.copyOf() 方法完成数据拷贝,底层调用 System.arryCopy() 方法将原数组的内容复制到新数组,最后将指针指向新的 char 数组。

69917732829297d4251e85dc769e8ac0.png

StringBuffer 常用方法

f83ec87ce9de191999064c25643011da.png

具体代码如下所示。

StringBuffer 

运行结果如下图所示。

9b326defa1029cd6a4c5c112e0d75acc.png

StringBuilder

StringBuilder 和 StringBuffer 是一对兄弟,因为它们拥有同一个父类 AbstractStringBuilder,同时实现的接口也是完全一样,都实现了 java.io.Serializable, CharSequence 两个接口,如下图所示。

42533b68f5716b4d388981a3267f71a8.png

f227cb7cfe8652b02f34ffd27ab8f787.png

那它们有什么区别呢?最大的区别在于 StringBuffer 对几乎所有的方法都实现了同步,StringBuilder 没有实现同步,如同样是对 AbstractStringBuilder 方法 append 的重写,StringBuffer 添加了 synchronized 关键字修饰,而 StringBuilder 没有,如下图所示。

c24652c46decc27491a1a9acffa61584.png

09547a6e0c5b5002c391d938152f9030.png

所以 StringBuffer 是线程安全的,在多线程系统中可以保证数据同步,而 StringBuilder 无法保证线程安全,所以多线程系统中不能使用 StringBuilder。

但是方法同步需要消耗一定的系统资源,所以 StringBuffer 虽然安全,但是效率不如 StringBuilder,也就是说使用 StringBuilder 更快,我们还是用上面的例子做一个测试。

分别定义 StringBuffer 和 StringBuilder 类型的字符串对象,对它们进行值的累加操作,循环执行 500000 次,然后统计各自的耗时,代码如下所示。

//StringBuffer

运行结果分别如下图所示。

314dc65af4c43d4c500c3efdde11bef3.png

49bf4c15fdd6ae70fef040d09337edde.png

通过结果可以看到,同样是执行 50 万次操作,StringBuffer 耗时 45 毫秒,而 StringBuilder 耗时 34 毫秒,相差虽然不是很大,但是 StringBuilder 效率确实要高于 StringBuffer,但是安全性不如 StringBuffer。

所以,在需要考虑线程安全的场景下我们可以使用 StringBuffer,不需要考虑线程安全,追求效率的场景下可以使用 StringBuilder。

StringBuilder 的具体使用如下所示。

StringBuilder 

运行结果如下图所示。

50fc26dba46be2a4ea1b56936d373516.png

StringBuilder 为什么线程不安全?

我们通过一个例子来测试,代码如下所示。

StringBuilder 

开启 10 个线程,每个现象对 stringBuilder 添加 1000 个 'a',操作完成之后,stringBuilder 的长度应该是 10*1000 = 10000,但是我们看到多次运行的结果如下。

58e330d4fbc71b17efc15c68f6cb2c8b.png

b45653554b10c6df74031c4f89f6e2b3.png

长度比 10000 小(也有可能等于 10000,概率较小),同时也可能会抛出数组下标越界的异常,证明 StringBuilder 确实是线程不安全的,为什么是这样呢?我们查看源码来分析。

StringBuilder 的 append() 方法底层调用 AbstractStringBuilder 的 append() 方法,如下所示。

28eaaedd3b29b47cdc1949bcdac8d07d.png

count 为字符串长度,len 为追加的字符串长度,count += len 这行代码如果是多线程同时访问,很可能会出现数据错误,比如 count = 0,len = 1,两个线程同时执行到这一行,获取的 count 都是 0,执行的结果都是 1,所以最终 count 的值为 1,而不是 2,这就解释了为什么最终的长度有可能比预期结果小的原因。

再来说说为什么会抛出数组下标越界异常?

字符的添加是通过调用 putStringAt(count,str) 方法完成的,count 为当前字符串的长度,通过 ensureCapacityinternal(count+len) 方法对数组进行扩容之后,它一定是小于等于数组最大容量的,putStringAt(count,str) 方法中每添加一个字符,都会给 count 加 1,当到达数组长度上限之后再进行扩容。

但是如果是两个线程同时执行 putStringAt(count,str),假设此时的 count = 3,数组容量为 4,两个线程拿到的 count 都为 3,数组容量大于 count,所以并不会进行扩容,这就意味着只剩一个空间,要插入两个字符,线程 A 执行完毕,count 变为 4,已经占满了整个数组,所以线程 B 执行的时候,超出了数组的长度,抛出异常。

986186cfe298ab190d0ce83eb90d877a.png

高频面试题

1、StringBuilder 的效率一定比 String 更高吗?

我们通常会说 StringBuilder 效率要比 String 高,严谨一点这句话不完全对,虽然大部分情况下使用 StringBuilder 效率更高,但在某些特定情况下不一定是这样,比如下面这段代码:

String 

此时,使用 String 创建 "HelloWorld" 的效率要高于使用 StringBuilder 创建 "HelloWorld",这是为什么呢?

因为 String 对象的直接相加,JVM 会自动对其进行优化,也就是说 "Hello"+"World" 在编译期间会自动优化为 "HelloWorld",直接一次性创建完成,所以效率肯定要高于 StringBuffer 的 append 拼接。

但是需要注意的是如果是这样的代码:

String 

对于这种间接相加的操作,效率要比直接相加低,因为在编译器不会对引用变量进行优化。

2、下面代码的运行结果是?

String 

true,因为 "Hello"+" World" 在编译期间会被 JVM 自动优化成 "Hello World",是一个字符串常量,所以和 str1 引用相同。

3、下面代码的运行结果是?

String 

false,JVM 只有在 String 对象直接拼接的时候才会进行优化,如果是对变量进行拼接则不会优化,所以 str2 + " World" 并不会直接优化成字符串常量 "Hello World",同时这种间接拼接的结果是存放在堆内存中的,所以 str1 和 str3 的引用肯定不同。

4、String str = new String("Hello World") 创建了几个对象?

这是很常见的一道面试题,大部分的答案都是 2 个,"Hello World" 是一个,另一个是指向字符串的变量 str,其实是不准确的。

因为代码的执行过程和类的加载过程是有区别的,如果只看运行期间,这段代码只创建了 1 个对象,new 只调用了一次,即在堆上创建的 "Hello World" 对象。

而在类加载的过程中,创建了 2 个对象,一个是字符串字面量 "Hello World" 在字符串常量池中所对应的实例,另一个是通过 new String("Hello World") 在堆中创建并初始化,内容与 "Hello World" 相同的实例。

所以在回答这道题的时候,可以先问清楚面试官是在代码执行过程中,还是在类加载过程中。这道题目如果换做是 String str = new String("Hello World") 涉及到几个对象,那么答案就是 2 个。

5、String、StringBuffer、StringBuilder 有什么区别?

1、String 一旦创建不可变,如果修改即创建新的对象,StringBuffer 和 StringBuilder 可变,修改之后引用不变。

2、String 对象直接拼接效率高,但是如果执行的是间接拼接,效率很低,而 StringBuffer 和 StringBuilder 的效率更高,同时 StringBuilder 的效率高于 StringBuffer。

3、StringBuffer 的方法是线程安全的,StringBuilder 是线程不安全的,在考虑线程安全的情况下,应该使用 StringBuffer。

6、下面代码的运行结果是?

public 

Hello,因为 String 是不可变的,传入 test 方法的参数相当于 str 的一个副本,所以方法内只是修改了副本,str 本身的值没有发生变化。

7、下面代码的运行结果是?

public 

Hello World,因为 StringBuffer 是可变类型,传入 test 方法的参数就是 str 的引用,所以方法内修改的就是 str 本身。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值