在Java的开发过程中,使用频率最高的就是String字符串,但由于在字符串存储和拼接的过程中,涉及到很多场景需要因地制宜的选用StringBuider与StringBuffer。
我们先来聊一聊String,由源码引入话题:
Java8中的String源码
public final class String implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
//...
}
我们从源码中可以总结出如下几点内容:
Java9以后String的底层数据由char类型改为了byte类型,看源码:
public final class String
implements java.io.Serializable, Comparable, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
}
这样做的目的是为了节省字符串占用的内存空间,在大字符串处理中提升效率,同时也降低了垃圾回收的次数,提升性能。
面试官:“String类设置为不可变对象的话,岂不是每次创建或者字符串拼接的时候都会创建对象,增加开销?那为什么还要用final修饰呢?”
我:“嗯~这个问题非常好呀,我理解的是两个原因”
1、String 类是最常用的类之一,为了效率,禁止被继承和重写
2、为了安全。String 类中有很多调用底层的本地方法,调用了操作系统的API,
如果方法可以重写,可能被植入恶意代码,破坏程序。其实Java 的安全性在这里就有一定的体现啦。
基于以上String的特点,Java官方自然也知道它的弊端,所以为了全场景的最优方案适配,便产生了StringBuilder和StringBuffer这两个String的近亲字符串处理类,他们都实现了CharSequence 接口。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,所以可以解决在字符串拼接时的性能问题,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法,比如 append 方法。
源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
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;
}
//...
}
StringBuilder源码
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
// ...
public StringBuilder append(String str) {
super.append(str);
return this;
}
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
// ...
}
StringBuilder替代String的字符串拼接案例
Java 是一门解释型的编程语言,所以当编译器遇到 + 号这个操作符的时候,会将
new String("Hello") + new String("World")
解释为
new StringBuilder().append("Hello").append("World").toString();
并且在循环字符串拼接的时候,String的“+”拼接方式,会在循环内部多次创建StringBuilder对象,白白浪费了资源,如:
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
这一段代码完全就可以直接使用StringBuilder调用append实现方式来替代:
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);
StringBuffer源码
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
public StringBuffer() {
super(16);
}
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
public synchronized String toString() {
return new String(value, 0, count);
}
// 其他方法
}
从它的源码我们可以看出,内部的实现与StringBuilder相差无机,无非是在方法的前面加了synchronized 关键字进行了同步,以确保在多线程场景下的线程安全,但在大部分的非多线程场景下,由于同步锁的存在会导致执行效率过低,并且即便在多线程情况下,依旧可以通过ThreadLocal搭配StringBuilder的方式来安全的修改字符串,所以,实际开发中,StringBuilder 的使用频率也是远高于 StringBuffer,甚至可以这么说,StringBuilder 完全取代了 StringBuffer。
三者异同点总结
相同点:
1、都可以储存和操作字符串
2、都使用 final 修饰,不能被继承
3、提供的 API 相似
异同点:
1、String 是只读字符串,String 对象内容是不能被改变的
2、StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
3、StringBuilder 线程不安全;StringBuffer 线程安全