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

为什么下面的工作正常?

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

但是下面这个被认为是危险的/不正确的:

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

有必要在循环之外声明变量吗?

180266 次浏览

while循环之外声明字符串str允许它在&在while循环之外。在while循环中声明String str允许它在while循环中被引用。

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

如果你也想在循环外使用str;在外面宣布。否则,第二版就可以了。

如果你不需要在while循环之后使用str(与作用域相关),那么第二个条件即。

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

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

局部变量的作用域应该总是尽可能的小。

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

因此,由于str是在循环之外使用的,因此str的最小可能作用域是 while循环。

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

唯一可能违反此规则的情况是,由于某种原因,每个时钟周期都必须从代码中挤出,这种情况非常重要,在这种情况下,您可能希望考虑在外部作用域中实例化一些东西并重用它,而不是在内部作用域的每次迭代中重新实例化它。然而,这并不适用于你的例子,由于java中字符串的不可变性:一个新的str实例总是在循环开始时创建,它将不得不在循环结束时被丢弃,所以没有优化的可能性。

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

在任何情况下,正确的做法都是正确地编写所有代码,为产品建立性能需求,根据该需求衡量最终产品,如果它不能满足需求,那么就进行优化。最后通常会发生的是,你会找到方法,在几个地方提供一些不错的、正式的算法优化,使我们的程序满足性能要求,而不是不得不遍历你的整个代码库,调整和修改东西,以便在这里和那里挤压时钟周期。

在循环中声明将限制各自变量的范围。这完全取决于项目对变量范围的要求。

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

让我们看看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规范的结果……

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

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

最小的范围中声明对象改进了可读性.
. properties

性能对于今天的编译器来说并不重要。(本场景中)
从维护的角度来看,2日选项更好 在同一个地方声明和初始化变量,在尽可能窄的范围内

正如唐纳德·欧文·克努斯所说:

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

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

这两个例子的结果是一样的。但是,第一个方法提供了在while循环之外使用str变量的方法;第二个则不然。

变量的声明应该尽可能靠近使用它们的地方。

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

它使变量的范围保持紧凑。这让优化器工作得更好。

正如很多人指出的那样,

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

比这个更好:

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

因此,如果你不重用变量,就不要在变量作用域之外声明变量。

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

限制变量范围

如果你的calculateStr()方法返回,然后你试图在str上调用一个方法,你就有NullPointerException的风险。

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

请跳到最新的答案…

对于那些关心绩效的人,可以去掉系统。输出并将循环限制为1字节。使用double (test 1/2)和使用String(3/4),以毫秒为单位的运行时间如下所示,使用Windows 7 Professional 64位和JDK-1.7.0_21。字节码(下面也给出了test1和test2的字节码)是不同的。我懒得用mutable &相对复杂的对象。

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优化的性能确实不容易。然而,这在某种程度上是可能的。谷歌卡尺中更好的测试和详细的结果

  1. 一些细节在博客:应该在循环内部还是循环之前声明变量?
  2. GitHub仓库:https://github.com/gunduru/jvdt
  3. 双case和100M循环的测试结果(是所有JVM细节):https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

DeclaredBefore 1,759.209 DeclaredInside 2,242.308

  • 声明前1759.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;
}

总结:declardbefore表示更好的性能——非常小——它违背了最小作用域原则。JVM实际上应该为您做这件事

对这个问题中几乎所有人的警告:这里是示例代码,在循环中,它可以很容易地在我的计算机上用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);
}
}

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

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

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

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

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

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

如果我们采用第二种方法,肯定会减少系统内存,提高性能。

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

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

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

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