为什么下面的代码可以正常工作?
String str;
while (condition) {
str = calculateStr();
.....
}
但是这个被认为是危险/不正确的:
while (condition) {
String str = calculateStr();
.....
}
在循环外声明变量是必要的吗?
为什么下面的代码可以正常工作?
String str;
while (condition) {
str = calculateStr();
.....
}
但是这个被认为是危险/不正确的:
while (condition) {
String str = calculateStr();
.....
}
在循环外声明变量是必要的吗?
在您的示例中,我假设 str
并没有在 while
循环之外使用,否则您不会提出这个问题,因为在 while
循环内部声明它不是一个选项,因为它无法编译。
所以,由于 str
没有在循环之外使用,因此 str
的最小可能作用域在 while 循环 内部。
所以,答案是非常明确的,str
绝对应该在 while 循环内部声明。别废话。
唯一违反此规则的情况是,如果每个时钟周期对代码来说至关重要,那么您可能希望在外部范围内实例化某些内容,并重复使用它,而不是在内部范围的每次迭代中重新实例化。然而,这不适用于您的示例,因为在 Java 中字符串是不可变的:在循环开始时将始终创建 str 的新实例,并且必须在循环结束时将其丢弃,因此没有可能进行优化。
编辑:(注入我的下面评论)
无论如何,正确的做法是编写所有代码,为您的产品建立性能要求,根据此要求测量最终产品,如果不满足要求,则去优化。而通常发生的情况是,您会找到一些漂亮且正式的算法优化方法,只需在几个地方就可以使我们的程序符合其性能要求,而不必在整个代码库中进行大量调整和修改以挤压时钟周期。
我比较了这两个(相似)示例的字节码:
让我们看一下第一个示例:
package inside;
public class Test {
public static void main(String[] args) {
while(true){
String str = String.valueOf(System.currentTimeMillis());
System.out.println(str);
}
}
}
执行 javac Test.java
后,执行 javap -c Test
命令,你会得到以下结果:
public class inside.Test extends java.lang.Object{
public inside.Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2; //Method java/lang/System.currentTimeMillis:()J
3: invokestatic #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
6: astore_1
7: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: goto 0
}
让我们来看看第二个例子:
package outside;
public class Test {
public static void main(String[] args) {
String str;
while(true){
str = String.valueOf(System.currentTimeMillis());
System.out.println(str);
}
}
}
在执行javac Test.java
之后,使用javap -c Test
命令,你会得到以下结果:
public class outside.Test extends java.lang.Object{
public outside.Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2; //Method java/lang/System.currentTimeMillis:()J
3: invokestatic #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
6: astore_1
7: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: goto 0
}
观察结果表明,这两个示例中没有区别。 这是JVM规范的结果...
但出于最佳编码实践的考虑,建议在尽可能小的范围内声明变量(在此示例中,它位于循环内部,因为这是唯一使用变量的地方)。
final
关键字的人:在inside
包中将str
声明为final
也没有任何区别 =) - nikodaemus在最小范围内声明对象可以提高代码的可读性。
对于今天的编译器来说,性能并不重要(在这种情况下)。
从维护的角度看,第二个选项更好。
在尽可能狭窄的范围内声明和初始化变量。
正如唐纳德·厄文·克努斯所说:
"我们应该忘记小的效率问题,大约97%的时间:过早的优化是万恶之源。"
即程序员让性能考虑影响代码的设计的情况。这可能会导致设计不如预期那样简洁,或者代码出现错误,因为代码被优化复杂化,程序员受到优化的干扰。
如果你想在循环以外使用 str
,那么就把它声明在循环外面。否则,第二个版本就可以了。
请跳转到更新的答案...
对于那些关心性能的人,请删除 System.out 并将循环限制为 1 字节。在 Windows 7 Professional 64 位和 JDK-1.7.0_21 上,使用 double(测试 1/2)和使用 String(3/4),以毫秒为单位给出了经过的时间。字节码(也在测试1和测试2下给出),并没有相同。我太懒了,不想用可变和相对复杂的对象进行测试。
double
Test1 耗时:2710 毫秒
Test2 耗时:2790 毫秒
String(只需在测试中将 double 替换为 string)
Test3 耗时:1200 毫秒
Test4 耗时:3000 毫秒
编译和获取字节码
javac.exe LocalTest1.java
javap.exe -c LocalTest1 > LocalTest1.bc
public class LocalTest1 {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
double test;
for (double i = 0; i < 1000000000; i++) {
test = i;
}
long finish = System.currentTimeMillis();
System.out.println("Test1 Took: " + (finish - start) + " msecs");
}
}
public class LocalTest2 {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (double i = 0; i < 1000000000; i++) {
double test = i;
}
long finish = System.currentTimeMillis();
System.out.println("Test1 Took: " + (finish - start) + " msecs");
}
}
Compiled from "LocalTest1.java"
public class LocalTest1 {
public LocalTest1();
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: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: dconst_0
5: dstore 5
7: dload 5
9: ldc2_w #3 // double 1.0E9d
12: dcmpg
13: ifge 28
16: dload 5
18: dstore_3
19: dload 5
21: dconst_1
22: dadd
23: dstore 5
25: goto 7
28: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
31: lstore 5
33: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
36: new #6 // class java/lang/StringBuilder
39: dup
40: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
43: ldc #8 // String Test1 Took:
45: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
48: lload 5
50: lload_1
51: lsub
52: invokevirtual #10 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
55: ldc #11 // String msecs
57: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
60: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
63: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: return
}
Compiled from "LocalTest2.java"
public class LocalTest2 {
public LocalTest2();
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: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: dconst_0
5: dstore_3
6: dload_3
7: ldc2_w #3 // double 1.0E9d
10: dcmpg
11: ifge 24
14: dload_3
15: dstore 5
17: dload_3
18: dconst_1
19: dadd
20: dstore_3
21: goto 6
24: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
27: lstore_3
28: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
31: new #6 // class java/lang/StringBuilder
34: dup
35: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
38: ldc #8 // String Test1 Took:
40: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
43: lload_3
44: lload_1
45: lsub
46: invokevirtual #10 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
49: ldc #11 // String msecs
51: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: return
}
要比较所有JVM优化后的性能并不容易,但是还是有一定的可能性。更好的测试和详细结果请查看Google Caliper。
这段代码与上面的代码不完全相同。如果你只编写一个虚拟循环,JVM会跳过它,因此至少需要分配和返回一些内容。这也是Caliper文档中推荐的做法。
@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
/* Dummy variable needed to workaround smart JVM */
double dummy = 0;
/* Test loop */
for (double i = 0; i <= size; i++) {
/* Declaration and assignment */
double test = i;
/* Dummy assignment to fake JVM */
if(i == size) {
dummy = test;
}
}
return dummy;
}
/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {
/* Dummy variable needed to workaround smart JVM */
double dummy = 0;
/* Actual test variable */
double test = 0;
/* Test loop */
for (double i = 0; i <= size; i++) {
/* Assignment */
test = i;
/* Not actually needed here, but we need consistent performance results */
if(i == size) {
dummy = test;
}
}
return dummy;
}
总结:declaredBefore 表示更好的性能 -真的非常微小- 但与最小作用域原则相悖。JVM 应该为您实现此操作。
在代码中,变量的可见范围越小越好。
解决这个问题的一个方法是提供一个封装while循环的变量作用域:
{
// all tmp loop variables here ....
// ....
String str;
while(condition){
str = calculateStr();
.....
}
}
while(condition){
String str = calculateStr();
.....
}
如果您只在condition
为真时在堆栈上定义对象,则性能会更好。也就是说,仅在需要时使用它。
将字符串str
声明在while
循环外,可以让其在while
循环内外被引用。将字符串str
声明在while
循环内部,只能在该while
循环内被引用。