为什么以下工作正常?
String str;
while (condition) {
str = calculateStr();
.....
}
但据说这个是危险的/不正确的:
while (condition) {
String str = calculateStr();
.....
}
是否有必要在循环外声明变量?
答案 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
中更好地测试和详细结果
这与上面的代码不同。如果你只编写一个虚拟循环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)
答案 10 :(得分:2)
根据谷歌Android开发指南,变量范围应该是有限的。请检查此链接:
答案 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 值的变量。顺便说一下,它对类属性更强。