String、StringBuffer、StringBuilder都属于字符串处理类,我们常用的字符串存储、拼接等功能通过这三个类都可以实现,但是在使用环境不同以及对代码的执行效率和安全性要求不同是,我们要有所区分。
先分析一下各个类的特点,然后再进行比较。
1、String:能在多线程环境下肆意使用的不可变的线程安全类,什么是“不可变”?没有接触过并发知识的童鞋可能会比较晕。下面给出一个简单的解释,一般来说,有Field的类,我们称之为有状态类,这些类的状态有可能会发生改变,如果一个类在单线程、多线程等环境下,它的状态都不会发生改变(一般用final修饰),那么这个类就属于不可变类。String通过把状态设置为private final让其状态不可变,将类修饰为final让本身不可继承,但是这还没有办法保证绝对的线程安全,如果发生指针逃逸(全局变量赋值,方法返回值,实例引用传递),引用类型所指向的内存堆的状态还是可能被改变,所以String采用了一些其它手段使自己变成一个不可变的线程安全类。String类中最核心的引用类型状态就是一个字符串数组char[] value(引用数据类型),String通过保护性拷贝保证了这个状态在各种环境下都是不能被调用者改变的,比如你想通过指针逃逸手段获得其状态value[]的引用,String不会直接把value[]引用给你,也不会直接使用你传入的引用给状态赋值,而是重新分配一块内存堆,拷贝当前状态值,或者拷贝传入的引用值到新的内存堆,再把这个新堆的引用给你,或赋值给本身状态。看看下面String的一个构造方法的代码:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.offset = 0;
this.count = count;
//拷贝了传入引用的内存堆,并把新的引用赋值给本身的状态value
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
String通过使用Arrays.copyOfRange()来实现保护性拷贝,这样它的状态就不会发生改变,我们知道线程安全问题发生的本质就是类的状态改变的不能预测性,连状态都不会发生变化了,还会发生线程安全问题吗,所以我们可以再任何环境下肆意使用String对象,而不能考虑线程安全问题。当然,我们也注意到了,内存堆的重新分配和拷贝必然带来大的空间、时间资源的开销,严重时可能会造成频繁的垃圾回收,安全就要赋值代价。
2、StringBuffer:通过继承AbstractStringBuilder实现了对自身状态的同步访问,它的状态char[] value继承自父类AbstractStringBuilder,这个状态没有final修饰,是可变的。StringBuffer类中有改变自身状态的方法,并且没有针对指针逃逸做出保护性的拷贝,所以该类在多线程环境下,如果肆意使用可能会产生线程安全问题(把对象引用共享给多个线程,每个线程无法保证读写同步),StringBuffer属于有条件的线程安全,依靠调用者自己去控制避免产生线程安全问题。虽然StringBuffer不够安全,但是这给了调用者带来了其他方面的好处。由于它可以改变自身状态,在很多情况下不需要拷贝自身状态而浪费空间和时间资源,从而提高了执行效率。且看如下代码:
//调用父类的append方法,通过使用synchronized增加同步功能
public synchronized StringBuffer append(char str[], int offset, int len) {
//调用父类的append方法
super.append(str, offset, len);
//返回自身的引用,产生指针逃逸,灵活但不安全,依赖使用者
return this;
}
// AbstractStringBuilder类实现的使用System.arraycopy方法拼接字符数组
public AbstractStringBuilder append(char str[], int offset, int len) {
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);
//对自身字符数组的count进行扩展后,向其尾部拼接字符数组
System.arraycopy(str, offset, value, count, len);
count = newCount;
return this;
}
可见,append方法属于同步方法,返回StringBuffer自身的引用,执行时不会做保护性拷贝,提高了执行效率,但是不够安全。
3、StringBuilder:也是继承AbstractStringBuilder类,但是与StringBuffer不同的是,它没有给任何方法加同步关键字synchronized,也没有实现像StringBuffer那么多的方法,只实现了一些常用的方法,如append、insert等,相当于是StringBuffer的一个轻量级版本,因为它是非线程安全的,所以在多线程环境下慎用。我们可以想到,没有了synchronized的束缚,它的执行效率肯定会有所提高。看一下它的一个append方法,与StringBuffer做一个对比就知道了:
//调用父类的append方法,没有使用synchronized增加同步功能
public synchronized StringBuffer append(char str[], int offset, int len) {
//调用父类的append方法
super.append(str, offset, len);
//返回自身的引用,产生指针逃逸,灵活但不安全,依赖使用者
return this;
}
特性分析完成后,在使用这三个类时,就可以针对不同的使用环境,做出合理的选择。下面以字符串拼接为例,来分析一下三个类的使用情况:
1)单线程环境下,不用考虑线程安全问题,就执行效率而言,StringBuilder无疑使最好的,因为与StringBuilder相比,StringBuffer有synchronized同步锁,增加了开销,String对自身做出了保护性拷贝,大量的增加了开销,所以选择的优先级别为:StringBuilder > StringBuffer > String。但是有些时候我们要拼接一些常量字符串(可能是因为字符串太长而分行进行拼接),比如“123”+ “abc”+ “456”,可以直接使用String str = “123”+ “abc”+ “456”,因为JVM在编译此代码时直接将其优化成为“123abc456”放在字节码文件的常量池区域,使用时直接load到内存不需要做计算。
2)多线程环境下,要考虑线程安全问题,不过这个也得看具体情况,就线程安全的级别而言,三个类的level为:String > StringBuffer > StringBuilder,为什么上面已经分析过了。如何选择呢,这个就得分具体情况了,以下列出了几种常用场景:
a.如果对线程安全问题一无所知,请使用String,这可以确保你可以无脑的使用并且程序不会出现问题,如果实在对执行效率有要求,就去学习以下多线程编程基础知识,安全的去使用StringBuffer。
b.对执行效率有要求,比如程序中存在大量的字符串的拼接,如果需要方法同步优先使用StringBuffer,不需要方法同步优先使用StringBuilder。