Java中字符串的拼接与性能
在JAVA中拼接两个字符串的最简便的方式就是使用操作符+
了。如果你用+
来连接固定长度的字符串,可能性能上会稍受影响,如果你是在循环中来+
多个字符串的话,性能将指数倍的下降。
假设有一个字符串,我们将对这个字符串做大量循环拼接操作,对比使用
+
,String.concat()
,StringUtils.join()
,StringBuilder.append()
与StringBuffer.append()
等方式。它们之间的性能差距有多大?
测试用例
测试代码
public static void main(String[] args) {
int n = 100000;
plusStr(n);
concatStr(n);
joinStr(n);
stringBuilderStr(n);
stringBufferStr(n);
}
public static void plusStr(int n) {
String s = "";
long st = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
s = s + String.valueOf(i);
}
long et = System.currentTimeMillis();
System.out.println("Plus:" + (et - st) + " ms");
}
public static void concatStr(int n) {
String s = "";
long st = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
s = s.concat(String.valueOf(i));
}
long et = System.currentTimeMillis();
System.out.println("Concat:" + (et - st) + " ms");
}
public static void joinStr(int n) {
String s = "";
long st = System.currentTimeMillis();
List<String> stringList = new ArrayList<>();
for (int i = 0; i < n; i++) {
stringList.add(String.valueOf(i));
}
s = StringUtils.join(stringList, "");
long et = System.currentTimeMillis();
System.out.println("StringUtils.join:" + (et - st) + " ms");
}
public static void stringBuilderStr(int n) {
String s = "";
long st = System.currentTimeMillis();
StringBuilder builder = new StringBuilder(s);
for (int i = 0; i < n; i++) {
builder.append(String.valueOf(i));
}
builder.toString();
long et = System.currentTimeMillis();
System.out.println("StringBuilder:" + (et - st) + " ms");
}
public static void stringBufferStr(int n) {
String s = "";
long st = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer(s);
for (int i = 0; i < n; i++) {
buffer.append(String.valueOf(i));
}
buffer.toString();
long et = System.currentTimeMillis();
System.out.println("StringBuffer:" + (et - st) + " ms");
}
测试结果:
- n = 10000时
Plus:259 ms Concat:53 ms StringUtils.join:7 ms StringBuilder:0 ms StringBuffer:1 ms
- n = 100000时
Plus:18403 ms Concat:5360 ms StringUtils.join:14 ms StringBuilder:5 ms StringBuffer:5 ms
源码解析
+
,由于String 是final对象,属于不可变对象,无法被修改,因而每次使用+
进行拼接都会创建新的对象,而不是改变原来的对象值,因而性能会较差。属于线程安全的;public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ... }
concat()
方法就是一次数组的拷贝,虽然在内存中时处理都是原子性操作,速度非常快,但最后的return语句会创建一个新String对象,限制了concat方法的速度。public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ... public String concat(String str) { if (str.isEmpty()) { return this; } int len = value.length; int otherLen = str.length(); char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); } ... }
StringUtils.Join()
(org.apache.commons.lang3),其内部还是用StringBuilder
实现,但是每次循环都多了个分隔符的判断,所以时间开销相对大了些,但相差不多,时间上来讲是一个数量级的。public class StringUtils { ... public static String join(Iterable<?> iterable, String separator) { return iterable == null ? null : join(iterable.iterator(), separator); } public static String join(Iterator<?> iterator, String separator) { if (iterator == null) { return null; } else if (!iterator.hasNext()) { return ""; } else { Object first = iterator.next(); if (!iterator.hasNext()) { String result = ObjectUtils.toString(first); return result; } else { StringBuilder buf = new StringBuilder(256); if (first != null) { buf.append(first); } while(iterator.hasNext()) { if (separator != null) { buf.append(separator); } Object obj = iterator.next(); if (obj != null) { buf.append(obj); } } return buf.toString(); } } } ... }
StringBuilder
可变字符串,继承自AbstractStringBuilder
,整个逻辑都只做字符数组的加长、拷贝,到最后也不会创建新的String对象,所以速度很快,完成拼接处理后在程序中用toString()
来得到最终的字符串主要用于字符串的拼接,属于线程不安全的;public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence { ... @Override public StringBuilder append(String str) { super.append(str); return this; } ... }
abstract class AbstractStringBuilder implements Appendable, CharSequence { ... 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; } ... }
StringBuffer
可变字符串,也是继承自AbstractStringBuilder,整个逻辑都只做字符数组的加长、拷贝,到最后也不会创建新的String对象,所以速度很快,完成拼接处理后在程序中用toString()来得到最终的字符串主要用于字符串的拼接,属于线程不安全的,但StringBuffer的append()方法使用了synchronized
关键字,以确保线程安全。public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence { ... @Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; } ... }
abstract class AbstractStringBuilder implements Appendable, CharSequence { ... 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; } ... }
总结
- 用
+
的方式效率最差,concat
由于是内部机制实现,比+
的方式好了不少,但两者对于时间/空间的开销都较大,因此适用于小数据量的操作。 Join
和StringBuffer
的内部都使用StringBuilder
实现,整体相差不大,但Join
内部包含了分隔符的处理,StringBuffer
则是包含了线程安全的处理。StringBuilder
的速度最快,但其存在线程安全的问题,而且只有JDK5及以上的版本支持。
扩充:
- 在编译阶段就能够确定的字符串常量,完全没有必要创建
StringBuilder
或StringBuffer
对象。直接使用字符串常量的+
连接操作效率最高(如:String str = "a" + "b" + "c";
)。 - 字符串的加号
+
方法, 虽然编译器对其做了优化,使用StringBuilder
的append
方法进行追加,但是每循环一次都会创建一个StringBuilder
对象,且都会调用toString
方法转换成字符串,所以开销很大(时间、空间),执行一次字符串+
,相当于str = new StringBuilder(str).append(“a”).toString()
。 - JVM对于
+
是这样处理的:首先创建一个String对象str,并把"abc"赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和"de"加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。 StringBuffer
和StringBuilder
的扩容策略:当字符串缓冲区容量不足时,原有容量将会加倍,以新的容量来申请内存空间,建立新的char数组,然后将原数组中的内容复制到这个新的数组当中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够预先评估StringBuilder
或StringBuffer
的大小,将能够有效的节省这些操作,从而提高系统的性能。