除了对这个进行剖析,我们还有另一种可能性来获得一些见解。我想集中关注可能的速度差异,而不是再次消除它们的事情。
所以让我们从这个Test
类开始:
public class Test {
public static volatile String A = "A String";
public static void main( String [] args ) throws Exception {
String a1 = A + "B";
String a2 = A + 'B';
a1.equals( a2 );
}
}
我用 javac Test.java 进行了编译(使用 javac -v: javac 1.7.0_55)
使用 javap -c Test.class 我们可以得到:
Compiled from "Test.java"
public class Test {
public static volatile java.lang.String A;
public Test();
Code:
0: aload_0
1: invokespecial #1
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2
3: dup
4: invokespecial #3
7: getstatic #4
10: invokevirtual #5
13: ldc #6
15: invokevirtual #5
18: invokevirtual #7
21: astore_1
22: new #2
25: dup
26: invokespecial #3
29: getstatic #4
32: invokevirtual #5
35: bipush 66
37: invokevirtual #8
40: invokevirtual #7
43: astore_2
44: aload_1
45: aload_2
46: invokevirtual #9
49: pop
50: return
static {};
Code:
0: ldc #10
2: putstatic #4
5: return
}
我们可以看到,涉及到两个StringBuilder(第4行和第22行)。所以我们首先发现的是,使用+
连接Strings
实际上与使用StringBuilder相同。
这里我们可以看到的第二件事是,两个StringBuilders都被调用了两次。第一次是为了添加易变的变量(第10行和第32行),第二次是为了添加常量部分(第15行和第37行)。
在A +“B”
的情况下,append
被调用时带有一个 Ljava/lang/String
(一个字符串)参数,而在A +'B'
的情况下,它被调用时带有一个C
(一个字符)参数。
因此,编译器不会将String转换为char,而是保留其原始状态*。
现在查看包含使用的方法的AbstractStringBuilder
:
public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1);
value[count++] = c;
return this;
}
And(同时)
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
实际调用的方法是关键。在这里最昂贵的操作肯定是ensureCapacity
,但仅当达到限制时(它将旧StringBuffer的char[]复制到新StringBuffer中)。因此,对于两者都是如此,没有真正的区别。
正如可以看到的,还有许多其他操作,但真正的区别在于value[count++] = c;
和str.getChars(0、len、value、count);
如果我们查看getChars,就会发现它归结为一个System.arrayCopy
,该方法用于将字符串复制到缓冲区的数组中,加上一些检查和额外的方法调用,而A + 'B'
只需要一次单个数组访问。
因此,我认为从理论上讲,使用A + "B"
比使用A + 'B'
要慢得多。
我认为在实际执行中也更慢。但要确定这一点,我们需要进行基准测试。
编辑:
当然,这都是在JIT发挥魔力之前。请参见Stephen C的答案。
编辑2:
我一直在查看Eclipse编译器生成的字节码,它几乎相同。因此,至少这两个编译器在结果上没有区别。
编辑2:
现在是有趣的部分
基准测试。这个结果是在热身后运行了几次a+'B'
和a+"B"
循环0..100M后生成的:
a+"B": 5096 ms
a+'B': 4569 ms
a+'B': 4384 ms
a+"B": 5502 ms
a+"B": 5395 ms
a+'B': 4833 ms
a+'B': 4601 ms
a+"B": 5090 ms
a+"B": 4766 ms
a+'B': 4362 ms
a+'B': 4249 ms
a+"B": 5142 ms
a+"B": 5022 ms
a+'B': 4643 ms
a+'B': 5222 ms
a+"B": 5322 ms
平均值为:
a+'B': 4608ms
a+"B": 5167ms
即使在合成知识的真实基准测试世界中(呵呵),a+'B'
比a+"B"
快约10%……
...至少(免责声明)在我的系统、我的编译器和我的CPU上,这真的没有什么区别/不值得注意。除非你有一段代码需要经常运行,而且所有应用程序的性能都取决于它。但那时你可能会首先考虑不同的事情。
编辑4:
思考一下。以下是用于进行基准测试的循环:
start = System.currentTimeMillis();
for( int i=0; i<RUNS; i++ ){
a1 = a + 'B';
}
end = System.currentTimeMillis();
System.out.println( "a+'B': " + (end-start) + " ms" );
所以我们不仅关注一个问题,而是测试Java循环性能、对象创建性能和变量赋值性能。因此,真正的速度差异可能会更大一些。
所以我们真正关心的不只是一个问题,我们还要测试Java循环、对象创建和变量赋值的性能。因此,实际的速度差异可能会更大。