纯粹出于兴趣,我一直在研究Oracle Java编译器如何处理String
串联,并且我看到了一些我没想到的东西。
给出以下代码:
public class StringTest {
public static void main(String... args) {
String s = "Test" + getSpace() + "String.";
System.out.println(s.toString());
}
// Stops the compiler optimising the concatenations down to a
// single string literal.
static String getSpace() {
return " ";
}
}
我期望编译器将其优化为相当于:
String s = new StringBuilder("Test").append(getSpace())
.append("String.").toString();
但它实际上编译为相当于:
String s = new StringBuilder().append("Test").append(getSpace())
.append("String.").toString();
我正在使用32位jdk1.7.0_55版本编译它。这是javap -v -l
:
public class StringTest
SourceFile: "StringTest.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/StringBuilder
#3 = Methodref #2.#25 // java/lang/StringBuilder."<init>":()V
#4 = String #27 // Test
#5 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#6 = Methodref #13.#29 // StringTest.getSpace:()Ljava/lang/String;
#7 = String #30 // String.
#8 = Methodref #2.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #34.#31 // java/lang/String.toString:()Ljava/lang/String;
#11 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = String #37 //
#13 = Class #38 // StringTest
#14 = Class #39 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 getSpace
#22 = Utf8 ()Ljava/lang/String;
#23 = Utf8 SourceFile
#24 = Utf8 StringTest.java
#25 = NameAndType #15:#16 // "<init>":()V
#26 = Utf8 java/lang/StringBuilder
#27 = Utf8 Test
#28 = NameAndType #40:#41 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #21:#22 // getSpace:()Ljava/lang/String;
#30 = Utf8 String.
#31 = NameAndType #42:#22 // toString:()Ljava/lang/String;
#32 = Class #43 // java/lang/System
#33 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#34 = Class #46 // java/lang/String
#35 = Class #47 // java/io/PrintStream
#36 = NameAndType #48:#49 // println:(Ljava/lang/String;)V
#37 = Utf8
#38 = Utf8 StringTest
#39 = Utf8 java/lang/Object
#40 = Utf8 append
#41 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = Utf8 toString
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/lang/String
#47 = Utf8 java/io/PrintStream
#48 = Utf8 println
#49 = Utf8 (Ljava/lang/String;)V
{
public StringTest();
flags: ACC_PUBLIC
LineNumberTable:
line 2: 0
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public static void main(java.lang.String...);
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
LineNumberTable:
line 4: 0
line 5: 27
line 6: 37
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #4 // String Test
9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: invokestatic #6 // Method getSpace:()Ljava/lang/String;
15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: ldc #7 // String String.
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_1
27: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: invokevirtual #10 // Method java/lang/String.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: return
LineNumberTable:
line 4: 0
line 5: 27
line 6: 37
static java.lang.String getSpace();
flags: ACC_STATIC
LineNumberTable:
line 10: 0
Code:
stack=1, locals=0, args_size=0
0: ldc #12 // String
2: areturn
LineNumberTable:
line 10: 0
}
有趣的是,我已经读过here ECJ编译器确实实际编译到争论的构造函数(尽管我还没有为自己验证过),所以我的问题是为什么Oracle的编译器没有进行相同的优化?
根据评论,我使用较长的String
进行了另一项测试,以便立即超过StringBuilder
支持char[]
的默认长度:
public class StringTest {
public static void main(String... args) {
String s = "Testing a much, much longer " + getSpace() + "String.";
System.out.println(s.toString());
}
// Stops the compiler optimising the concatenations down to a single string literal
static String getSpace() {
return " ";
}
}
除了文字内容略有不同外,生成的字节码完全相同,仍然使用no-args构造函数在附加StringBuilder
之前对其进行实例化。在这种情况下,代码的有争议的构造函数应该超出我的判断。这是因为需要在第一次调用char[]
时重新调整后备append()
的大小,然后可能需要在下一次{{1}时再次执行此操作如果附加的append()
特别大。
在AnubianNoob的建议中,我对String
进行了快速性能测试,看看它是否确实针对空数组进行了优化。这是使用的代码:
System.arraycopy(...)
在带有i7-2600 CPU @ 3.40 GHz 3.39 GHz和3.24 GB可用RAM的Windows 7.1 32位计算机上运行:
public class ArrayCopyTest {
public static void main(String... args) {
char[] array = new char[16];
final long test1Start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
System.arraycopy(array, 0, array, 0, array.length);
}
final long test1End = System.nanoTime();
System.out.println("Elapsed Time (empty array copies)");
System.out.println("=================================");
System.out.println((test1End - test1Start) + "ns");
char[] array2 = new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
final long test2Start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
System.arraycopy(array2, 0, array2, 0, array2.length);
}
final long test2End = System.nanoTime();
System.out.println("Elapsed Time (non-empty array copies)");
System.out.println("=====================================");
System.out.println((test2End - test2Start) + "ns");
}
}
为了确定,我跑了大约五次。实际上,当数组没有空时,它在一百万次迭代中表现更好。正如Mike Strobel正确指出的那样,上述并不是一个有意义的基准。 / p>
答案 0 :(得分:4)
可能是因为String
构造函数无论如何调用了append()
:
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
答案 1 :(得分:4)
我认为这只是懒惰。为什么?因为如果你选择了arg-constructor,你需要进一步检查。你必须检查要连接的第一个表达式是否是一个字符串,如果是,你可以使用arg构造函数,否则,你必须回退到no-arg构造函数。这比简单地总是使用no-arg构造函数要多得多。
如果我是那个编译器开发人员,我也会选择简单的方法,因为隐式字符串连接肯定不是许多应用程序中的瓶颈,而且差别很小,以至于不值得麻烦。
大多数人认为编译器是由超级人类设计的魔术程序,总能做到最好的东西。但事实并非如此,编译器也是由通常的程序员编写的,他们并不总是在思考什么是 编译任何特定事物的最佳方法。他们有紧迫的时间表和需要完成的功能,因此最简单的解决方案往往是首选解决方案。
答案 2 :(得分:1)
这可能是因为JVM优化了字符串连接,它可能更好地识别字节码中的字符串连接模式,就像现在实现它一样。
答案 3 :(得分:0)
正如另一个人所提到的那样,StringBuilder
类在其构造函数中调用append()
,并且自己追加它会更具可读性和一致性。
考虑:
new StringBuilder("Hello").append("World");
new StringBuilder().append("Hello").append("World");
这可能不是最好的例子,但是两个附加内容比将其传递给构造函数要简单得多。速度是一样的。
答案 4 :(得分:0)
顺便说一句,JDK问题跟踪器中存在相关问题:JDK-4059189和相关问题。最初的提案是1997年的日期!那里没有太多的讨论。这意味着这个问题被认为是不重要的,或者这个案例是由JIT优化的。