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


236

为什么以下工作正常?

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

但这是危险的/不正确的:

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

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

Answers:


289

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

在您的例子我相信str不会使用的外while循环,否则你就不会问这个问题,因为它声明的内部while循环不会是一个选项,因为它不会编译。

所以,既然str使用外循环,在尽可能小的范围str while循环。

所以,答案是着重str绝对应该被while循环内声明。不,如果,不,不,但。

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

编辑:(将我的评论插入下面的答案中)

在任何情况下,正确的处理方式是正确编写所有代码,为产品建立性能要求,根据该要求评估最终产品,如果不满足要求,则进行优化。通常最终会发生的事情是,您找到了在几个地方提供一些不错的正式算法优化的方法,从而使我们的程序能够满足其性能要求,而不必遍历整个代码库并对其进行调整和修改。为了在这里和那里挤压时钟周期。


2
关于最后一段的查询:如果不是另一个,则String是不可改变的,那么会影响吗?
哈里·乔伊

1
@HarryJoy是的,当然,以可变的StringBuilder为例。如果使用StringBuilder在循环的每次迭代中构建新的字符串,则可以通过在循环外部分配StringBuilder来优化性能。但是,仍然不建议这样做。如果您没有充分的理由这样做,那就是过早的优化。
Mike Nakis 2012年

7
@HarryJoy做事情的正确方法是正确编写所有代码,为产品建立性能要求,根据此要求评估最终产品,如果不满足要求,则进行优化。你知道吗?通常,您将能够在几个地方提供一些不错的,形式化的算法优化,这将达到目的,而不必遍历整个代码库并进行调整和修改,以便在此处和在此挤压时钟周期。
Mike Nakis 2012年

2
@MikeNakis,我想您是在狭窄范围内考虑。
Siten 2012年

5
您会看到,现代的多千兆赫,多核,流水线,多级内存缓存CPU使我们能够专注于遵循最佳实践,而不必担心时钟周期。此外,仅当且仅当已确定必要时才建议进行优化,并且在必要时,通常需要进行几次高度本地化的调整,以实现所需的性能,因此无需乱扔所有的代码以性能为名的小技巧。
Mike Nakis 2012年

293

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

让我们看一下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.javajavap -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. example

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

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


3
这是JVM Soecification的结果,而不是“编译器优化”的结果。方法所需的堆栈插槽均在该方法的入口处分配。这就是指定字节码的方式。
罗恩侯爵

2
@Arhimed还有另一个理由将其放入循环(或只是'{}'块)内:如果在另一个作用域中声明了一些over变量,则编译器将为另一个作用域的变量重用在堆栈框架中分配的内存。 。
Serge 2012年

1
如果它通过数据对象列表循环,那么它将对大量数据有什么影响?大概四万
Mithun Khatri 2014年

7
对于任何你的final恋人:宣布strfinalinside包装的情况下没有什么区别=)
skia.heliou

27

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

对于今天的编译器而言,性能并不重要。(在这种情况下)
从维护角度来看,第二个选项更好。
在尽可能小的范围内,在同一位置声明和初始化变量。

正如Donald Ervin Knuth所说:

“我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源”

即,程序员出于性能考虑而影响一段代码设计的情况。这可能会导致一个设计,是不是干净,因为它可能已经代码是不正确的,因为代码是复杂通过优化和程序员分心优化


1
“第二个选项的性能稍快” =>您测量过吗?根据答案之一,字节码是相同的,所以我看不出性能有何不同。
assylias 2012年

很抱歉,但这实际上不是测试Java程序性能的正确方法(无论如何您如何测试无限循环的性能?)
assylias 2012年

我同意您的其他观点-只是我相信没有任何性能差异。
assylias 2012年

11

如果您还想使用str外圈;在外面声明。否则,第二版就可以了。


11

请跳至更新的答案...

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

测试1耗时:2710毫秒

测试2耗时:2790毫秒

字符串(在测试中只需用字符串替换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. 博客上的一些详细信息:应该在循环内还是循环前声明变量?
  2. GitHub存储库:https : //github.com/gunduru/jvdt
  3. 双重大小写和100M循环的测试结果(并且是所有JVM详细信息):https : //microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

在1,759.209之前宣布在内部2,242.308宣布

  • 在1,759.209 ns之前声明
  • 已宣布内部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;
}

摘要:clarifiedBefore表示性能更好-确实很小-并且违反最小范围原则。JVM实际上应该为您执行此操作


无效的测试方法,并且您不提供任何结果解释。
罗恩侯爵

1
@EJP对于对此主题感兴趣的人,这应该很清楚。方法学摘自PrimosK的答案,以提供更多有用的信息。老实说,我不知道如何改善此答案,也许您可​​以单击“编辑”并向我们展示如何正确执行此操作?
OnurGünduru14年

2
1)Java字节码在运行时进行了优化(重新排序,折叠等),因此不必过多担心在.class文件中写入的内容。2)有1.000.000.000次运行获得2.8s的性能胜利,因此相对于安全适当的编程风格,每次运行大约2.8ns。对我来说显然是赢家。3)由于您未提供任何有关预热的信息,因此您的时间安排毫无用处。
2015年

@仅使用卡尺对双循环和100M循环进行硬编码更好的测试/微基准测试。在线结果,如果您希望其他情况可以随时进行编辑。
OnurGünduru2015年

谢谢,这消除了点1)和3)。但是,即使每个周期的时间增加到〜5ns,这仍然是一个可以忽略的时间。从理论上讲,优化的潜力很小,实际上,每个周期执行的工作通常会贵很多。因此,在几分钟甚至几小时的运行中,最多可能需要几秒钟的时间。在花时间进行此类低级优化之前,我会检查其他具有较高潜力的选项(例如,Fork / Join,并行流)。
硬编码

7

解决此问题的一种方法是提供一个封装了while循环的可变范围:

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

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



5

如果您不需要使用strwhile循环之后(与范围有关),则第二个条件即

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

更好,因为只有在conditiontrue 时才在堆栈上定义一个对象。即在需要时使用它


2
请注意,即使在第一个变量中,如果条件为false,也不会构造任何对象。
菲利普·温德勒

// @菲利普:是的,你是对的。我的错。我在想现在的样子。您怎么看?
Cratylus 2012年

1
在Java世界中,“在堆栈上定义一个对象”是一个有点怪异的名词。另外,在运行时在堆栈上分配变量通常是noop,那么为什么要麻烦呢?界定范围以帮助程序员是真正的问题。
菲利普·温德勒

3

我认为,回答您问题的最佳资源是以下帖子:

在循环之前或循环中声明变量之间的区别?

根据我的理解,这件事将取决于语言。IIRC Java对此进行了优化,因此没有什么区别,但是JavaScript(例如)将在循环中每次都进行整个内存分配。特别是在Java中,我认为第二个在完成性能分析时会运行得更快。


3

正如许多人指出的那样,

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

不是比这更好:

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

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


1
除了可能以这种方式:链接
Dainius Kreivys 2014年

2

在wile循环之外声明String str允许在while循环内外引用它。在while循环内部声明String str 只能在while循环内部引用它。




1

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

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

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

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

如果我们能肯定地遵循第二篇,将会减少我们的系统内存并提高性能。


0

在循环内部声明会限制各个变量的范围。这完全取决于项目对变量范围的要求。



-1

这两个示例导致相同的结果。但是,第一个为您提供str了在while循环之外使用变量的方法;第二个不是。


-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);
    }
}

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

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


1
当然,第二个更快,但是您在做不同的事情:test1创建了很多带有大数组的Foo对象,而test2不是。test2反复使用同一Foo对象,这在多线程环境中可能很危险。
2015年

在多线程环境中存在危险???请解释原因。我们在谈论局部变量。它是在方法的每次调用时创建的。
rt15 2015年

如果将Foo对象传递给异步处理数据的操作,则在更改其中的数据时,该操作可能仍在Foo实例上运行。它甚至不必多线程就可以产生副作用。因此,当您不知道谁仍在使用实例时,实例重用非常危险
硬编码

附注:您的setValue方法应为bigStuff[(int) (value % STUFF_SIZE)] = value;(尝试输入2147483649L的值)
硬编码

谈到副作用:您是否比较了方法的结果?
硬编码

-1

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


-2

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

更普遍地,避免使用具有值的变量。顺便说一句,它对于类属性更强。


2
这与问题无关。NullPointerException(在将来的函数调用中)的可能性将不取决于变量的声明方式。
沙漠冰

1
我不这么认为,因为问题是“最好的方法是什么?”。恕我直言,我希望使用更安全的代码。
雷米Doolaeghe

1
NullPointerException.如果尝试使用此代码,return str;则编译错误为零。
罗恩侯爵
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.