Effective Java 第三版——57. 最小化局部变量的作用域
Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。
9. 通用编程
这一章专门讨论Java语言的具体细节。讨论了局部变量、控制结构、类库、数据类型以及两种Java语言之外工具:反射和本地方法。最后,讨论了优化和命名惯例。
57. 最小化局部变量的作用域
这条目在性质上类似于条目 15,即“最小化类和成员的可访问性”。通过最小化局部变量的作用域,可以提高代码的可读性和可维护性,并降低出错的可能性。
较早的编程语言(如C)要求必须在代码块的头部声明局部变量,并且一些程序员继续习惯这样做。 这是一个值得改进的习惯。 作为提醒,Java允许你在任何合法的语句的地方声明变量(as does C, since C99)。
用于最小化局部变量作用域的最强大的技术是再首次使用的地方声明它。 如果变量在使用之前被声明,那就变得更加混乱—— 这也会对试图理解程序的读者来讲,又增加了一件分散他们注意力的事情。 到使用该变量时,读者可能不记得变量的类型或初始值。
过早地声明局部变量可能导致其作用域不仅过早开始而且结束太晚。 局部变量的作用域从声明它的位置延伸到封闭块的末尾。 如果变量在使用它的封闭块之外声明,则在程序退出该封闭块后它仍然可见。如果在其预定用途区域之前或之后意外使用变量,则后果可能是灾难性的。
几乎每个局部变量声明都应该包含一个初始化器。如果还没有足够的信息来合理地初始化一个变量,那么应该推迟声明,直到认为可以这样做。这个规则的一个例外是try-catch语句。如果一个变量被初始化为一个表达式,该表达式的计算结果可以抛出一个已检查的异常,那么该变量必须在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<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
要了解为什么这些for循环优于while循环,请考虑以下代码片段,其中包含两个while循环和一个bug:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
doSomethingElse(i2.next());
}
第二个循环包含一个复制粘贴错误:它初始化一个新的循环变量i2,但是使用旧的变量i,不幸的是,它仍在范围内。 生成的代码编译时没有错误,并且在不抛出异常的情况下运行,但它做错了。 第二个循环不是在c2上迭代,而是立即终止,给出了c2为空的错误印象。 由于程序无声地出错,因此错误可能会长时间无法被检测到。
如果将类似的复制粘贴错误与for循环(for-each循环或传统循环)结合使用,则生成的代码甚至无法编译。第一个循环中的元素(或迭代器)变量不在第二个循环中的作用域中。下面是它与传统for循环的示例:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
...
// Compile-time error - cannot find symbol i
for (Iterator<Element> 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用于存储第一个变量的限定值,从而避免了每次迭代中冗余计算的代价。作为一个规则,如果循环测试涉及一个方法调用,并且保证在每次迭代中返回相同的结果,那么应该使用这种用法。
最小化局部变量作用域的最终技术是保持方法小而集中。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。