原文出处:http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html
作者写的比较磨叽,我也不是非常专业的翻译,有不妥之处,请指正。
这个问题已经被很多人问了很多遍了。到底是在循环内部还是外部进行变量声明比较好呢?针对这个问题,我最近找了很多有关这方面的资料。我发现大多数资料都给出了一大堆的讨论,但是很少能够真正的支持某一种方案。我觉得把变量声明放在循环外部既可以增加代码的可读性,也有利于提高算法的性能及内存的使用效率。
概述
可读性的问题似乎更像是一个宗教性问题而对性能影响不大。而关于内存和性能的问题则是人们长久以来一直关心的非常明确的问题。我(作者)试图解决这两个问题,并使用javap工具从字节码的层次进行说明。我不是一个字节码专家,但是我会尽量准确地进行解释说明。如果你在其中发现了什么错误,请告诉我。
基本数据类型
对于这个示例,我采用了下面的方法:
public void testPrimitives1(int count) {
int a = 0;
for (int i = 0; i < count; i++) {
a = i;
}
}
public void testPrimitives2(int count) {
int a;
for (int i = 0; i < count; i++) {
a = i;
}
}
public void testPrimitives3(int count) {
for (int i = 0; i < count; i++) {
int a = i;
}
}
显然,这个例子很简单。这是因为我不想被陷在复杂字节码之中(“I did not want to get caught up in a lot of code in the byte codeup”。the byte codeup我不知道怎么翻译好)。如你所见,我使用了两个在循环外部进行变量声明的方法,在我们解读字节码的时候你就会明白这么做的原因了。
public void testPrimitives1(int count) {
int a = 0;
for (int i = 0; i < count; i++) {
a = i;
}
}
public void testPrimitives2(int count) {
int a;
for (int i = 0; i < count; i++) {
a = i;
}
}
public void testPrimitives3(int count) {
for (int i = 0; i < count; i++) {
int a = i;
}
}
显然,这个例子很简单。这是因为我不想被陷在复杂字节码之中(“I did not want to get caught up in a lot of code in the byte codeup”。the byte codeup我不知道怎么翻译好)。如你所见,我使用了两个在循环外部进行变量声明的方法,在我们解读字节码的时候你就会明白这么做的原因了。
public void testPrimitives1(int);
Code:
0: iconst_0
1: istore_2
2: iconst_0
3: istore_3
4: iload_3
5: iload_1
6: if_icmpge 17
9: iload_3
10: istore_2
11: iinc 3, 1
14: goto 4
17: return
Code:
0: iconst_0
1: istore_2
2: iconst_0
3: istore_3
4: iload_3
5: iload_1
6: if_icmpge 17
9: iload_3
10: istore_2
11: iinc 3, 1
14: goto 4
17: return
通过观察如上所示的第一个方法对应的字节码可以发现,头两行是iconst_0和istore_2。这两行对应着循环变量的声明和初始化。在其他的两个方法中你也会看到这两行。
紧接着的两行是iconst_0和istore_3。这对应着int a=0这个语句中变量a的声明和初始化。
接下来的三行用来装载计数参数以及计数参数与循环变量的比较。
再接下来的两行是循环变量的装载并将它存储到一个变量中。
再接下来的一行会递增循环变量
最后,是实现循环的goto指令以及从方法中返回。
可以看到这个例子并没有过多的内容,我们可以列出都发生了什么:
-
循环变量的声明和初始化。
-
基本数据类型变量的声明和初始化
-
装载输入参数和循环变量,并将两者进行比较。
-
装载循环变量并将其存储到基本数据类型的变量。
-
递增循环变量。
-
返回到循环的起始处
我们再来看看下一个例子。
public void testPrimitives2(int);
Code:
0: iconst_0
1: istore_3
2: iload_3
3: iload_1
4: if_icmpge 15
7: iload_3
8: istore_2
9: iinc 3, 1
12: goto 2
15: return
在这个例子中,我们可以看到:
-
循环变量的声明和初始化。
-
装载输入参数和循环变量,并将两者进行比较。
-
装载循环变量并将其存储到基本数据类型的变量。
-
递增循环变量。
-
返回到循环的起始处。
这个例子与前一个基本相同,唯一的差别是存储位置的变化。这是因为在前一个例子中并没有对变量进行初始化,所以并没有产生istore_<x>指令。存储位置的变化是因为在两个例子中采用了不同的声明顺序。
基本数据类型的结论
性能和内存
尽管第二个方法多出来一个变量声明,但是由于没有进行初始化,所以这两个方法在功能上是一致的。将这两个方法与第一个方法进行比较,可以看出,差别仅仅存在于对基本类型的初始化操作。所以,可以说,这些方法本质上都是一样的,只不过第一个方法有对基本数据类型的初始化操作。所以,如果你喜欢像我一样将循环变量声明在循环外部的话,你可以不再担心这样做会对性能造成什么影响。如果你既想获得最佳的性能,又想将变量在循环外部进行声明,那么就不要将变量进行初始化就行了。我不喜欢使用未经初始化的变量,所以我宁愿损失那么一丁点性能也要在循环外部进行变量声明。
可读性
因为我喜欢在没有查看循环内部的时候就知道在循环中都使用了哪些变量,所以我总是喜欢在循环外部进行变量声明。对于那些在循环内部进行变量声明的论调我表示理解,但是我并不接受。我所能做的只是在修改已有代码时,不改变变量声明的风格而已。不过,在我写代码的时候,我会将变量声明放在循环外部。
对象类型
对于对象的测试,我在下面的方法中使用了StringBuffer:
public void testObjects1(int count) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.setLength(0);
sb.append(i);
}
}
public void testObjects2(int count) {
StringBuffer sb;
for (int i = 0; i < count; i++) {
sb = new StringBuffer();
sb.append(i);
}
}
public void testObjects3(int count) {
for (int i = 0; i < count; i++) {
StringBuffer sb = new StringBuffer();
sb.append(i);
}
}
和基本数据类型的实验一样,这些测试方法也很简单。StringBuffer的setLength(0)方法是将字符串长度设为0。之所以在这些例子中使用StringBuffer是因为它具有setLength()这种“重置”方法,使得用户可以重用对象实例。
public void testObjects1(int count) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.setLength(0);
sb.append(i);
}
}
public void testObjects2(int count) {
StringBuffer sb;
for (int i = 0; i < count; i++) {
sb = new StringBuffer();
sb.append(i);
}
}
public void testObjects3(int count) {
for (int i = 0; i < count; i++) {
StringBuffer sb = new StringBuffer();
sb.append(i);
}
}
和基本数据类型的实验一样,这些测试方法也很简单。StringBuffer的setLength(0)方法是将字符串长度设为0。之所以在这些例子中使用StringBuffer是因为它具有setLength()这种“重置”方法,使得用户可以重用对象实例。
下面展示了对应的字节码:
public void testObjects1(int);
Code:
0: new #2; //class java/lang/StringBuffer
3: dup
4: invokespecial #3; //Method java/lang/StringBuffer."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: iload_3
11: iload_1
12: if_icmpge 32
15: aload_2
16: iconst_0
17: invokevirtual #4; //Method java/lang/StringBuffer.setLength:(I)V
20: aload_2
21: iload_3
22: invokevirtual #5; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
25: pop
26: iinc 3, 1
29: goto 10
32: return
头4行是对StringBuffer的声明和初始化。
下面2行是对循环变量的声明和初始化。
再下面3行是对循环变量及参数的装载以及两者的比较。
接下来的3行是对字符数组的重置,然后对字符数组和循环变量进行装载,接着对循环变量存储到StringBuffer变量中,append方法进行出栈操作。
最后,对循环变量进行递增并返回到循环顶部。
将方法的过程进行总结如下:
-
声明并初始化StringBuffer
-
声明并初始化循环变量
-
装载输入参数和循环变量并将两者进行比较
-
重置StringBuffer中的字符数组
-
装载字符数组和循环变量
-
将循环变量存入StringBuffer并调用出栈指令。
-
递增循环变量
-
返回到循环起始
public void testObjects2(int);
Code:
0: iconst_0
1: istore_3
2: iload_3
3: iload_1
4: if_icmpge 27
7: new #2; //class java/lang/StringBuffer
10: dup
11: invokespecial #3; //Method java/lang/StringBuffer."<init>:()V
14: astore_2
15: aload_2
16: iload_3
17: invokevirtual #5; //Method java/lang/StringBuffer.append:(I) java/lang/StringBuffer;
20: pop
21: iinc 3, 1
24: goto 2
27: return
这个例子和上面的例子非常相似。和基本数据类型的例子一样,第二个例子和第三个例子是完全一样的:
public void testObjects3(int);
Code:
0: iconst_0
1: istore_2
2: iload_2
3: iload_1
4: if_icmpge 27
7: new #2; //class java/lang/StringBuffer
10: dup
11: invokespecial #3; //Method java/lang/StringBuffer."<init>":()V
14: astore_3
15: aload_3
16: iload_2
17: invokevirtual #5; //Method java/lang/StringBuffer.append:(I) java/lang/StringBuffer;
20: pop
21: iinc 2, 1
24: goto 2
27: return
第二个和第三个例子的过程如下所示:
-
声明并初始化循环变量
-
装载输入参数和循环变量并将两者进行比较
-
初始化StringBuffer
-
装载字符数组和循环变量
-
将循环变量存入字符数组并调用出栈指令
-
递增循环变量
-
返回到循环起始
这两个例子中的字节码除了代码的顺序和重置方法的调用与第一个例子不同之外,其他部分都完全相同。第一个例子与这两个例子的最大区别就是对StringBuffer实例的初始化使得对象实例被分配在堆中。所以,在循环内部声明新的对象实例会导致每次循环过程都重新分配内存和调用初始化操作。
对象情况下的结论
内存和性能
如果一个对象实例被声明在循环内部,那么每次执行循环体时都会为对象重新分配内存并初始化对象实例。初始化过程并不会占用过多的时间,但是内存分配会。在某些情况下,你可能无法跑到循环外部去创建对象实例。不过,如果可能的话,重置并重用对象应该是更好的选择。
可读性
正如我针对基本数据类型喜欢在循环外部进行变量声明一样,只要有可能,我就会在循环内部重置对象实例而不是重新创建一个。当然,如果被使用的对象实例很小,并且循环的次数不多的话,那么这个问题就不是那么突出了。不过,对这个问题还是小心一点好
结论
如你所见,到底在循环内部或外部进行声明这个问题,对于对象而言是有差别的;而对于基本数据类型而言,则更多地是个人选择问题。我的个人观点是,如果能够在循环体中对对象实例进行重用,那么最好采用这种方案。从一致性角度讲,我相信将变量在循环体外部进行声明会更好。所以,如果你能对对象实例进行重用,那么你大可不必改变你的编码习惯。
来看看下面的例子:
紧接着的例子是:public void testPrimitives3(int);
Code:
0: iconst_0
1: istore_2
2: iload_2
3: iload_1
4: if_icmpge 15
7: iload_2
8: istore_3
9: iinc 2, 1
12: goto 2
15: return