该【论文翻译(译文) 】是由【Alone-丁丁】上传分享,文档一共【9】页,该文档可以免费在线阅读,需要了解更多关于【论文翻译(译文) 】的内容,可以使用淘豆网的站内搜索功能,选择自己适合的文档,以下文字是截取该文章内的部分文字,如需要获得完整电子版,请下载此文档到您的设备,方便您编辑和打印。论文翻译(译文)
Java编程中遇到的陷阱、圈套
谜题1:奇数性
下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转吗?
publicstaticbooleanisOdd(inti){
returni%2==1;
}
奇数可以被定义为被2整除余数为1的整数。表达式i%2计算的是i整除2时所产生的余数,因此看起来这个程序应该能够正确运转。遗憾的是,它不能;它在四分之一的时间里返回的都是错误的答案。
为什么是四分之一?因为在所有的int数值中,有一半都是负数,而isOdd方法对于对所有负奇数的判断都会失败。在任何负整数上调用该方法都回返回false,不管该整数是偶数还是奇数。
这是Java对取余操作符(%)的定义所产生的后果。该操作符被定义为对于所有的int数值a和所有的非零int数值b,都满足下面的恒等式:
(a/b)*b+(a%b)==a
换句话说,如果你用b整除a,将商乘以b,然后加上余数,那么你就得到了最初的值a。该恒等式具有正确的含义,但是当与Java的截尾整数整除操作符相结合时,它就意味着:当取余操作返回一个非零的结果时,它与左操作数具有相同的正负符号。
当i是一个负奇数时,i%2等于-1而不是1,因此isOdd方法将错误地返回false。为了防止这种意外,请测试你的方法在为每一个数值型参数传递负数、零和正数数值时,其行为是否正确。
这个问题很容易订正。只需将i%2与0而不是与1比较,并且反转比较的含义即可:
publicstaticbooleanisOdd(inti){
returni%2!=0;
}
如果你正在在一个性能临界(performance-critical)环境中使用isOdd方法,那么用位操作符AND(&)来替代取余操作符会显得更好:
publicstaticbooleanisOdd(inti){
return(i&1)!=0;
}
总之,无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号。该
这么做可以正常运行,但是显得很丑陋。其实我们还是有办法去避免这种方式所产生的拖沓冗长的代码。你可以通过确保至少有一个操作数为字符串类型,来强制+操作符去执行一个字符串连接操作,而不是一个加法操作。这种常见的惯用法用一个空字符串("")作为一个连接序列的开始,如下所示:
(""+'H'+'a');
这种惯用法可以确保子表达式都被转型为字符串。尽管这很有用,但是多少有一点难看,而且它自身可能会引发某些混淆。你能猜到下面的语句将会打印出什么吗?如果你不能确定,那么就试一下:
("2+2="+2+2);
,你还可以使用
("%c%c",'H','a');
总之,使用字符串连接操作符使用格外小心。+操作符当且仅当它的操作数中至少有一个是String类型时,才会执行字符串连接操作;否则,它执行的就是加法。如果要连接的没有一个数值是字符串类型的,那么你可以有几种选择:
预置一个空字符串;
;
使用一个字符串缓冲区;
,可以用printf方法。
这个谜题还包含了一个给语言设计者的教训。操作符重载,即使在Java中只在有限的范围内得到了支持,它仍然会引起混淆。为字符串连接而重载+操作符可能就是一个已铸成的错误。
谜题3:尽情享受每一个字节
下面的程序循环遍历byte数值,以查找某个特定值。这个程序会打印出什么呢?
publicclassBigDelight{
publicstaticvoidmain(String[]args){
for(byteb=;b<;b++){
if(b==0x90)
("Joy!");
}
}
}
,以查找0x90。这个数值适合用byte表示,,因此你可能会想这个循环在该迭代会找到它一次,并将打印出Joy!。但是,所见为虚。如果你运行该程序,就会发现它没有打印任何东西。怎么回事?
简单地说,0x90是一个int常量,它超出了byte数值的范围。这与直觉是相悖的,因为0x90是一个两位的十六进制字面常量,每一个十六进制位都占据4个比特的位置,所以整个数值也只占据8个比特,即1个byte。问题在于byte是有符号类型。常量0x90是一个正的最高位被置位的8位int数值。合法的byte数值是从-128到+127,但是int常量0x90等于+144。
拿一个byte与一个int进行的比较是一个混合类型比较(mixed-typecomparison)。如果你把byte数值想象为苹果,把int数值想象成为桔子,那么该程序就是在拿苹果与桔子比较。请考虑表达式((byte)0x90==0x90),尽管外表看起来是成立的,但是它却等于false。
为了比较byte数值(byte)0x90和int数值0x90,Java通过拓宽原始类型转换将byte提升为一个int[],然后比较这两个int数值。因为byte是一个有符号类型,所以这个转换执行的是符号扩展,将负的byte数值提升为了在数字上相等的int数值。在本例中,该转换将(byte)0x90提升为int数值-112,它不等于int数值0x90,即+144。
由于系统总是强制地将一个操作数提升到与另一个操作数相匹配的类型,所以混合类型比较总是容易把人搞糊涂。这种转换是不可视的,而且可能不会产生你所期望的结果。有若干种方法可以避免混合类型比较。我们继续有关水果的比喻,你可以选择拿苹果与苹果比较,或者是拿桔子与桔子比较。你可以将int转型为byte,之后你就可以拿一个byte与另一个byte进行比较了:
if(b==(byte)0x90)
("Joy!");
或者,你可以用一个屏蔽码来消除符号扩展的影响,从而将byte转型为int,之后你就可以拿一个int与另一个int进行比较了:
if((b&0xff)==0x90)
("Joy!");
上面的两个解决方案都可以正常运行,但是避免这类问题的最佳方法还是将常量值移出到循环的外面,并将其在一个常量声明中定义它。下面是我们对此作出的第一个尝试:
publicclassBigDelight{
privatestaticfinalbyteTARGET=0x90;
publicstaticvoidmain(String[]args){
for(byteb=;b<
;b++){
if(b==TARGET)
("Joy!");
}
}
}
遗憾的是,它根本就通不过编译。常量声明有问题,编译器会告诉你问题所在:0x90对于byte类型来说不是一个有效的数值。如果你想下面这样订正该声明,那么程序将运行得非常好:
privatestaticfinalbyteTARGET=(byte)0x90;
总之,要避免混合类型比较,因为它们内在地容易引起混乱(谜题5)。为了帮助实现这个目标,请使用声明的常量替代“魔幻数字”。你已经了解了这确实是一个好主意:它说明了常量的含义,集中了常量的定义,并且根除了重复的定义。现在你知道它还可以强制你去为每一个常量赋予适合其用途的类型,从而消除了产生混合类型比较的一种根源。
对语言设计的教训是byte数值的符号扩展是产生bug和混乱的一种常见根源。而用来抵销符号扩展效果所需的屏蔽机制会使得程序显得混乱无序,从而降低了程序的可读性。因此,byte类型应该是无符号的。还可以考虑为所有的原始类型提供定义字面常量的机制,这可以减少对易于产生错误的类型转换的需求(谜题27)。
谜题4:优柔寡断
下面这个可怜的小程序并不能很好地做出其自己的决定。它的decision方法将返回true,但是它还返回了false。那么,它到底打印的是什么呢?甚至,它是合法的吗?
publicclassIndecisive{
publicstaticvoidmain(String[]args){
(decision());
}
staticbooleandecision(){
try{
returntrue;
}finally{
returnfalse;
}
}
}
你可能会认为这个程序是不合法的。毕竟,decision方法不能同时返回true和false。如果你尝试一下,就会发现它编译时没有任何错误,并且它所打印的是false。为什么呢?
原因就是在一个try-finally语句中,finally语句块总是在控制权离开try语句块时执行的[]。无论try语句块是正常结束的,还是意外结束的,情况都是如此。一条语句或一个语句块在它抛出了一个异常,或者对某个封闭型语句执行了一个break或continue,或是象这个程序一样在方法中执行了一个return时,将发生意外结束。它们之所以被称为意外结束,是因为它们阻止程序去按顺序执行下面的语句。
当try语句块和finally语句块都意外结束时,在try语句块中引发意外结束的原因将被丢弃,而整个try-finally语句意外结束的原因将于finally语句块意外结束的原因相同。在这个程序中,在try语句块中的return语句所引发的意外结束将被丢弃,而try-finally语句意外结束是由finally语句块中的return造成的。简单地讲,程序尝试着(try)返回(return)true,但是它最终(finally)返回(return)的是false。
丢弃意外结束的原因几乎永远都不是你想要的行为,因为意外结束的最初原因可能对程序的行为来说会显得更重要。对于那些在try语句块中执行break、continue或return语句,只是为了使其行为被finally语句块所否决掉的程序,要理解其行为是特别困难的。
总之,每一个finally语句块都应该正常结束,除非抛出的是不受检查的异常。千万不要用一个return、break、continue或throw来退出一个finally语句块,并且千万不要允许将一个受检查的异常传播到一个finally语句块之外去。
对于语言设计者,也许应该要求finally语句块在未出现不受检查的异常时必须正常结束。朝着这个目标,try-finally结构将要求finally语句块可以正常结束[]。return、break或continue语句把控制权传递到finally语句块之外应该是被禁止的,任何可以引发将被检查异常传播到finally语句块之外的语句也同样应该是被禁止的。
谜题5:令人混淆的构造器案例
本谜题呈现给你了两个容易令人混淆的构造器。main方法调用了一个构造器,但是它调用的到底是哪一个呢?该程序的输出取决于这个问题的答案。那么它到底会打印出什么呢?甚至它是否是合法的呢?
publicclassConfusing{
privateConfusing(Objecto){
("Object");
}
privateConfusing(double[]dArray){
("doublearray");
}
publicstaticvoidmain(String[]args){
newConfusing(null);
}
}
传递给构造器的参数是一个空的对象引用,因此,初看起来,该程序好像应该调用参数类型为Object的重载版本,并且将打印出Object。另一方面,数组也是引用类型,因此null也可以应用
于类型为double[]的重载版本。你由此可能会得出结论:这个调用是模棱两可的,该程序应该不能编译。如果你试着去运行该程序,就会发现这些直观感觉都是不对的:该程序打印的是doublearray。这种行为可能显得有悖常理,但是有一个很好的理由可以解释它。
Java的重载解析过程是以两阶段运行的。第一阶段选取所有可获得并且可应用的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性[]。
在我们的程序中,两个构造器都是可获得并且可应用的。构造器Confusing(Object)可以接受任何传递给Confusing(double[])的参数,因此Confusing(Object)相对缺乏精确性。(每一个double数组都是一个Object,但是每一个Object并不一定是一个double数组。)因此,最精确的构造器就是Confusing(double[]),这也就解释了为什么程序会产生这样的输出。
如果你传递的是一个double[]类型的值,那么这种行为是有意义的;但是如果你传递的是null,这种行为就有违直觉了。理解本谜题的关键在于在测试哪一个方法或构造器最精确时,这些测试没有使用实际的参数:即出现在调用中的参数。这些参数只是被用来确定哪一个重载版本是可应用的。一旦编译器确定了哪些重载版本是可获得且可应用的,它就会选择最精确的一个重载版本,而此时使用的仅仅是形式参数:即出现在声明中的参数。
要想用一个null参数来调用Confusing(Object)构造器,你需要这样写代码:newConfusing((Object)null)。这可以确保只有Confusing(Object)是可应用的。更一般地讲,要想强制要求编译器选择一个精确的重载版本,需要将实际的参数转型为形式参数所声明的类型。
以这种方式来在多个重载版本中进行选择是相当令人不快的。在你的API中,应该确保不会让客户端走这种极端。理想状态下,你应该避免使用重载:为不同的方法取不同的名称。当然,有时候这无法实现,例如,构造器就没有名称,因而也就无法被赋予不同的名称。然而,你可以通过将构造器设置为私有的并提供公有的静态工厂,以此来缓解这个问题[EJItem1]。如果构造器有许多参数,你可以用Builder模式[Gamma95]来减少对重载版本的需求量。
如果你确实进行了重载,那么请确保所有的重载版本所接受的参数类型都互不兼容,这样,任何两个重载版本都不会同时是可应用的。如果做不到这一点,那么就请确保所有可应用的重载版本都具有相同的行为[EJItem26]。
总之,重载版本的解析可能会产生混淆。应该尽可能地避免重载,如果你必须进行重载,那么你必须遵守上述方针,以最小化这种混淆。如果一个设计糟糕的API强制你在不同的重载版本之间进行选择,那么请将实际的参数转型为你希望调用的重载版本的形式参数所具有的类型。
论文翻译(译文) 来自淘豆网m.daumloan.com转载请标明出处.