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

Declaring variables inside or outside of a loop

提问人:Harry Joy 提问时间:1/10/2012 最后编辑:BSMPHarry Joy 更新时间:10/8/2020 访问量:187884

问:

为什么以下工作正常?

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

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

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

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

Java 优化 while-loop

评论

1赞 user202729 10/17/2022
这回答了你的问题吗?在循环之前或循环中声明变量之间的区别?

答:

3赞 Jay Tomten 1/10/2012 #1

在循环外部声明 String 允许在循环内部和外部引用它。在循环中声明 String 只允许在该循环中引用它strwhilewhilestrwhilewhile

8赞 Jan Zyka 1/10/2012 #2

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

14赞 Azodious 1/10/2012 #3

如果您也想在 looop 之外使用;在外面宣布。否则,第二个版本就可以了。str

7赞 Cratylus 1/10/2012 #4

如果您不需要使用 after the while 循环(与范围相关),则第二个条件即str

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

更好,因为如果您仅在 为 true 时才在堆栈上定义对象。即,如果需要,请使用它condition

评论

3赞 Philipp Wendler 1/10/2012
请注意,即使在第一个变体中,如果条件为 false,也不会构造任何对象。
0赞 Cratylus 1/10/2012
@菲利普:是的,你是对的。我的错。我当时的想法就像现在一样。你觉得怎么样?
1赞 Philipp Wendler 1/10/2012
好吧,“在堆栈上定义一个对象”在 Java 世界中是一个有点奇怪的术语。此外,在堆栈上分配变量在运行时通常是一个 noop,那么为什么要麻烦呢?帮助程序员的范围界定是真正的问题。
330赞 Mike Nakis 1/10/2012 #5

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

在您的示例中,我假设不会在循环外部使用,否则您不会提出问题,因为在循环中声明它不是一个选项,因为它不会编译。strwhilewhile

因此,由于不在循环之外使用,因此 while 循环的最小可能范围。strstr

因此,答案是绝对应该在 while 循环中声明。没有如果,没有和,没有但是。str

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

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

无论如何,正确的做事方法是正确地编写所有代码,为您的产品建立性能要求,根据此要求衡量您的最终产品,如果它不满足它,那么就去优化它。通常最终发生的事情是,你找到方法在几个地方提供一些漂亮和正式的算法优化,使我们的程序满足其性能要求,而不必遍历整个代码库,调整和破解一些东西,以挤压时钟周期。

评论

2赞 Harry Joy 1/11/2012
对最后一段的查询:如果它是另一个,那么 String 不是不可变的,那么它会影响吗?
2赞 Mike Nakis 1/11/2012
@HarryJoy 是的,当然,以 StringBuilder 为例,它是可变的。如果使用 StringBuilder 在循环的每次迭代中构建一个新字符串,则可以通过在循环外部分配 StringBuilder 来优化内容。但是,这仍然不是一种可取的做法。如果你没有很好的理由就这样做,这是一个不成熟的优化。
7赞 Mike Nakis 1/11/2012
@HarryJoy 正确的做事方法是正确地编写所有代码,为产品建立性能要求,根据此要求衡量最终产品,如果它不满足它,那么就去优化它。你知道吗?你通常能够在几个地方提供一些漂亮和正式的算法优化,这将解决问题,而不必遍历整个代码库,调整和破解一些东西,以便在这里和那里挤压时钟周期。
2赞 Siten 1/11/2012
@MikeNakis我认为你在非常狭窄的范围内思考。
6赞 Mike Nakis 1/16/2012
你看,现代的多千兆赫兹、多核、流水线、多级内存缓存 CPU 使我们能够专注于遵循最佳实践,而不必担心时钟周期。此外,只有当且仅当确定有必要时,才建议进行优化,并且在必要时,一些高度本地化的调整通常会达到所需的性能,因此没有必要以性能的名义在所有的代码中乱扔小技巧。
1赞 ab02 1/10/2012 #6

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

324赞 PrimosK 1/16/2012 #7

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

让我们看一下 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. 示例

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

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

评论

7赞 user207421 4/23/2012
它是 JVM Soecification 的结果,而不是“编译器优化”。方法所需的堆栈槽都在进入该方法时分配。这就是指定字节码的方式。
2赞 Serge 9/27/2012
@Arhimed还有一个原因将其放在循环(或只是“{}”块)中:如果您在另一个作用域中声明一些变量,编译器将重用堆栈帧中为另一个作用域中的变量分配的内存。
1赞 Mithun Khatri 12/5/2014
如果它通过数据对象列表循环,那么它对大量数据有什么影响吗?大概是40数千。
9赞 nikodaemus 1/22/2015
对于你们中的任何一个爱好者:在包装盒中声明也没有区别=)finalstrfinalinside
1赞 Abhishek Bhandari 1/20/2012 #8

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

34赞 Chandra Sekhar 1/22/2012 #9

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

对于当今的编译器来说,性能并不重要。(在此方案中)
从维护的角度来看,第二种选择更好。
在尽可能窄的范围内,在同一个位置声明和初始化变量。

正如唐纳德·欧文·克努斯(Donald Ervin Knuth)所说:

“我们应该忘记小效率,比如说大约 97% 的时间: 过早优化是万恶之源”

即)程序员让性能考虑因素影响一段代码设计的情况。这可能会导致设计不如预期干净代码不正确,因为优化使代码变得复杂,程序员因优化而分心。

评论

1赞 assylias 9/27/2012
“第二个选项的性能稍快”=>你测量过吗?根据其中一个答案,字节码是相同的,所以我看不出性能会有什么不同。
0赞 assylias 9/27/2012
对不起,这真的不是测试 java 程序性能的正确方法(无论如何,你怎么能测试无限循环的性能?
0赞 assylias 9/27/2012
我同意你的其他观点——只是我相信没有性能差异。
0赞 olyanren 1/23/2012 #10

这两个例子的结果是同一件事。但是,第一个为您提供了在 while 循环之外使用变量的功能;第二个不是。str

3赞 vikiiii 1/23/2012 #11

变量应尽可能靠近它们的使用位置进行声明。

它使 RAII(资源获取即初始化)更容易。

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

2赞 Pavan 1/23/2012 #12

正如许多人所指出的,

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

没有比这更好的了:

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

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

评论

1赞 Dainius Kreivys 3/7/2014
除了可能以这种方式:链接
3赞 James Jithin 1/23/2012 #13

根据 Google Android 开发指南,变量范围应该受到限制。请查看此链接:

限制变量范围

-2赞 Rémi Doolaeghe 11/22/2012 #14

如果方法返回 null,然后尝试在 str 上调用方法,则存在以下风险。NullPointerExceptioncalculateStr()

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

评论

2赞 Desert Ice 11/22/2012
这与问题无关。NullPointerException(在将来的函数调用中)的概率不取决于变量的声明方式。
1赞 Rémi Doolaeghe 11/23/2012
我不这么认为,因为问题是“最好的方法是什么?恕我直言,我更喜欢更安全的代码。
1赞 user207421 7/1/2014
如果尝试此代码,则会遇到编译错误的风险为零。NullPointerException.return str;
13赞 Onur Günduru 6/28/2013 #15

请跳至更新后的答案...

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

Test1 花费:2710 毫秒

Test2 花费: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

DeclaredBefore 1,759.209 DeclaredInside 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;
}

总结:declaredBefore 表示更好的性能 - 非常小 - 并且它违反了最小范围原则。JVM 实际上应该为您做到这一点

评论

0赞 user207421 7/1/2014
测试方法无效,并且您没有提供任何结果解释。
1赞 Onur Günduru 7/5/2014
@EJP 对于那些对这个主题感兴趣的人来说,这应该很清楚。方法取自 PrimosK 的回答,以提供更多有用的信息。老实说,我不知道如何改进这个答案,也许您可以单击编辑并向我们展示如何正确操作?
2赞 Hardcoded 1/3/2015
1)Java字节码在运行时得到优化(重新排序,折叠等),所以不要太在意.class文件中写入的内容。2) 有 1.000.000.000 次运行才能获得 2.8 秒的性能胜利,因此与安全和适当的编程风格相比,每次运行大约需要 2.8ns。对我来说,这显然是赢家。3)由于您没有提供有关热身的信息,因此您的时间安排毫无用处。
0赞 Onur Günduru 1/6/2015
@Hardcoded更好的测试/微基准测试,仅针对双环和 100M 环路使用卡尺。在线结果,如果您想要其他案例,请随时编辑。
0赞 Hardcoded 1/8/2015
谢谢,这消除了第 1) 点和 3)。但即使时间上升到每个周期~5ns,这仍然是一个不容忽视的时间。从理论上讲,优化潜力很小,但实际上,您每个周期所做的事情通常要昂贵得多。因此,在几分钟甚至几小时的运行中,最多只有几秒钟的可能性。还有其他具有更高潜力的选项(例如分叉/连接、并行流),我会在花时间进行这种低级优化之前检查这些选项。
-1赞 rt15 11/21/2014 #16

在这个问题中警告几乎所有人:这是示例代码,在循环中,在我的 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);
    }
}

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

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

评论

2赞 Hardcoded 1/3/2015
当然,第二个更快,但你正在做不同的事情:test1 正在创建大量带有大数组的 Foo-Object,而 test2 则不是。test2 一遍又一遍地重用同一个 Foo 对象,这在多线程环境中可能很危险。
0赞 rt15 1/5/2015
在多线程环境中很危险???请解释原因。我们谈论的是一个局部变量。它是在每次调用该方法时创建的。
0赞 Hardcoded 1/8/2015
如果将 Foo-Object 传递给异步处理数据的操作,则当您更改 Foo 实例中的数据时,该操作可能仍在 Foo 实例上工作。它甚至不必是多线程的,就会有副作用。因此,当您不知道谁仍在使用实例时,实例重用是非常危险的
0赞 Hardcoded 1/8/2015
Ps:你的setValue方法应该是(尝试值2147483649L)bigStuff[(int) (value % STUFF_SIZE)] = value;
0赞 Hardcoded 1/8/2015
说到副作用:你有没有比较过你的方法的结果?
0赞 Ganesa Vijayakumar 5/7/2015 #17

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

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

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

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

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

4赞 Naveen Goyal 9/10/2015 #18

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

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

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

8赞 Morten Madsen 9/18/2015 #19

这个问题的一个解决方案可能是提供一个封装 while 循环的变量作用域:

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

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

0赞 Sanjit 8/9/2017 #20

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