提问人:Harry Joy 提问时间:1/10/2012 最后编辑:BSMPHarry Joy 更新时间:10/8/2020 访问量:187884
在循环内部或外部声明变量
Declaring variables inside or outside of a loop
问:
为什么以下工作正常?
String str;
while (condition) {
str = calculateStr();
.....
}
但据说这个是危险的/不正确的:
while (condition) {
String str = calculateStr();
.....
}
是否有必要在循环外声明变量?
答:
在循环外部声明 String 允许在循环内部和外部引用它。在循环中声明 String 只允许在该循环中引用它。str
while
while
str
while
while
在内部,变量可见的范围越小越好。
如果您也想在 looop 之外使用;在外面宣布。否则,第二个版本就可以了。str
如果您不需要使用 after the while 循环(与范围相关),则第二个条件即str
while(condition){
String str = calculateStr();
.....
}
更好,因为如果您仅在 为 true 时才在堆栈上定义对象。即,如果需要,请使用它condition
评论
局部变量的范围应始终尽可能小。
在您的示例中,我假设不会在循环外部使用,否则您不会提出问题,因为在循环中声明它不是一个选项,因为它不会编译。str
while
while
因此,由于不在循环之外使用,因此 while 循环内的最小可能范围。str
str
因此,答案是绝对应该在 while 循环中声明。没有如果,没有和,没有但是。str
唯一可能违反此规则的情况是,如果出于某种原因,必须从代码中挤出每个时钟周期至关重要,在这种情况下,您可能需要考虑在外部作用域中实例化某些内容并重用它,而不是在内部作用域的每次迭代中重新实例化它。但是,由于 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 规范的结果......
但以最佳编码实践的名义,建议在尽可能小的范围内声明变量(在本例中,它位于循环内部,因为这是使用变量的唯一位置)。
评论
final
str
final
inside
确实,上面提到的问题是一个编程问题。您希望如何对代码进行编程?您需要在哪里访问“STR”?声明在本地用作全局变量的变量是没有用的。我相信编程的基础知识。
在最小范围内声明对象可提高可读性。
对于当今的编译器来说,性能并不重要。(在此方案中)
从维护的角度来看,第二种选择更好。
在尽可能窄的范围内,在同一个位置声明和初始化变量。
正如唐纳德·欧文·克努斯(Donald Ervin Knuth)所说:
“我们应该忘记小效率,比如说大约 97% 的时间: 过早优化是万恶之源”
即)程序员让性能考虑因素影响一段代码设计的情况。这可能会导致设计不如预期干净或代码不正确,因为优化使代码变得复杂,程序员因优化而分心。
评论
这两个例子的结果是同一件事。但是,第一个为您提供了在 while 循环之外使用变量的功能;第二个不是。str
变量应尽可能靠近它们的使用位置进行声明。
它使 RAII(资源获取即初始化)更容易。
它使变量的范围保持紧密。这样可以使优化器更好地工作。
正如许多人所指出的,
String str;
while(condition){
str = calculateStr();
.....
}
没有比这更好的了:
while(condition){
String str = calculateStr();
.....
}
因此,如果您不重用变量,请不要声明其范围之外的变量......
评论
根据 Google Android 开发指南,变量范围应该受到限制。请查看此链接:
如果方法返回 null,然后尝试在 str 上调用方法,则存在以下风险。NullPointerException
calculateStr()
更一般地说,避免使用具有 null 值的变量。顺便说一句,它对职业属性更强。
评论
NullPointerException.
return str;
请跳至更新后的答案...
对于那些关心性能的人,请取出 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 中提供更好的测试和详细结果
- 博客上的一些细节:你应该在循环中还是在循环之前声明一个变量?
- GitHub 存储库:https://github.com/gunduru/jvdt
- 双写和 100M 循环(是的,所有 JVM 详细信息)的测试结果:https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4
- 声明在 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 实际上应该为您做到这一点
评论
在这个问题中警告几乎所有人:这是示例代码,在循环中,在我的 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);
}
}
结论:根据局部变量的大小,即使变量不那么大,差异也可能很大。
只是说有时,循环之外或内部确实很重要。
评论
bigStuff[(int) (value % STUFF_SIZE)] = value;
即使在代码下方执行后,该变量仍将可用并在内存中保留一些空间。str
String str;
while(condition){
str = calculateStr();
.....
}
该变量将不可用,并且将释放在下面代码中为变量分配的内存。str
str
while(condition){
String str = calculateStr();
.....
}
如果我们遵循第二个,这肯定会减少我们的系统内存并提高性能。
我认为回答您的问题的最佳资源是以下帖子:
根据我的理解,这件事将取决于语言。IIRC Java 对此进行了优化,因此没有任何区别,但 JavaScript(例如)每次都会在循环中完成整个内存分配。特别是在 Java 中,我认为第二个在完成分析后会运行得更快。
这个问题的一个解决方案可能是提供一个封装 while 循环的变量作用域:
{
// all tmp loop variables here ....
// ....
String str;
while(condition){
str = calculateStr();
.....
}
}
当外部范围结束时,它们将自动取消引用。
我认为物体的大小也很重要。 在我的一个项目中,我们声明并初始化了一个大型二维数组,该数组使应用程序抛出内存不足异常。 我们将声明移出循环,并在每次迭代开始时清除数组。
下一个:什么是差一错误,如何解决?
评论