在循环内部或外部声明变量

时间:2012-01-10 13:01:16

标签: java optimization while-loop

为什么以下工作正常?

String str;
while (condition) {
    str = calculateStr();
    .....
}

但据说这个是危险的/不正确的:

while (condition) {
    String str = calculateStr();
    .....
}

是否有必要在循环外声明变量?

20 个答案:

答案 0 :(得分:278)

我比较了这两个(类似)例子的字节码:

让我们看看 1。示例

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

}

让我们来看看 2。示例

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规范的结果......

但是,在最佳编码实践的名义下,建议在尽可能小的范围内声明变量(在此示例中,它位于循环内部,因为这是使用变量的唯一位置)。

答案 1 :(得分:270)

局部变量的范围应始终尽可能小。

在您的示例中,我假设str while循环之外使用,否则您不会问这个问题,因为在{{1}内声明它循环不是一个选项,因为它不会编译。

因此,由于while 在循环外使用,str的最小可能范围是 while循环中。

所以,答案是强调 str绝对应该在while循环中声明。没有ifs,没有ands,没有buts。

唯一可能违反此规则的情况是,由于某种原因,必须从代码中挤出每个时钟周期至关重要,在这种情况下,您可能需要考虑在外部作用域中实例化某些内容并重新使用它不是在内部范围的每次迭代中重新实例化它。但是,由于java中字符串的不变性,这不适用于您的示例:str的新实例将始终在循环的开头创建,并且必须在其结尾处丢弃,因此存在不可能在那里进行优化。

编辑:(在答案中注明我的评论)

在任何情况下,正确的做法是正确编写所有代码,为产品建立性能要求,根据此要求测量最终产品,如果不满足,则优化。通常最终会发生的事情是,您可以找到方法在几个地方提供一些很好的和正式的算法优化,使我们的程序满足其性能要求,而不必遍及整个代码库,调整和破解为了在这里和那里挤压时钟周期。

答案 2 :(得分:23)

声明最小范围中的对象可提高可读性

性能对今天的编译器无关紧要。(在这种情况下)
从维护角度来看,第二选项更好 在尽可能最窄的范围内,在同一个地方声明和初始化变量。

正如 Donald Ervin Knuth 所说:

  

“我们应该忘记效率低,大约97%的时间说:   过早优化是所有邪恶的根源“

即程序员让性能考虑因素影响一段代码的设计的情况。这可能导致设计不干净,因为它可能是代码不正确,因为代码复杂优化,程序员被优化分散注意力。

答案 3 :(得分:12)

如果你想在外面使用str;在外面宣布它。否则,第二版很好。

答案 4 :(得分:8)

请跳到更新的答案...

对于那些关心性能的人,请取出System.out并将循环限制为1个字节。使用double(测试1/2)和使用String(3/4),下面给出了Windows 7 Professional 64位和JDK-1.7.0_21的经过时间(以毫秒为单位)。字节码(下面也给出了test1和test2)并不相同。我太懒了,无法用可变的&amp; amp;相对复杂的物体。

Test1拍摄:2710毫秒

Test2拍摄:2790毫秒

String(只需在测试中用字符串替换double)

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

中更好地测试和详细结果
  1. 博客上的一些详细信息:Should you declare a variable inside a loop or before the loop?
  2. GitHub存储库:https://github.com/gunduru/jvdt
  3. 双案例和100M循环的测试结果(以及所有JVM详细信息):https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4
  4. DeclaredBefore 1,759.209 DeclaredInside 2,242.308

    • 在1,759.209 ns之前宣布
    • DeclaredInside 2,242.308 ns

    双重声明的部分测试代码

    这与上面的代码不同。如果你只编写一个虚拟循环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实际上应该为你做这个

答案 5 :(得分:7)

在内部,变量可见的范围越小越好。

答案 6 :(得分:7)

解决这个问题的一个方法是提供一个封装while循环的变量范围:

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}

当外部范围结束时,它们将自动取消引用。

答案 7 :(得分:6)

如果您不需要在while循环(范围相关)之后使用str,那么第二个条件即

  while(condition){
        String str = calculateStr();
        .....
    }

更好,因为只有在condition为真时才在堆栈上定义对象。即如果需要,请使用

答案 8 :(得分:3)

在wile循环外声明String str允许它在&amp;内部引用在while循环之外。在while循环中声明String str允许它在while循环内引用

答案 9 :(得分:2)

变量应声明为尽可能接近它们的位置。

它使RAII (Resource Acquisition Is Initialization)更容易。

它使变量的范围保持紧张。这可以让优化器更好地工作。

答案 10 :(得分:2)

根据谷歌Android开发指南,变量范围应该是有限的。请检查此链接:

Limit Variable Scope

答案 11 :(得分:2)

正如许多人所指出的那样,

String str;
while(condition){
    str = calculateStr();
    .....
}

比这更好:

while(condition){
    String str = calculateStr();
    .....
}

因此,如果您不重复使用变量,请不要在其范围之外声明变量...

答案 12 :(得分:1)

真的,上面提到的问题是一个编程问题。您想如何编写代码?你在哪里需要访问“STR”?没有使用声明一个在本地用作全局变量的变量。我相信编程的基础知识。

答案 13 :(得分:1)

在循环内声明限制了相应变量的范围。这完全取决于项目对变量范围的要求。

答案 14 :(得分:0)

这两个例子导致了同样的事情。但是,第一个提供了在while循环之外使用str变量;第二个不是。

答案 15 :(得分:0)

str变量将可用并在内存中保留一些空间,即使在代码下面执行时也是如此。

 String str;
    while(condition){
        str = calculateStr();
        .....
    }

str变量将不可用,并且将释放在下面的代码中为str变量分配的内存。

while(condition){
    String str = calculateStr();
    .....
}

如果我们确实遵循第二个,这将减少我们的系统内存并提高性能。

答案 16 :(得分:0)

我认为对象的大小也很重要。 在我的一个项目中,我们声明并初始化了一个大型二维数组,该数组使应用程序抛出了内存异常。 我们将声明移出循环,并在每次迭代开始时清除数组。

答案 17 :(得分:0)

这并不能真正回答您的问题(“为什么不起作用?”),但是我想不同意@ mike-nakis的观点,并说块范围界定是个坏主意。

从哲学上讲,for / while循环是所编写函数的一部分,并且不应具有自己的不同变量。函数和类显然是彼此分开的,因此函数和类的作用域是个好主意,而块作用域不是。块范围定义是Java的异常,尽管其他一些语言也可以做到。

一个不实际的原因是它不便携。 JavaScript没有块作用域,其他语言也可能没有。引用https://www.oreilly.com/library/view/javascript-the-definitive/0596000480/ch04s03.html

无阻止范围

请注意,与C,C ++和Java不同,JavaScript没有块级范围。在函数中声明的所有变量,无论在何处声明,都在整个函数中定义。在下面的代码中,变量i,j和k都具有相同的作用域:这三个变量都是在函数主体中定义的。如果代码是用C,C ++或Java编写的,则情况并非如此:

由于您无法从本地作用域中获得任何收益,并且由于它不具有可移植性,因此最好完全避免使用它。

通常,您的子例程应该足够简短,以使查找变量的声明位置变得容易。如果确实需要在函数内使用单独的作用域,则最好调用另一个函数。

答案 18 :(得分:-1)

警告这个问题中的几乎所有人:这里是示例代码,在循环内部,Java 7在我的计算机上很容易慢200倍(并且内存消耗也略有不同)。但这是关于分配而不仅仅是范围。

public class Test
{
    private final static int STUFF_SIZE = 512;
    private final static long LOOP = 10000000l;

    private static class Foo
    {
        private long[] bigStuff = new long[STUFF_SIZE];

        public Foo(long value)
        {
            setValue(value);
        }

        public void setValue(long value)
        {
            // Putting value in a random place.
            bigStuff[(int) (value % STUFF_SIZE)] = value;
        }

        public long getValue()
        {
            // Retrieving whatever value.
            return bigStuff[STUFF_SIZE / 2];
        }
    }

    public static long test1()
    {
        long total = 0;

        for (long i = 0; i < LOOP; i++)
        {
            Foo foo = new Foo(i);
            total += foo.getValue();
        }

        return total;
    }

    public static long test2()
    {
        long total = 0;

        Foo foo = new Foo(0);
        for (long i = 0; i < LOOP; i++)
        {
            foo.setValue(i);
            total += foo.getValue();
        }

        return total;
    }

    public static void main(String[] args)
    {
        long start;

        start = System.currentTimeMillis();
        test1();
        System.out.println(System.currentTimeMillis() - start);

        start = System.currentTimeMillis();
        test2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

结论:根据局部变量的大小,差异可能很大,即使变量不是很大。

只是说有时候,循环外部或内部都很重要。

答案 19 :(得分:-2)

如果您的NullPointerException方法返回 null ,然后您尝试在str上调用方法,则存在calculateStr()的风险。

更一般地说,避免使用具有 null 值的变量。顺便说一下,它对类属性更强。