本条目与第15条本质上是类似的。将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
较早的编程语言(如C语言)要求局部变量必须在代码块的开头进行声明,处于习惯,有些程序员目前还是继续这样做。这个习惯应该改正。在此提醒,Java允许你在任何可以出现语句的地方声明变量。
要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的方法进行声明
。如果变量在使用之前进行声明,这只会造成混乱,对于试图理解程序功能的读者来说,这又多了一种分散他们注意力的因素。等要用到该变量时,读者可能已经记不起该变量的类型或者初始化值了。
过早的声明局部变量不仅会使它的作用域过早的扩展,而且结束得过晚。局部变量的作用域从它被声明的点开始扩展,一直到外围块的结束处。如果变量是在使用它的块之外被声明的,当程序退出该块后,该变量仍是可见的。如果变量在它的目标使用区域之前或者之后被意外的使用,后果将可能是灾难性的。
几乎每一个局部变量的声明都应该包含一个初始化表达式
。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个受检异常,该变量就必须在try块内部被初始化。如果变量的值必须在try块的外部用到,它就必须在try块之前被声明,但是在try块之前,它还不能被有意义的初始化。请参照第65条中的例子。
循环中提供了特殊的机会来将变量的作用域最小化。无论是传统的for循环,还是for-each形式的for循环,都允许声明循环变量,它们的作用域被限定在正好需要的范围之内。(这个范围包括循环体,以及循环体之外的初始化、测试、更新部分)。因此,如果在循环终止之后不再需要循环遍历的内容,for循环就优先于while循环
。
例如,下面是一种遍历集合的首选做法(详见第58条):
// Preferred idiom for iterating over a collection or array
for (Element e : c) {
... // Do Something with e
}
如果需要访问迭代器,可能要调用它的remove方法,首选做法是利用传统的for循环代替for-each循环:
// Idiom for iterating when you need the iterator
for (Iterator i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
为了弄清楚为什么这些for循环比while循环更好,请参考下面的代码片段,它包含两个while循环,以及一个Bug:
Iterator i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
...
Iterator i2 = c2.iterator();
while (i.hasNext()) { // BUG!
doSomethingElse(i2.next());
}
第二个循环中包含一个剪切-粘贴错误,本来是要初始化一个新的循环遍历i2,却使用了旧的循环变量i,遗憾的是,这时i仍然还在有效范围内。结果代码仍然可以通过编译,运行的时候也不会抛出异常,但是它所做的事情却是错误的。第二个循环并没有在c2上迭代,而是立即终止,造成c2为空的假象。因为这个程序的错误是悄然发生的,所以可能在很长时间内都不会被发现。
如果类似的剪切 - 粘贴错误发生在前面任何一种for循环中,结果代码根本就不能通过编译。在第二个循环开始之前,第一个循环的元素(或者迭代器)变量已经不在它的作用域范围之内了。下面就是一个传统for循环的例子:
for (Iterator i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
...
// Compile-time error - cannot find symbol i
for (Iterator i2 = c2.iterator(); i.hasNext(); ) {
Element e2 = i2.next();
... // Do something with e2 and i2
}
如果使用for循环,犯这种剪切 - 粘贴错误的可能性就会大大降低,因为通常没有必要在两个循环中使用不同的变量名。循环是完全独立的,所以重用元素(或者迭代器)变量的名词不会有任何危害。实际上,这也是很流行的做法。
使用for循环与使用while循环相比还有另外一个优势:更简短,从而增强了可读性。下面是另一种对局部变量的作用域进行最小化的循环做法:
for (int i = 0, n = expensiveComputation(); i < n; i++) {
... // Do something with i;
}
关于这种做法要关注的重点是,它具有两个循环变量i和n,二者具有相同的作用域。第二个变量n被用来保存第一个变量的极限值,从而避免在每次迭代中执行冗余计算。通常,如果循环测试中涉及方法调用,并且可以保证在每次迭代中都返回同样的结果,就应该使用这种做法。
最后一种将局部变量的作用域最小化的方法是使方法小而集中
。如果把两个操作合并到同一个方法中,与其中一个操作相关的局部变量就有可能会出现在执行另一个操作的代码范围之内。为了防止这种情况发生,只需将这个方法分成两个:每个操作用一个方法来完成。