1.奇数性
下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转吗?public static boolean isOdd(int i){
return i % 2 == 1;
}
奇数可以被定义为被 2 整除余数为 1 的整数。表达式 i % 2 计算的是 i 整除 2时所产生的余数,因此看起来这个程序应该能够正确运转。遗憾的是,它不能。因为在所有的 int 数值中,有一半都是负数,而 isOdd 方法对于对所有负奇数的判断都会失败。在任何负整数上调用该方法都回返回false ,不管该整数是偶数还是奇数。
当 i 是一个负奇数时,i % 2 等于-1 而不是 1, 因此 isOdd 方法将错误地返回 false。为了防止这种意外,请测试你的方法在为每一个数值型参数传递负数、零和正数数值时,其行为是否正确。这个问题很容易订正。只需将 i % 2 与 0 而不是与 1 比较,并且反转比较的含义即可:
public static boolean isOdd(int i){
return i % 2 != 0;
}
用位操作符 AND(&)来替代取余操作符会显得更好:
public static boolean isOdd(int i){
return (i & 1) != 0;
}
总之,无论何时使用到了取余操作符,都要考虑到操作数和结果的符号。该操作符的行为在其操作数非负时是一目了然的,但是当一个或两个操作数都是负数时,它的行为就不那么显而易见了。
2.小数运算
下面是一个小数运算程序,它会打印出什么呢?public class Change{
public static void main(String args[]){
System.out.println(2.00 - 1.10);
}
}
你可能会很天真地期望该程序能够打印出 0.90,但是它如何才能知道你想要打印小数点后两位小数呢?如果你运行该程序,你就会发现它打印的是 0.8999999999999999。问题在于 1.1 这个数字不能被精确表示成为一个 double,因此它被表示成为最接近它的 double 值。该程序从2 中减去的就是这个值。遗憾的是,这个计算的结果并不是最接近0.9的double 值。表示结果的 double 值的最短表示就是你所看到的打印出来的那个数字。
解决该问题的一种方式是使用某种整数类型,例如 int 或 long,并且以分为单位来执行计算。下面是我们用 int类型来以分为单位表示货币值后重写的 println 语句。这个版本将打印出正确答案 90 分:System.out.println((200 - 110) + "cents");
解决该问题的另一种方式是使用执行精确小数运算的 BigDecimal。它还可以通过 JDBC 与 SQL DECIMAL 类型进行互操作。一定要用BigDecimal(String)构造器,而千万不要用 BigDecimal(double)。后一个构造器将用它的参数的“精确”值来创建一个实例:new BigDecimal(0.1)将返回一个表示 0.1000000000000000555111512312578270211815834 的BigDecimal。通过正确使用 BigDecimal,程序就可以打印出我们所期望的结果0.90:
import java.math.BigDecimal;
public class Change1{
public static void main(String args[]){
System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10")));
}
}
这个版本并不是十分地完美,BigDecimal 的计算很有可能比那些使用原始类型的计算要慢一些,对某些大量使用小数计算的程序来说,这可能会成为问题,而对大多数程序来说,这显得一点也不重要。总之, 在需要精确答案的地方,要避免使用 float 和 double;对于货币计算,要使用 int、long 或 BigDecimal。
3.令人混淆的构造器
main 方法调用了一个构造器,但是它调用的到底是哪一个呢?该程序的输出取决于这个问题的答案。那么它到底会打印出什么呢?甚至它是否是合法的呢?public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
传递给构造器的参数是一个空的对象引用,因此,初看起来,该程序好像应该调用参数类型为 Object 的重载版本,并且将打印出 Object。另一方面,数组也是引用类型,因此 null 也可以应用于类型为 double[ ]的重载版本。由此可能会得出结论:这个调用是模棱两可的,该程序应该不能编译。如果试着去运行该程序,就会发现这些直观感觉都是不对的:该程序打印的是 double array。这种行为可能显得有悖常理,但是有一个很好的理由可以解释它。
Java 的重载解析过程是以两阶段运行的。第一阶段选取所有可获得并且可应用的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性。
在程序中,两个构造器都是可获得并且可应用的。构造器Confusing(Object)可以接受任何传递给 Confusing(double[ ])的参数,因此Confusing(Object)相对缺乏精确性。(每一个 double 数组都是一个 Object,但是每一个 Object 并不一定是一个 double 数组。)因此,最精确的构造器就是Confusing(double[ ]),这也就解释了为什么程序会产生这样的输出。
要想用一个 null 参数来调用 Confusing(Object)构造器,你需要这样写代码:new Confusing((Object)null)。这可以确保只有 Confusing(Object)是可应用的。更一般地讲,要想强制要求编译器选择一个精确的重载版本,需要将实际的参数转型为形式参数所声明的类型。
4.隐藏域
在下面的程序中,子类的一个域具有与超类的一个域相同的名字。那么,这个程序会打印出什么呢?class Base {
public String className = "Base";
}
class Derived extends Base {
private String className = "Derived";
}
public class PrivateMatter {
public static void main(String[ ] args) {
System.out.println(new Derived().className);
}
}
对该程序的表面分析可能会认为它应该打印 Derived,因为这正是存储在每一个Derived 实例的 className 域中的内容。更深入一点的分析会认为 Derived 类不能编译,因为 Derived 中的 className变量具有比 Base 中的 className 变量更具限制性的访问权限。如果你尝试着编译该程序,就会发现这种分析也不正确。该程序确实不能编译,但是错误却出在 PrivateMatter 中。
如果className是一个实例方法,而不是一个实例域,那么Derived.className()将覆写 Base.className(),而这样的程序是非法的。一个覆写方法的访问修饰符所提供的访问权限与被覆写方法的访问修饰符所提供的访问权限相比,至少要一样多。因为className 是一个域,所以 Derived.className 隐藏(hide)了Base.className,而不是覆盖了它。对一个域来说,当它要隐藏另一个域时,如果隐藏域的访问修饰符提供的访问权限比被隐藏域的少,尽管这么做不可取的,但是它确实是合法的。事实上,对于隐藏域来说,如果它具有与被隐藏域完全无关的类型,也是合法的:即使 Derived.className 是GregorianCalendar 类型的,Derived 类也是合法的。如果Derived类中对className域的访问修饰符大于或等于,就能正常访问。
在程序中的编译错误出现在 PrivateMatter 类试图访问Derived.className 的时候。尽管 Base 有一个公共域 className,但是这个域没有被继承到 Derived 类中,因为它被 Derived.className 隐藏了。在 Derived类内部,域名 className 引用的是私有域 Derived.className。因为这个域被声明为是 private 的,所以它对于 PrivateMatter 来说是不可访问的。因此,编译器产生了类似下面这样的一条错误信息:
PrivateMatter.java:11: className has private access in Derived
System.out.println(new Derived().className);
^
请注意,尽管在 Derived 实例中的公共域 Base.className 被隐藏了,但还是可以通过将 Derived 实例转型为 Base 来访问到它。下面版本的PrivateMatter 就可以打印出 Base:
public class PrivateMatter {
public static void main(String[] args) {
System.out.println(((Base)new Derived()).className);
}
}
这说明了覆写与隐藏之间的一个非常大的区别。一旦一个方法在子类中被覆写,就不能在子类的实例上调用它了(除了在子类内部,通过使用 super 关键字来方法)。 然而,你可以通过将子类实例转型为某个超类类型来访问到被隐藏的域,在这个超类中该域未被隐藏。
如下示例:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Sub sub = new Sub();
Field[] fileds = sub.getClass().getFields();
for (Field field : fileds) {
System.out.println(field.getName());
}
}
}
class Base {
public String name = "base";
public int age = 22;
}
class Sub extends Base {
private String name = "sub";
}
运行结果:
name
age
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Sub sub = new Sub();
Field[] fileds = sub.getClass().getFields();
for (Field field : fileds) {
System.out.println(field.getName());
}
}
}
class Base {
public String name = "base";
public int age = 22;
}
class Sub extends Base {
protected String name = "sub";
}
运行结果:
name
age
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Sub sub = new Sub();
Field[] fileds = sub.getClass().getFields();
for (Field field : fileds) {
System.out.println(field.getName());
}
}
}
class Base {
public String name = "base";
public int age = 22;
}
class Sub extends Base {
public String name = "sub";
}
运行结果:
name
name
age
本文探讨了Java编程中常见的几个陷阱,包括奇数判断错误、小数运算不精确、构造器混淆及隐藏域问题。
336

被折叠的 条评论
为什么被折叠?



