从源码的角度解读String、StringBuilder、StringBuffer的性能差别
首先我们来看一段程序
package more;
/**
* 测试string StringBuffer StringBuilder的性能
* @author ybsem
*
*/
public class SSbSb {
public static void main(String[] args) {
String string = "s";
StringBuilder stringBuilder = new StringBuilder("s");
StringBuffer stringBuffer = new StringBuffer("s");
Long startString = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
string = string + i;
}
Long endString = System.currentTimeMillis();
System.out.println("创建string常量时间为:"+(endString-startString));
Long startStringBuilder = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
stringBuilder = stringBuilder.append(i);
}
Long endStringBuilder =System.currentTimeMillis();
System.out.println("创建stringBuilder对象时间为:"+(endStringBuilder-startStringBuilder));
Long startStringBuffer = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
stringBuffer = stringBuffer.append(i);
}
Long endStringBuffer = System.currentTimeMillis();
System.out.println("创建stringBuffer对象时间为:"+(endStringBuffer-startStringBuffer));
}
}
测试结果
由此我们可以从性能上看出String<StringBuffer<StringBuilder
现在我们从源码的角度来解读下为什么出现这种问题
String
首先来看String的核心代码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];//final类型char数组
//省略其他代码……
……
}
由此可以看出一个String string = “s”其实是一个字符数组,并且是不可变的10000; i++) { string = string + i; }
这段代码其实相当于创建了一万个新的String对象,所以有大量对字符串的操作时不宜使用String
StringBuilder
我们来看下StringBuilder的核心源码
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj)); //当传入的是一个对象时先将其转换为String类型
}
@Override
public StringBuilder append(String str) {
super.append(str); //执行父类的append()方法,他的父类为AbstractStringBuilder
return this;
}
//StringBuilder的父类append()方法public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); //扩容函数
str.getChars(0, len, value, count); //value是原字符串的值
/*
* public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
* if (srcBegin < 0) {
* throw new StringIndexOutOfBoundsException(srcBegin);
* }
* if (srcEnd > value.length) {
* throw new StringIndexOutOfBoundsException(srcEnd);
* }
* if (srcBegin > srcEnd) {
* throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
* }
* System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); //是一个本地 //方法用将两个字符串相连,通过数组的copy
* }
*
*/
count += len; //conut初始值为0,第一次执行append()方法时count变为字符 //串的值
return this;
}
由上边的源码可以看出StringBuilder是通过底层数组进行连接的效率高,但同时也应该看到该段代码没有进行同步,所以在多线程环境下是不安全的
StringBuffer
再来看StringBuffer的源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
/父类的appen()的方法
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基本相同
StringBuffer与StringBuilder的区别在于将append()进行了同步,保证了多线程条件下的安全性,但在获得安全性的同时牺牲了性能,所以StringBuffer要比StringBuilder要快的原因
总结如下:
- 在进行少量对字符串的操作时使用String
- 在多线程环境下,并且对字符串操作次数较多则使用StringBuffer
- 在单线程环境下,并且对字符串操作次数较多则使用StringBuilde
分析完性能我们再来详细的分析下append()方法
先来分析String的构造函数
/** The value is used for character storage. */
private final char value[];
/* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
有这段代码可知以及代码注释可知,string其实是一个数组,由于他是一个不可变类,新创建的字符串是参数字符串的副本。 所以呢除非需要这个副本,否则没必要用这个构造函数来进行创造。String类的字符串串创建时数组的大小是由字符串常量池也就是jvm来进行分配的。具体我们可以来分析下StringBuilder
先来看一段stringBuilder的构造函数
1 /**
2 * Constructs a string builder with no characters in it and an
3 * initial capacity of 16 characters.
4 */
5 public StringBuilder() {
6 super(16);
7 }
8 /**
9 * Constructs a string builder with no characters in it and an
10 * initial capacity specified by the {@code capacity} argument.
11 */
12 public StringBuilder(int capacity) {
13 super(capacity);
14 }
15
16 /**
17 * Constructs a string builder initialized to the contents of the
18 * specified string. The initial capacity of the string builder is
19 * {@code 16} plus the length of the string argument.
20 */
21 public StringBuilder(String str) {
22 super(str.length() + 16);
23 append(str);
24 }
/**上述构造函数调用super函数
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
这段代码清晰的看出他是基于数组构建的。初始大小为16,stringBuilder继承子AbstractStringBuilder。其次需要了解下他的一个很用要的方法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;
}
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
检查数组大小是否足够分三步,count+len其实就是新字符床的长度(count旧字符串的长度len需要连接字符串的额长度),若最小长度也就是count+len小于0,不做任何事返回,因为这代表没有任何意义。若大于零则执行ensureCapacityInternal方法,value.length代表的是连接前的字符串的值,若新字符串也就是连接后字符串的大小大于旧字符串的长度,则代表数组大小可能不够用,进行扩容。执行expandCapacity方法,扩容后大小为length*2+2。这是很可怕的,如果在构造的时候没有指定容量,那么很有可能在扩容之后占用了浪费大量的内存空间。使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。扩容之后新申请的数组大小newCapacity小于0同时连接后字符串所需最小空间minimumCapacity也小于0,代表无法申请到足够大的空间,发生内存溢出抛出异常。若申请到改大小的空间之后执行数组的复制,返回连接后的数组,相当于新建了一个字符串的值。
StringBuffer进行复制的原理基本相同。
字符串连接时应该注意
若用String类型的字符串通过+进行拼接时,会创建一个新的String对象,你要知道String对象一旦创建就是不能被改变的,要达到字符串拼接的效果,就得不停创建新对象。StringBuilder直到最后sb.toString()才会创建String对象,之前都没有创建新对象,或者在超过数组代表长度的字符串时才进行字符串的创建,故从效率上来讲append()要优于+