通用程序设计: 将局部变量的作用域最小化:外文翻译资料
2022-09-27 11:30:23
英语原文共 369 页,剩余内容已隐藏,支付完成后下载完整资料
外文翻译
通用程序设计:
将局部变量的作用域最小化:
较早的程序语言(如C语言)要求局部变量必须在一个代码块的开头处进行声明,出于习惯,有些程序员们目前还是继续这样做。这个习惯应该改正。再次提醒,java允许你在任何可以出现语句的地方声明变量。
要使局部变量的作用于最小化,最有力的方法就是第一次使用它是地方声明。如果变量在这之前进行声明,这只会造成混乱,对于试图理解程序功能的读者来说,这又多了一种只会分散他们注意力的因素。等到用到该变量的时候,读者可能已经记不起该变量的类型或者初始值了。
过早的声明局部变量不仅会使它的作用域过早地扩展,而且结束得也过于晚了。局部变量的作用域从它被声明的点看是扩展,一直到外围快的结束出。如果变量是在“使用它的块”之外被声明的,当程序退出该块之后,该变量仍然是可见的。如果变量在它的目标使用区域之前或者之后被意外地使用的话,后果可能是灾难性的。
几乎每个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,知道可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个受检的异常,该变量就必须在try块的内部初始化。如果变量的值必须在try块的外部被使用到,他就必须在try块之前被声明,但是在try块之前,它还不能被“有意义地初始化”。
循环中提供了特殊的机会来将变量的作用域最小化。(无论是转筒的还是for-each形式的)for循环,都允许声明循环变量(loop variable),它的作用域被限定在正好需要范围之内。(这个范围包括循环体,以及循环体之前的初始化、测试、更新部分。)因此,如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环。
例如,下面是一种遍历集合的首选做法
//Preferred idiom for iterating over a collection
For(Element e:c){
doSomething(e);
}
在java 1.5发行版本之前,首选的做法如下(现在仍然有适用之处):
//No for-each loop or generics before release 1.5
For(Interator i = c.iterator();i.hasNext();){
doSomething((Element)i.next());
}
为了弄清楚为什么这个for循环比while循环更好,请考虑下面的代码片段,它包含两个while循环,以及一个Bug:
Iteratorlt;Elementgt; I = c.iterator();
While(i.hasNext()){
doSmothing(i.next);
}
hellip;
Iteratorlt;Elementgt; i2 = c2.iterator();
While(i.hasNext()){ // BUG
doSmothing(i2.next);
}
第二个循环中包含一个“剪切-粘贴”错误:它本来是要初始化一个新的循环变量i2,却使用了旧的循环变量I;遗憾的是,这时i仍然还在有效范围之内。结果循环并没有在c2上迭代,而是立即终止,造成c2为空的假象。因为这个程序的错误是悄然发生的,所以可能在很长时间内都不会被发现。
使用for循环域使用while循环相比还有另外一个优势:更简短,从容增强了可读性。
下面是另外一种对局部变量的作用域进行最下化的循环做法:
For(int i=0,n=expensiveComputation();ilt;n;i ){
doSomething(i);
}
关于这种做法要关注的一点是,它具有两个循环变量i和n,两者具有完全相同的作用域。第二个变量n被用来保存第一个变量的极限值,从而避免在每次迭代中执行冗余计算的开销。通常,如果循环测试中涉及方法调用,它可以保证在每次迭代中都会返回同样的结果,就应该使用这种做法。
最后一种“将局部变量的作用域最小化”的方法是使用方法小二集中。如果把两个操作合并到同一个方法中,与其中一个操作先关的局部变量就有可能会出现在执行另一个操作的代码范围之内。为了防止这种情况发生,只要把这个方法分成两个,每个方法各执行一个操作。
For-each循环优先于传统的for循环
在Java 1.5发行版本之前,对集合jinxing遍历的首选做法如下:
//No longer the preferred idiom to iterate over a collection
For(Iterator i=c.iterator();i.hasNext();){
doSomething((Element)i.next());//(No generics 1.5)
}
遍历数组的首选做法如下:
// No longer the preferred idiom to iterate over a array!
For(int i=0;ilt;a.length;i ){
doSomething(a[i]);
}
这些做法都比while循环更好,但是它们也并不完美。迭代器和索引变量都会造成一些混乱。而且,它们也代表着出错的可能。迭代器和索引变量在每个循环中出现三次,其中两次让你很容易出错。一旦出错,就无法保证编译器能够发现这个错误。
Java 1.5发行版本引入for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组:
//The preferred idiom iterating over collections and arrys
For(Element e:elements){
doSomething(e);
}
当见到冒号(:)时,可以把它读作“在...里面”。因此上面的循环可以读作“对于元素中的每个元素e。”注意,利用for-each循环不会有性能损失,甚至用于数组也一样。实际上,在某些情况下,比起普通的for循环,它还稍有些性能优势,因为它对数组索引的边界值计算一次,虽然可以手工完成这项工作,但程序员并总会这么做。
在对多个集合进行嵌套式迭代式,for-each循环相对于传统for循环的这种优势还会更加明显。For-each循环在简洁性和预防Bug方面有着传统for循环无法比拟的优势,并且没有性能损失。应该尽可能地使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:
- 过滤—如果需要遍历集合,并删除选定的元素,就需要使用县式的迭代器,一遍可以调用它的remove方法。
- 转换—如果需要遍历表或者数组,并取代它部分或者全部元素值,就需要列表迭代器或者数值索引,以便设定元素的值。
- 平行迭代—如果需要并行地遍历多个集合,就需要更显式地控制迭代器或者索引变量,以便迭代器或者索引变量都可以得到同步前移。
在以上任何一种情况下,就要使用普通for循环,要警惕本条目中提到的陷阱,并且要确保做到最好。
了解和使用类库
假如你希望产生位于0和某个上界之间的随机整数。面对这个常见的任务,许多程序会编写出如下所示的方法
Private static final Random rnd = new Random();
//Common but deeply flawed!
static int random(int n){
return Math.abs(rnd.nextInt())%n;
}
这个方法看起来可能不错,但是有三个缺点。第一个缺点是,如果n是一个比较小的2的乘方,经过相当短的周期之后,它会产生的随机数列将会重复。第二个缺点是,如果n不是2的乘方,那么平均起来,有些数会比其他的数出现得更为频繁如果n比较大,这个缺点会非常明显。这可以通过下面的程序直观的体现出来,它会产生一百万个经过细心指定的范围内的随机数,并打印出多少个字数落在随机数取值范围内的前半部分:
public static void main(String[] args){
int n = 2*(Integer.MAX_VALUE/3);
int low = 0;
for(int i = 0;ilt;1000000;i )
if(random(n)lt;n/2);
low ;
System.out.println(low);
}
如果random方法正常的话,这个程序打印出来的数将接近一百万的一半,但是如果真正运行这个程序,就会发现他打印出来的数接近于666666.由random方法产生的数字有2/3落在随机数取值的前半部分。
Random方法的第三个缺点是,在极少数情况下,它的失败是灾难性的,返回一个落在指定范围之外的数。之所以如此,是因为这个方法试图通过调用Math.abs,将rnd.nextInt()返回的值映射为一个非负整数int。如果nextInt返回Integer.MAIN_VALUE,那么Math.abs也会返回Integer.MAIN_VALUE,假设n不是2的乘方,那么取模操作符(%)将返回一个负数。这几乎肯定会是程序失败,而且这种失败很难重现。
使用标准类库的好处是,不必浪费是时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上。还一个好处是,它们的性能往往会随着时间的推移而不断提高,无需你做任何努力。因为许多人在使用它们,被当做工业标准在使用,所以,提供这些标准类库的组织有强烈的机动要使它们运行的更快。这些年来,许多Java平台类库已经被重新编写了,有时候是重复编写,从而导致性能上有了显著的提高。
标准类库也会随着时间的推移而增加新的功能。如果类库中漏掉了某些功能,开发者社区就会把这些缺点告示出来,漏掉的功能就会添加到后续的版本中。Java平台类库始终是在这个社区的推动下不断发展。
使用标准类库的最会一个好处就是,可以使自己的代码融入主流。这样的代码更容易读、更易维护、更易被大多数的开发人员重用。
如果需要精确的答案,避免使用floa和double
float和double类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。Float和double类型尤其不适合用于货币计算,因为让一个float或者double精确地表示0.1(或者10的任何其它负数次方)都是不可能的。
例如,假设你的口袋中有$1.03,花掉了42¢之后还剩下多少钱呢?下面是一个很简单的程序片段,要回答这个问题:
System.out.println(1.03-.42);
遗憾的是,它的输出结果是0.6100000000000001。这并不是个别例子。假设你的口袋里有$1,你买了9个垫圈,每个10¢。那么你应该找回多少个零头呢?
System.out.println(1.00-9*0.9);
根据这个程序片段,你得到的是$0.099999999999998。
你可能会认为,只要在打印之前将结果做一下摄入就可以解决问题,但遗憾的是,这种做法并不总是可行。例如,假如你的口袋里有$1,你看到货架上有一排美味的糖果,标价分别为10¢、20¢、30¢,等等,一直到$1。你打算从标价为10¢的糖果,每种1颗,一直到不能支付货架上下一种价格的糖果为止,那么你可以买多少颗糖果?还会找回多少零头?下面是一个简单的程序,用来解决这个问题:
//Broken – uses floating point for monetary calculation!
Public staric void main (String[] args)
{
double funds = 1.00;
int intemsBought = 0;
for (double price =.10;fundgt;=price; price =.10)
{
funds -=price;
itemBought ;
}
System.out,println(itemBought “items bought.”);
System.out,println(“Change:$” funds);
}
如果真正运行这个程序,你会发现你可以支付3颗糖果
剩余内容已隐藏,支付完成后下载完整资料
资料编号:[150422],资料为PDF文档或Word文档,PDF文档可免费转换为Word