前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >参加了这么多面试,还是不懂StringBuffer和StringBuilder的区别?

参加了这么多面试,还是不懂StringBuffer和StringBuilder的区别?

作者头像
南风
发布2019-09-25 15:17:43
4330
发布2019-09-25 15:17:43
举报
文章被收录于专栏:Java大联盟Java大联盟

StringBuffer

上一节我们详细介绍了 String 类的使用:

【面试专题】讲透必考点String,带配套视频

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

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

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

代码语言:javascript
复制
//String
long startTime = System.currentTimeMillis();
String str = "";
for(int i = 0;i<50000;i++){
    str += i;
}
long endTime = System.currentTimeMillis();
System.out.println("String类型操作耗时"+(endTime-startTime)+"毫秒");

//StringBuffer
long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for(int i = 0;i<50000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer类型操作耗时"+(endTime-startTime)+"毫秒");

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

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

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

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

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

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

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

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

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

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

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

StringBuffer 常用方法

具体代码如下所示。

代码语言:javascript
复制
StringBuffer stringBuffer = new StringBuffer();
System.out.println("StringBuffer:"+stringBuffer);
System.out.println("StringBuffer的长度:"+stringBuffer.length());
stringBuffer = new StringBuffer("Hello World");
System.out.println("StringBuffer:"+stringBuffer);
System.out.println("下标为2的字符是:"+stringBuffer.charAt(2));
stringBuffer = stringBuffer.append("Java");
System.out.println("append之后的StringBuffer:"+stringBuffer);
stringBuffer = stringBuffer.delete(3, 6);
System.out.println("delete之后的StringBuffer:"+stringBuffer);
stringBuffer = stringBuffer.deleteCharAt(3);
System.out.println("deleteCharAt之后的StringBuffer:"+stringBuffer);
stringBuffer = stringBuffer.replace(2,3,"StringBuffer");
System.out.println("replace之后的StringBuffer:"+stringBuffer);
String str = stringBuffer.substring(2);
System.out.println("substring之后的String:"+str);
str = stringBuffer.substring(2,8);
System.out.println("substring之后的String:"+str);
stringBuffer = stringBuffer.insert(6,"six");
System.out.println("insert之后的StringBuffer:"+stringBuffer);
System.out.println("e的下标是:"+stringBuffer.indexOf("e"));
System.out.println("下标6之后的e的下标是:"+stringBuffer.indexOf("e",6));
stringBuffer = stringBuffer.reverse();
System.out.println("reverse之后的StringBuffer:"+stringBuffer);
str = stringBuffer.toString();
System.out.println("StringBuffer对应的String:"+str);

运行结果如下图所示。

StringBuilder

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

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

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

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

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

代码语言:javascript
复制
//StringBuffer
long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for(int i = 0;i<500000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer类型操作耗时"+(endTime-startTime)+"毫秒");

//StringBuilder
long startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for(int i = 0;i<500000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuilder类型操作耗时"+(endTime-startTime)+"毫秒");

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

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

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

StringBuilder 的具体使用如下所示。

代码语言:javascript
复制
StringBuilder stringBuilder = new StringBuilder();
System.out.println("StringBuilder:"+stringBuilder);
System.out.println("StringBuilder的长度:"+stringBuilder.length());
stringBuilder = new StringBuilder("Hello World");
System.out.println("StringBuilder:"+stringBuilder);
System.out.println("下标为2的字符是:"+stringBuilder.charAt(2));
stringBuilder = stringBuilder.append("Java");
System.out.println("append之后的StringBuilder:"+stringBuilder);
stringBuilder = stringBuilder.delete(3, 6);
System.out.println("delete之后的StringBuilder:"+stringBuilder);
stringBuilder = stringBuilder.deleteCharAt(3);
System.out.println("deleteCharAt之后的StringBuilder:"+stringBuilder);
stringBuilder = stringBuilder.replace(2,3,"StringBuilder");
System.out.println("replace之后的StringBuilder:"+stringBuilder);
String str = stringBuilder.substring(2);
System.out.println("substring之后的String:"+str);
str = stringBuilder.substring(2,8);
System.out.println("substring之后的String:"+str);
stringBuilder = stringBuilder.insert(6,"six");
System.out.println("insert之后的StringBuilder:"+stringBuilder);
System.out.println("e的下标是:"+stringBuilder.indexOf("e"));
System.out.println("下标6之后的e的下标是:"+stringBuilder.indexOf("e",6));
stringBuilder = stringBuilder.reverse();
System.out.println("reverse之后的StringBuilder:"+stringBuilder);
str = stringBuilder.toString();
System.out.println("StringBuilder对应的String:"+str);

运行结果如下图所示。

StringBuilder 为什么线程不安全?

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

代码语言:javascript
复制
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 1000; j++){
                stringBuilder.append("a");
            }
        }
    }).start();
}

try {
    Thread.sleep(100);
    System.out.println(stringBuilder.length());
} catch (InterruptedException e) {
    e.printStackTrace();
}

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

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

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

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 执行的时候,超出了数组的长度,抛出异常。

高频面试题

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

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

代码语言:javascript
复制
String str = "Hello"+"World";
StringBuilder stringBuilder = new StringBuilder("Hello");
stringBuilder.append("World");

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

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

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

代码语言:javascript
复制
String str1 = "Hello";
String str2 = "World";
String str3 = str1+str2;

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

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

代码语言:javascript
复制
String str1 = "Hello World";
String str2 = "Hello"+" World";
System.out.println(str1 == str2);

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

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

代码语言:javascript
复制
String str1 = "Hello World";
String str2 = "Hello";
String str3 = str2 + " World";
System.out.println(str1 == str3);

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、下面代码的运行结果是?

代码语言:javascript
复制
public static void main(String[] args) {
    String str = "Hello";
    test(str);
    System.out.println(str);
}
public static void test(String str){
    str+="World";
}

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

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

代码语言:javascript
复制
public static void main(String[] args) {
  StringBuffer str = new StringBuffer("Hello");
  test(str);
  System.out.println(str);
}
public static void test(StringBuffer str){
  str.append(" World");
}

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java大联盟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档