为什么不建议在循环体中使用+进行字符串拼接?
最近复习一下了阿里Java开发规范,并记录一下。
我们首先来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率如何吧(jdk1.8)
public class StringBuilderAndAdd {
public static void main(String[] args) {
long s1 = System.currentTimeMillis();
new StringBuilderAndAdd().addMethod();
System.out.println(" + 拼接:" + (System.currentTimeMillis() - s1));
s1 = System.currentTimeMillis();
new StringBuilderAndAdd().stringBuilderMethod();
System.out.println(" StringBuilder 拼接:" + (System.currentTimeMillis() - s1));
}
public String addMethod() {
String result = "";
for (int i = 0; i < 100000; i++) {
result += "123";
}
return result;
}
public String stringBuilderMethod() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < 100000; i++) {
result.append("123");
}
return result.toString();
}
}
执行结果,很明显采用 + 进行字符串拼接效果远远比不上StringBuilder的效果。
下面来分析一下原因:
1. 先来看看 ‘+’ 的分析
public static void main(String[] args) {
String a = "123";
for (int i = 0; i < 20; i++) {
a += "456";
}
}
如下反编译后的代码,
Java中的 + 对字符串的拼接,其实现原理是使用 StringBuilder 的 append() 来实现的,使用 + 拼接字符串,其实只是 Java 提供的一个语法糖。
// 程序反编译后的代码,反编译可以使用指令 jad -sjava StringBuilderAndAdd.class(jad指令首先要有jad.exe在jdk的bin目录下)
public static void main(String args[])
{
String a = "123";
for(int i = 0; i < 20; i++)
a = (new StringBuilder()).append(a).append("456").toString();
}
而从Bytecode层面来看下,从idea中直接打开查看到,它在循环体中通过NEW java/lang/StringBuilder 创建对象。每次循环都创建对象很明显会花费很多时间。
// 程序Bytecode片段
public static main([Ljava/lang/String;)V
L0
LINENUMBER 17 L0
LDC "123"
ASTORE 1
L1
LINENUMBER 18 L1
ICONST_0
ISTORE 2
L2 //循环
FRAME APPEND [java/lang/String I]
ILOAD 2
BIPUSH 20
IF_ICMPGE L3
L4
LINENUMBER 19 L4
NEW java/lang/StringBuilder // 创建新StringBuilder对象,这个是在循环体内,所以每循环一次就会创建一个对象。
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "456"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L5
LINENUMBER 18 L5
IINC 2 1
GOTO L2 //跳转到L2,继续循环
L3
LINENUMBER 21 L3
FRAME CHOP 1
RETURN
2. 再来看看 ‘StringBuilder’ 的分析
// 程序反编译后的代码
public static void main(String[] args) {
StringBuilder a = new StringBuilder();
for (int i = 0; i < 20; i++) {
a.append("456");
}
}
如下可以看到循环只会调用append()方法操作字符串,不会重新创建StringBuiler对象。
public static void main(String args[])
{
StringBuilder a = new StringBuilder();
for(int i = 0; i < 20; i++)
a.append("456");
}
而从Bytecode层面来看下,从idea中直接打开查看到,它在进入循环体前就已经通过NEW java/lang/StringBuilder创建了对象,在循环体内只是调用了它的append()方法。
// 程序Bytecode片段
public static main([Ljava/lang/String;)V
L0
LINENUMBER 11 L0
NEW java/lang/StringBuilder // new StringBuilder对象,只会new一次
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ASTORE 1
L1
LINENUMBER 13 L1
ICONST_0
ISTORE 2
L2 // 循环
FRAME APPEND [java/lang/StringBuilder I]
ILOAD 2
BIPUSH 20
IF_ICMPGE L3
L4
LINENUMBER 14 L4
ALOAD 1
LDC "456"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
POP
L5
LINENUMBER 13 L5
IINC 2 1
GOTO L2 // 这里循环一次后的跳转到L2去
L3
LINENUMBER 16 L3
FRAME CHOP 1
RETURN
其实,StringBuilder内部也有一个char数组
char[] value;
但是与String不同的是,这个字符数组不是被final修饰的,所以是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
int count;
StringBuilder类继承了 AbstractStringBuilder 类.
调用StringBuilder.append()实际上是调用了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; // 从这个操作来看可以很明显分析出,StringBuilder类是线程不安全的。
return this;
}
该方法首先会判断参数是否为 null ,如果为 null 就调用appendNull()方法。
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
// 这里不太懂为什么是这样的
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
ensureCapacityInternal()方法是判断拼接后的字符数组长度是否超过当前数组长度,如果超过,则调用 Arrays.copyOf() 方法进行扩容并复制
/**
* This method has the same contract as ensureCapacity, but is
* never synchronized.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
// 首先尝试计算扩容后大小为现有value.length的2倍 + 2
int newCapacity = value.length * 2 + 2;
// 判断是否还是比“必须要有的容量大小” 要小
// 小就直接用“必须要有的容量大小”
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
// 上面计算要是溢出就进入这个if
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
// 扩容,并把之前的数据copy进去
value = Arrays.copyOf(value, newCapacity);
}
最后,将拼接的字符串 str 复制到目标数组 value 中。
str.getChars(0, len, value, count);
因此在循环体拼接字符串时,应该使用 StringBuilder 的 append() 去完成拼接。