面试题系列第8篇:谈谈String、StringBuffer、StringBuilder的区别?

简介: 面试题系列第8篇:谈谈String、StringBuffer、StringBuilder的区别?

《Java面试题系列》:一个长知识又很有意思的专栏。深入挖掘、分析源码、汇总原理、图文结合,打造公众号系列文章,面试与否均可提升Level。欢迎持续关注【程序新视界】。第8篇。


关于字符串的面试题除了内存分布、equals比较,最常见的就是与StringBuffer和StringBuilder之间的区别了。


如果你回答:String类是不可变的,StringBuffer和StringBuilder是可变类,StringBuffer是线程安全的,StringBuilder则不是线程安全的。


就上面的总结而言,好像知道的有点少。本篇文章就带领大家全面的了解一下它们三个的区别与底层实现。


String字符串的拼接

关于String字符串前面多篇文章已经详细描述过,它的不可变性也是因为每当通过“+”操作时,都会在内存中生成新的字符串而导致的。


String a = "hello ";

String b = "world!";

String ab = a + b;

1

2

3

针对上述代码,内存分布图如下:


image.png其中a和b初始化时位于字符串常量池,ab拼接后的对象位于堆中。可以很直观的看出,经过拼接新生成了String对象。如果拼接多次,那么会生成多个中间对象。


上面的结论在Java8之前是成立的,在Java8时JDK对“+”号拼接进行了优化,上面所写的拼接方式会被优化为基于StringBuilder的append方法进行处理。


stack=2, locals=4, args_size=1
     0: ldc           #2                  // String hello
     2: astore_1
     3: ldc           #3                  // String world!
     5: astore_2
     6: new           #4                  // class java/lang/StringBuilder
     9: dup
    10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
    13: aload_1
    14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    17: aload_2
    18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    24: astore_3
    25: return

上面是通过javap -verbose命令反编译字节码的结果,很显然可以看到StringBuilder的创建和append方法的调用。


此时,如果再笼统的回答:通过加号拼接字符串会创建多个String对象,因此性能比StringBuilder差,就是错误的了。因为本质上加号拼接的效果最终经过编译器处理之后和StringBuilder是一致的。


如果你在代码中使用如下写法:


StringBuilder sb = new StringBuilder("hello ");

sb.append("world!");

System.out.println(sb.toString());

1

2

3

编译器的插件甚至建议你使用String来代替。


StringBuffer与StringBuilder的对比

StringBuffer和StringBuilder实现的核心代码基本一致,很多代码都是公用的。这两个类均继承自抽象类AbstractStringBuilder。


我们来从构造方法到append方法来逐一看一下它们的区别。先看StringBuilder的构造方法:


public StringBuilder(String str) {

   super(str.length() + 16);

   append(str);

}

1

2

3

4

其中super方法便是调用的AbstractStringBuilder的构造方法。对应StringBuffer的构造方法中实现也是如此:


public StringBuffer(String str) {

   super(str.length() + 16);

   append(str);

}

1

2

3

4

从构造方法来说,StringBuffer和StringBuilder是一样的。下面再看看append方法,StringBuilder实现如下:


@Override

public StringBuilder append(String str) {

   super.append(str);

   return this;

}

1

2

3

4

5

StringBuffer对应的方法如下:


@Override

public synchronized StringBuffer append(String str) {

   toStringCache = null;

   super.append(str);

   return this;

}

1

2

3

4

5

6

很显然,在StringBuffer的append方法实现上除了内部将toStringCache变量赋值为null,唯一的不同就是在方法上使用synchronized进行了同步处理。


toStringCache是用来缓存最后一次调用toString方法时生成的字符串,当StringBuffer内容变动时,改值也会变动。


通过上面的append方法的对比,我们可以很轻易的发现StringBuffer是线程安全的,StringBuilder是非线程安全的。当然,使用synchronized进行同步处理,性能便会降低很多。


StringBuffer与StringBuilder的底层实现

StringBuffer与StringBuilder都调用了父类的构造方法:


AbstractStringBuilder(int capacity) {

   value = new char[capacity];

}

1

2

3

通过该构造方法我们可以看到它们用来处理字符串信息的关键属性为value。在初始化时先初始化一个长度为传入字符串长度+16的char[]数组,也就是value值,用来存储实际的字符串。


在调用父类构造方法之后便是调用各自的append方法(见前面的代码),而其中的核心处理又的调用父类的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;
}

上述代码中其中str.getChars方法用来对传入的str字符串进行拼接,在原有的value数组后面进行填充。而count用来记录当前value数字中已经使用的长度。

image.png那么,当没有使用synchronized进行同步操作时,线程不安全发生在哪里?上面代码中count+=len并不是原子操作。比如当前count为5,两个线程同时执行到++操作,拿到的值都为5,执行完加操作之后赋值给count,两个线程赋值都为6,而不是7。此时便出现了线程不安全的问题。


为什么String要设计成不可变

在Java中将String设计成不可变的是综合考虑到各种因素的结果,有如下原因:


1、字符串常量池的需要,如果字符串可变,改变一个对象会影响到另外一个独立的对象。不变这也是字符串常量池存在的前提条件。


2、Java中String对象的哈希码被频繁地使用,比如在HashMap等容器中。字符串不变保证了hash码的唯一性,可以方向缓存并使用。


3、安全性,确保String在当做参数传递时保持不变,避免安全隐患。比如在数据库用户名、密码、访问路径等传输过程中的保持不变,防止改变字符串指向对象的值被改变。


4、由于字符串变量不可变,在多线程中可以被共享使用。


小结

单纯的死记硬背面试题我们都会,但要在记忆面试题的过程中了解更多底层实现原理,不仅仅有助于理解“为什么”,同时还能学到更多相关的知识和原理。


在本文中简化了StringBuilder和StringBuffer内部数据的copy、数组扩容等步骤的讲解,感兴趣的朋友可以继续对照源码进行深入研究。



目录
相关文章
|
2月前
|
安全 Java
String、StringBuffer、StringBuilder的区别
这篇文章讨论了Java中String、StringBuffer和StringBuilder的区别。String是不可变的,每次操作都会产生新的对象,效率低且浪费内存。StringBuilder可以在原字符串基础上进行操作,不开辟额外内存,弥补了String的缺陷。StringBuffer和StringBuilder类似,但StringBuffer的方法是线程安全的。文章还列举了StringBuffer的常用方法,并提供了使用示例代码。最后总结了这三者的主要区别。
String、StringBuffer、StringBuilder的区别
|
1月前
|
canal 安全 索引
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
33 5
|
23天前
|
存储 安全 Java
String、StringBuffer 和 StringBuilder 的区别
【10月更文挑战第21天】String、StringBuffer 和 StringBuilder 都有各自的特点和适用场景。了解它们之间的区别,可以帮助我们在编程中更合理地选择和使用这些类,从而提高程序的性能和质量。还可以结合具体的代码示例和实际应用场景,进一步深入分析它们的性能差异和使用技巧,使对它们的理解更加全面和深入。
14 0
|
2月前
|
安全 Java
Java StringBuffer 和 StringBuilder 类详解
在 Java 中,`StringBuffer` 和 `StringBuilder` 用于操作可变字符串,支持拼接、插入、删除等功能。两者的主要区别在于线程安全性和性能:`StringBuffer` 线程安全但较慢,适用于多线程环境;`StringBuilder` 非线程安全但更快,适合单线程环境。选择合适的类取决于具体的应用场景和性能需求。通常,在不需要线程安全的情况下,推荐使用 `StringBuilder` 以获得更好的性能。
|
2月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
39 0
java基础(13)String类
|
1月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
55 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
29天前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
24 2
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
2月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①
|
1月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
51 4