当我有一个字符串需要将一个字符连接到它的末尾时,
出于任何性能原因,我应该更喜欢 s = .... + ']'
而不是 s = .... + "]"
吗?
我知道数组字符串连接和字符串构建器, 而且我不是在寻求有关如何连接字符串的建议。
我也知道有些人会很想向我解释过早的优化,一般来说我不应该为这些小事烦恼,请不要...
我问是因为从编码风格偏好我更愿意使用后者, 但在我看来,第一个应该稍微好一点,因为知道附加的只是一个字符,因此不需要像复制单个字符串时那样对这个单个字符进行任何内部循环。
更新
正如@Scheintod 所写,这确实是一个理论上的问题,并且必须更多地满足我的愿望,以更好地理解 java 的工作原理,而不是任何现实生活中的“让我们节省另一个微秒”的场景...... 也许我应该说得更清楚。
我喜欢了解“幕后”的工作方式,而且我发现它有时可以帮助我创建更好的代码...
真相 - 我根本没有考虑编译器优化...
我没想到 JIT 会为我使用 StringBuilder
s 而不是 String
s...
因为我(可能错误地)一方面认为字符串构建器比字符串“更重”,但另一方面在构建和修改字符串方面更快。所以我会假设在某些情况下使用 StringBuilder
s 会比使用 stings 效率更低......(如果不是这种情况,那么整个 String 类应该将其实现更改为这样就像 StringBuilder
一样,并为实际的不可变字符串使用一些内部表示...... - 还是 JIT 正在做的事情? - 假设对于一般情况,最好不要让开发者选择……)
如果它确实将我的代码更改到这样的程度,那么也许我的 Q 应该在那个级别询问它是否适合 JIT 做这样的事情,如果使用它会更好。
也许我应该开始查看编译后的字节码...[我需要学习如何在 java 中做到这一点...]
作为旁注和示例,说明我什至会考虑查看字节码 - 看看我的一篇关于 Optimizing Actionscript 2.0 - a bytecode perspective - Part I 的相当古老的博客文章它表明知道你的代码编译成什么确实可以帮助你编写更好的代码。
最佳答案
除了对此进行分析之外,我们还有另一种可能获得一些见解。我想关注可能的速度差异,而不是再次删除它们的东西。
让我们从这个 Test
类开始:
public class Test {
// Do not optimize this
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 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: getstatic #4 // Field A:Ljava/lang/String;
10: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
13: ldc #6 // String B
15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: astore_1
22: new #2 // class java/lang/StringBuilder
25: dup
26: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
29: getstatic #4 // Field A:Ljava/lang/String;
32: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: bipush 66
37: invokevirtual #8 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
40: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
43: astore_2
44: aload_1
45: aload_2
46: invokevirtual #9 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
49: pop
50: return
static {};
Code:
0: ldc #10 // String A String
2: putstatic #4 // Field A:Ljava/lang/String;
5: return
}
我们可以看到,涉及到两个 StringBuilder(第 4、22 行)。所以我们发现的第一件事是,使用 +
连接 Strings
实际上与使用 StringBuilder 相同。
我们在这里可以看到的第二件事是 StringBuilders 都被调用了两次。第一次用于附加 volatile 变量(第 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;
}
和
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
但只有在达到限制的情况下(它会将旧 StringBuffers char[] 的数组副本复制到新的数组中)。所以这对两者都是正确的,并没有真正的区别。
正如人们所看到的,还有许多其他操作已经完成,但真正的区别在于 value[count++] = c;
和 str.getChars(0, len, value, count );
如果我们查看 getChars,我们会发现,这一切都归结为一个 System.arrayCopy
,它在这里用于将 String 复制到 Buffer 的数组中,加上一些检查和额外的方法调用 vs.一个单一的数组访问。
所以我会说理论上使用 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% 左右>...
... 至少(免责声明)在 my system 上使用 my compiler 和 my cpu 并且 真的没有区别/在现实世界的程序中不明显。除了因为你有一段代码你经常运行,你所有的应用程序性能都取决于它。但是一开始你可能会做一些不同的事情。
EDIT4:
考虑一下。这是用于基准测试的循环:
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 - 连接字 rune 字 ('x' )与单个字符字符串文字 ("x"),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24859500/