文章来源于公众号:程序新视界 作者:丑胖侠二师兄
关于字符串的面试题除了内存分布、equals
比较,最常见的就是与StringBuffer
和StringBuilder
之间的区别了。
如果你回答:String
类是不可变的,StringBuffer
和StringBuilder
是可变类,StringBuffer
是线程安全的,StringBuilder
则不是线程安全的。
就上面的总结而言,好像知道的有点少。本篇文章就带领大家全面的了解一下它们三个的区别与底层实现。
String字符串的拼接
关于String
字符串前面多篇文章已经详细描述过,它的不可变性也是因为每当通过“+”操作时,都会在内存中生成新的字符串而导致的。
String a = "hello ";
String b = "world!";
String ab = a + b;
针对上述代码,内存分布图如下:
其中 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());
编译器的插件甚至建议你使用String
来代替。
StringBuffer与StringBuilder的对比
StringBuffer
和StringBuilder
实现的核心代码基本一致,很多代码都是公用的。这两个类均继承自抽象类AbstractStringBuilder
。
我们来从构造方法到append
方法来逐一看一下它们的区别。先看StringBuilder
的构造方法:
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
其中super
方法便是调用的AbstractStringBuilder
的构造方法。对应StringBuffer
的构造方法中实现也是如此:
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
从构造方法来说,StringBuffer
和StringBuilder
是一样的。下面再看看append
方法,StringBuilder
实现如下:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer
对应的方法如下:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
很显然,在StringBuffer
的append
方法实现上除了内部将toStringCache
变量赋值为null
,唯一的不同就是在方法上使用synchronized
进行了同步处理。
toStringCache
是用来缓存最后一次调用toString
方法时生成的字符串,当StringBuffer
内容变动时,该值也会变动。
通过上面的append
方法的对比,我们可以很轻易的发现StringBuffer
是线程安全的,StringBuilder
是非线程安全的。当然,使用synchronized
进行同步处理,性能便会降低很多。
StringBuffer与StringBuilder的底层实现
StringBuffer
与StringBuilder
都调用了父类的构造方法:
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
通过该构造方法我们可以看到它们用来处理字符串信息的关键属性为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
数字中已经使用的长度。
那么,当没有使用synchronized
进行同步操作时,线程不安全发生在哪里?上面代码中count+=len
并不是原子操作。比如当前count
为 5,两个线程同时执行到 ++ 操作,拿到的值都为 5,执行完加操作之后赋值给count
,两个线程赋值都为 6,而不是 7。此时便出现了线程不安全的问题。
为什么String要设计成不可变
在 Java 中将String
设计成不可变的是综合考虑到各种因素的结果,有如下原因:
1、字符串常量池的需要,如果字符串可变,改变一个对象会影响到另外一个独立的对象。不变这也是字符串常量池存在的前提条件。
2、Java 中String
对象的哈希码被频繁地使用,比如在HashMap等容器中。字符串不变保证了hash码的唯一性,可以方向缓存并使用。
3、安全性,确保String
在当做参数传递时保持不变,避免安全隐患。比如在数据库用户名、密码、访问路径等传输过程中的保持不变,防止改变字符串指向对象的值被改变。
4、由于字符串变量不可变,在多线程中可以被共享使用。
小结
单纯的死记硬背面试题我们都会,但要在记忆面试题的过程中了解更多底层实现原理,不仅仅有助于理解“为什么”,同时还能学到更多相关的知识和原理。
在本文中简化了StringBuilder
和StringBuffer
内部数据的 copy 、数组扩容等步骤的讲解,感兴趣的朋友可以继续对照源码进行深入研究。
以上就是W3Cschool字节宝
关于Java面试题:谈谈String、StringBuffer、StringBuilder的区别?的相关介绍了,希望对大家有所帮助。