首先我们来看一段程序
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, 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的源码
@Overridepublic 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
分析完性能我们再来分析下他们的构成
先来分析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。扩容之后新申请的数组大小newCapacity小于0同时连接后字符串所需最小空间minimumCapacity也小于0,代表无法申请到足够大的空间,发生内存溢出抛出异常。若申请到改大小的空间之后执行数组的复制,返回连接后的数组,相当于新建了一个字符串的值。
StringBuffer进行复制的原理基本相同。
字符串连接时应该注意
若用String类型的字符串通过+进行拼接时,会创建一个新的String对象,你要知道String对象一旦创建就是不能被改变的,要达到字符串拼接的效果,就得不停创建新对象。StringBuilder直到最后sb.toString()才会创建String对象,之前都没有创建新对象,或者在超过数组代表长度的字符串时才进行字符串的创建,故从效率上来讲append()要优于+