第8章 一般编程
本章重点关注语言的一些细节之处。讨论如何处理与使用局部变量、控制结构、类库及各种数据类型,以及语言之处的两种类设施的使用:反射编程及本地方法。最后讨论性能优化与命名规范。
Effective Java(2nd Edition) Item 45 最小化局部变量的作用域(译文)
本条目本质上与条目13相似,条目13是“最小化类与成员变量的可访问性”。通过最小化局部变量的作用域,增加了代码的可读性与可维护性,减少了可能的错误。
象c这样老点的编程语言,规定必须在块的开头处声明变量,有些程序员保持着这个习惯。但这是一个值得打破的习惯。作为一个善意的提醒,Java允许在语句合法的任何地方声明变量。
最小化局部变量的最有力的技术是在第一次使用它的地方声明它。如果一个变量在使用它的前面被声明,就会混乱—对在试图弄清程序在做什么的读者,会分散他的注意力。在使用变量的地方,读者可能已记不清它的类型或初始值。
过早的声明一个局部变量,不仅会使它的作用域过早开始,还可能使它的作用域过迟结束。局部变量的作用域从声明它的地方开始,到所在块的结束处结束。如果在使用变量的语句块外面声明变量,哪么在语句块结束后,这个变量仍是可见的。如果变量在其使用域之前或之后被偶然使用,结果可能是灾难性的。
几乎所有的局部变量应在声明处初始化。如果你感觉还没有足够多的信息初始化一个变量,就应该推迟对它的声明,值到有足够多的信息对它初始化。这个规则的一个例外是try-catch语句。如果一个方法会抛出一个受检查的异常,这个方法的一个变量要初始化,这个变量应该在try语句块中初始化。但如果这个变量要在try语句块的其它地方被使用,则虽然该变量”感觉”还不应初始化,也只能在try语句块的前面声明这个变量。例子可见231页。
循环语句是展示最小化变量作用域的特别时机。For循环,无论是传统的还是for-each形式,都允许声明循环变量,从而限制了循环变量的作用域只在需要它们的区域内(这个区域包括了循环体,初始化、测试及循环体前的修改),因此,如果循环结束后不再需要循环变量,哪么for循环优于while循环。
例如,下面是一个在集合上迭代时应优先使用的习惯用法(Item 46)。
// Preferred idiom for iterating over a collection
for (Element e : c) {
doSomething(e);
}
在1.5版本前,下面是优先使用的习惯用法(现仍可用):
// No for-each loop or generics before release 1.5
for (Iterator i = c.iterator(); i.hasNext(); ) {
doSomething((Element) i.next());
}
为了看清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,不幸的是,i仍然在作用域内。程序编译没有错误,运行时也没有异常抛出,但却做了错事。第二个循环没有在c2上迭代,而是立即终止了循环,从而给出c2是空的假象。因为这个错误无声无息,所以可能会很长时间不被发现。
如果相似的剪切-粘贴错误发生在for上,无论是for-each还是传统的,最终的代码可能都无法编译。在第二个循环开始点,第一个循环的元素(或迭代)变量已不在其作用域范围内,下面的示例是传统的for循环:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
doSomething(i.next());
}
...
// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
doSomething(i2.next());
}
而且,如果使用了for循环就不太可能产生剪切-粘贴错误,因为在两个循环体内没有必要使用两个不同名的循环变量。因为循环是独立的,所以使用同名的元素(迭代)变量并没不妥。事实上,常常就是这样做的。
与while循环相比,for循环还有一个优势是它比较简短,提高了可读性。
下面是另一个循环惯用法,它能最小化局变量的作用域。
for (int i = 0, n = expensiveComputation(); i < n; i++) {
doSomething(i);
}
这个惯用法中,重点应注意它有两个循环变量,i和n,它们两个都正好有相同的作用域。第二个变量n用来存放第一个变量的上限,避免了每次循环都要重复计算的开销。作为一条规则,如果循环测试需要方法调用,若能保证每次循环的这个方法调用都返回相同的值,就应使用这个惯用法。
最小化局部变量作用域的最后一个技术是保持方法短小,功能集中。如果一个方法中有两项活动,与一项活动相关的局部变量就有可能在执行第二项活动的代码的作用域中,为防止这种情况发生,只要将这个方法分成两个方法,每个方法完成一项活动。