无聊的笔记:之一(Java字符串链接方式效率对比和解析)
简介
在JAVA
开发过程中,经常会遇到字符串操作,对字符串的拼接操作更常见。
拼接字符串主要有以下几种方法:
1. "" + ""
2. "".concat("")
3. new StringBuilder().append()
4. new StringBuffer().append()
还有一个
StringUtils
工具类的join()
方法,这里不做讨论。
时间紧迫的朋友可以跳过分析,直接看结论。
分析
1. 直接使用+
拼接字符串
这是最方便的。但是它的性能在大部分情况下也是最低的一个。为什么这么说呢?请看官继续向下:
废话少说,先上个例子:
public class Test1 {
public static void main(String[] args) {
String s = "abc";
String s1 = "123" + s;
String s2 = "efg" + s1;
}
}
我们使用javap
工具对字节码文件 Test1.class 反编译一下,瞅瞅+
操作符干了些啥。
javap是java字节码反编译工具。-c 参数表示显示反汇编命令。
C:\com\demo> javap -c .\Test1.class
Compiled from "Test1.java"
public class com.demo.Test1 {
public com.demo.Test1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String 123
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: new #3 // class java/lang/StringBuilder
26: dup
27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
30: ldc #8 // String efg
32: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: aload_2
36: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_3
43: return
}
从反编译的代码中可以看出,编译器会把+
符号优化成StringBuilder
类,并使用 StringBuilder.append()
方法对字符拼接。最后调用 StringBuilder.toString()
方法,返回String。
在上面的代码中,我们对字符串进行了两次+
操作,在优化后的代码也创建两次StringBuilder对象
和调用两次toString()
方法。
既然编译器会把+
操作优化成StringBuilder
方法,那它们效率会一样?其实不然。
2. 为什么+
操作符比StringBuilder
效率低?
首先看看StringBuilder
怎么拼接字符串。
StringBuffer sb = new StringBuffer();
sb.append("abc");
sb.append("123");
sb.append("efg");
sb.toString();
上面的代码,只 new 了一个 StringBuilder 对象,而且只调用了一次 toString()
方法。
我们知道在java中实例化对象,其实是很费时的,还要回收什么的。要不就不会搞出 单例模式 这种东西了。
而toString()
这个方法更是浪费时间,我们来看下 StringBuilder 的 toString()
方法的源码。
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
//省略 n 多代码
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
//省略 n 多代码
}
StringBuilder和StringBuffer 都是 AbstractStringBuilder 的实现类,其底层是用 char[]
数组来储存数据的。StringBuilder默认初始char[]
大小是16,当添加的字符大于16个,就会自动扩容。扩容这个操作本质是对数组的复制,也挺费时的,有兴趣的可以看一下源码。而+
操作生成的StringBuilder默认大小为16,如果拼接的字符串过大,会频繁的扩容导致效率低下。
所以如果使用StringBuilder的时候,我们可以给一个参数capacity
,初始化char[]
的大小,这样可以避免频繁的扩容。
toString() 方法就是对 char[]
数组的复制。这可是个挺费时间的操作。
所以,虽然编译器对 +
操作进行了优化,但是由于频繁的实例化StringBuilder、频繁的调用toString()、在添加的字符串较大的情况下还会频繁的扩容,导致其效率极其低下。
但是,+
一定比StringBuilder
效率低嘛?答案当然是否定的。
3. +
什么时候比StringBuilder
高效?
我们再上两段代码:
public static void main(String[] args) {
// +
String s1 = "abc"+"123"+"efg";
// StringBuilder
StringBuilder sb = new StringBuilder();
String s2 = sb.append("abc").append("123").append("efg").toString();
}
同样把这段代码反编译一下,你知道,编译器把上面的代码编译成什么样了嘛?
public static void main(String[] args) {
String s1 = "abc123efg";
StringBuilder sb = new StringBuilder();
String s2 = sb.append("abc").append("123").append("efg").toString();
}
编译器直接在编译的时候就把 +
给组合完成了,运行时间为 0 。谁效率高谁效率低显而易见。
所以进行大量的字符串拼接操作 StringBuilder 更合适,而进行少量的,可预知的字符串拼接 +
更合适。(一般 for 循环中用 StringBuilder ,而静态变量等用 +)
4. StringBuilder与StringBuffer的区别。
它们两个的区别主要是 StringBuilder 是线程不安全的,而StringBuffer是线程不安全的。
我们看一下两个类的 append()
方法的源码:
StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
// ...
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
// ...
}
StringBuffer
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
// ...
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
// ...
}
StringBuilder比StringBuffer少了同步锁。其他基本相同,因为它们都是AbstractStringBuilder的实现类。是双胞胎(其实眼瞎的我莫名感觉他们的名字长得还蛮像的)
所以,在线程安全的情况下,StringBuilder更高效一点(毕竟同步锁也要花时间),而线程不安全就只能用 StringBuffer。
5. String.concat()拼接与StringBuilder的比较
上代码:
String s = "abc".concat("123").concat("efg");
来瞅瞅源码:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
String.concat()
方法调用了一次Arrays.copyOf(value, len + otherLen)
方法,一次性分配了两个个字符串长度的内存空间,只执行了一次空间分配,并对拼接的两个字符各复制了一次,复制了两次。
而StringBuilder的append()
在char[]
未满的情况下,不会扩容。只会在初始化的时候执行一次空间分配,对两个字符串各复制了一次,复制了两次,但是最后会调用toString()
,还会复制一次。
所以,比较之下,在只有两个字符串拼接的情况下,concat的效率要高一点,而大量的字符串拼接,StringBuilder效率会高一点,而且好在初始化的时候,指定char[]
容器的大小,这样可以避免过度的扩容。
测试环境
- 系统:windows10 x64
- 处理器:i7 9700k
- 内存:16G
- 台式电脑
测试代码
public class Demo {
public static void main(String[] args) {
int number = 10000000; //拼接次数
long start ; //开始时间
// concat
String s2 = "";
start = System.currentTimeMillis();
for(int i = 0 ; i < number ; ++i){
s2.concat(String.valueOf(i));
}
System.out.println(" concat 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
// StringBuilder
StringBuilder stringBuilder = new StringBuilder();
start = System.currentTimeMillis();
for(int i = 0 ; i < number ; ++i){
stringBuilder.append(String.valueOf(i));
}
System.out.println("StringBuilder 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
//StringBuffer
StringBuffer stringBuffer = new StringBuffer();
start = System.currentTimeMillis();
for(int i = 0 ; i < number ; ++i){
stringBuffer.append(String.valueOf(i));
}
System.out.println(" StringBuffer 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
// +
String s = "";
start = System.currentTimeMillis();
for(int i = 0 ; i < number ; ++i){
s += String.valueOf(i);
}
System.out.println(" + 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
}
}
测试结果
测试结果取几次或者1次的平均值整数,不同运行环境会有偏差
+ | concat | StringBuilder | StringBuffer | |
---|---|---|---|---|
1000次 | 3ms | 1ms | 1ms | 1ms |
100000次 | 15584ms | 10ms | 6ms | 6ms |
1000000次 | 2212323ms | 56ms | 34ms | 44ms |
10000000次 | 很多很多ms | 408ms | 353ms | 448ms |
结论
+
最好不要直接大量使用。只有在逻辑较简单的情况下(没有for循环之类)或者字符确定的情况下时候使用。如:String a = "abc"+"123"
。- 一般情况,在大量字符串拼接操作中。使用StringBuilder和StringBuffer,并尽可能的估算字符串的大小,使用带
capacity
参数的构造函数,也就是char[]
的大小,这样可以避免重复的扩容。 - StringBuffer 在线程不安全的情况下使用,其他情况一般使用StringBuilder。
- 只有两个字符串拼接的时候使用 concat(),这个性能最好。 但是每次拼接操作都会分配内存的操作,而上面两个并不一定每次拼接操作都会分配内存。
作者:BobC
文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。