Java易犯错误

yuxianhui

贡献于2013-05-31

字数:0 关键词: Java开发

u 1 第第 11 部部分分 语语法法 程序员们总是被层出不穷的复杂问题所困扰 假如我们最基本的开放工具 —— 设计和编写程序的语言本身就是复杂的 那么这个语言自己也会成为这些复 杂问题的一部分 而非它们的解决方案了 —— C. A. R. Hoare, The Emperor’s Old Clothes Java 语言从 C++派生而来 并借鉴了 Objective C Eiffel Smalltalk Mesa 和 Lisp 这些 语言的一些特性 当使用其他语言的程序员转用 Java 来编程时 他们很快就会发现 Java 的 一些特性和自己以前所熟悉的语言非常相似 因此这些程序员通常会认为这些特性在 Java 中 和在以前所使用的语言中表现一致 其实完全不是这样 这些想法在 C++程序员中尤其普遍 这一部分重点强调了 Java 语言上经常会 绊倒 新手的陷阱和语言行为 本部分包括以下 10 个单元 Item 1 什么时候 被覆盖的 方法并非真的被覆盖了 本单元解释了调用子类的实例 方法和静态方法之间的微妙差别 Item 2 String.equals()方法与 == 运算符的用法比较 ”本单元解释了这两种方法比 较字符串的不同之处 并指出了常量池是如何混淆这两种用法的 第 1 部分 语法 2 t Item 3 Java 是强类型语言 本单元解释了基本类型的转换和提升的规则 这对从 C++ 转到 Java 的程序员尤为重要 Item 4 那是构造函数吗 本单元给出了一个经典的 然而又非常简单的语言陷阱 当 我们培训新的 Java 学员时 这个陷阱总是让学员们问出这样的问题 编译器怎么会没发现 它 Item 5 不能访问被覆盖的方法 本单元又一次讨论了 Java 语言中的方法调用 读完以 后 你将完全理解这个知识点 Item 6 避免落入 隐藏变量成员 的陷阱 本单元讨论了这一最常见的陷阱 所有 Java 语言的入门课程都应该提及这个问题 并且和 this 引用一起讨论 Item 7 提前引用 这一较短的单元向我们演示了什么是 提前引用 以及如何去避免 它 Item 8 设计可继承的构造函数 本单元是取自来之不易的实践经验 对于每一个想开 发可重用 Java 类的程序员来说 这个单元是必读的 Item 9 通过引用传递基本类型 本单元对从 C++转换到 Java 的程序员特别有价值 它 解答了在 Java 中传递引用的相关问题 Item 10 布尔运算符与 短路 运算符 本单元解释了 Java 编程中另一个常见的陷阱 使用逻辑运算符 单元中也举了一个使用短路 short-circuit 运算符的清晰例子 Item 1: 什么时候 被覆盖的 方法并非真的被覆盖了 好吧 我承认 本单元的标题确实带有一定的欺骗性 虽然它的本意并非欺骗你 而是 帮助你理解方法覆盖的概念 想必你已经阅读了一两本这样的 Java 书籍 它们在开头都指出 了面向对象编程的 3 个主要概念 封装 继承和多态 理解这 3 个概念对于领会 Java 语言来 说至关重要 而搞懂方法的覆盖又是理解继承概念的关键部分 覆盖实例方法会在 Item 5 谈到 本单元介绍静态方法的覆盖 如果你还不明白两者的区 别 那么 Item 1 和 Item 5 正适合你 假如你已经急不可待地喊出 不能覆盖静态方法 那 么 你也许需要放松片刻再继续往下看 不过 在此之前先看看你是否能够猜出下面例子的 输出结果 Item 1: 什么时候 被覆盖的 方法并非真的被覆盖了 u 3 这个例子摘自 Java 语言规范 8.4.8.5 节 01: class Super 02: { 03: static String greeting() 04: { 05: return "Goodnight"; 06: } 07: 08: String name() 09: { 10: return "Richard"; 11: } 12: } 01: class Sub extends Super 02: { 03: static String greeting() 04: { 05: return "Hello"; 06: } 07: 08: String name() 09: { 10: return "Dick"; 11: } 12: } 01: class Test 02: { 03: public static void main(String[] args) 04: { 05: Super s = new Sub(); 06: System.out.println(s.greeting() + ", " + s.name()); 07: } 08: } 运行 Test 类的结果如下 Goodnight, Dick 要是你得出了同样的输出结果 那么你或许对方法的覆盖有了较好的理解 如果你的结 果和答案不一致 那就让我们一起找出原因 我们先分析一下各个类 Super 类由方法 greeting 和 name 组成 Sub 类继承了 Super 类 而且同样含有 greeting 和 name 方法 Test 类只有一 个 main 方法 第 1 部分 语法 4 t 在 Test 类的第 5 行中 我们创建了一个 Sub 类的实例 在这里 你必须明白的是 虽然 变量 s 的数据类型为 Super 类 但是它仍旧是 Sub 类的一个实例 如果你对此有些迷惑 那 么可以这样理解 变量 s 是一个被强制转换为 Super 型的 Sub 类的实例 下一行(第 6 行)显示 了 s.greeting()返回的值 加上一个字符串 紧随其后的是 s.name()的返回值 关键问题 就在这里 我们调用的到底是 Super 类的方法 还是 Sub 类的方法 让我们首先判断调 用的是哪个类的 name()方法 两个类中的 name()方法都不是静态方法 而是实例方法 因为 Sub 类继承了 Super 类 而且有一个和它父类同样标识的 name()方法 所以 Sub 类中的 name() 方法覆盖了 Super 类中的 name()方法 那么前面提到的变量 s 又是 Sub 类的一个实例 这样 一来 s.name()的返回值就是 Dick 了 至此 我们解决了问题的一半 现在我们需要判断被调用的 greeting()方法究竟是 Super 类的还是 Sub 类的 需要注意的是 两个类中的 greeting()方法都是静态方法 也称为类方法 尽管事实上 Sub 类的 greeting()方法具有相同的返回类型 相同的方法名 以及相同的方法参 数 无 然而它并不覆盖 Super 类的 greeting()方法 由于变量 s 被强制转换为 Super 型 并 且 Sub 类的 greeting()方法没有覆盖 Super 类的 greeting()方法 因此 s.greeting()的返回值为 Goodnight 还是很迷惑 请记住这条规则 “实例方法被覆盖 静态方法被隐藏” 假如 你就是刚才大喊 不能覆盖静态方法 的读者之一 那么你完全正确 现在你可能会问 “隐藏和覆盖有什么区别 ” 你也许还未理解这点 然而实际上我们 刚刚在这个 Super/Sub 类的例子中已经解释了两者的不同 使用类的全局名可以访问被隐藏 的方法 即使变量 s 是 Sub 类的一个实例 而且 Sub 类的 greeting()方法隐藏了 Super 类的同 名方法 我们仍旧能够将 s 强制转换为 Super 型 以便访问被隐藏的 greeting()方法 与被隐 藏的方法不同 对被覆盖的方法而言 除了覆盖它们的类之外 其他任何类都无法访问它们 这就是为何变量 s 调用的是 Sub 类的 而非 Super 类的 name()方法 本单元简要解释了 Java 语言中一个不时引起混淆的问题 也许对你来说 理解隐藏静态 方法和覆盖实例方法的区别的最佳方式 就是自己创建几个类似于 Sub/Super 的类 再重复 一次规则 实例方法被覆盖而静态方法被隐藏 被覆盖的方法只有覆盖它们的类才能访问它 们 而访问被隐藏的方法的途径是提供该方法的全局名 现在你终于明白标题里问题的答案了吧 什么时候“被覆盖的”方法并非真地被覆盖了 呢 答案就是 “永远不会” 另外 我还有几个要点告诉大家 请谨记 Item 2: String.equals( )方法与 == 运算符的用法比较 u 5 l 试图用子类的静态方法隐藏父类中同样标识的实例方法是不合法的 编译器将会报 错 l 试图用子类的实例方法覆盖父类中同样标识的静态方法也是不合法的 编译器同样 会报错 l 静态方法和最终方法 带关键字 final 的方法 不能被覆盖 l 实例方法能够被覆盖 l 抽象方法必须在具体类1中被覆盖 Item 2: String.equals( )方法与 == 运算符的用法比较 具有 C++经验的读者们肯定会对何时使用等于运算符 = = 以及何时使用 string 类感到 困惑 这种困惑主要是 String.equals(...)方法和= =运算符的混淆 尽管它们有时产生的结果一 样 然而实际上 它们的用法是不同的 看看下面的例子 01: public class StringExample 02: { 03: public static void main (String args[]) 04: { 05: String s0 = "Programming"; 06: String s1 = new String ("Programming"); 07: String s2 = "Program" + "ming"; 08: 09: System.out.println("s0.equals(s1): " + (s0.equals(s1))); 10: System.out.println("s0.equals(s2): " + (s0.equals(s2))); 11: System.out.println("s0 == s1: " + (s0 == s1)); 12: System.out.println("s0 == s2: " + (s0 == s2)); 13: } 14: } 这个例子包含了 3 个 String 型变量 其中两个被赋值以常量表达式“Programming” 另一 个被赋值以一个新建的值为“Programming”的 String 类的实例 使用 equals(...)方法和“= =”运 算符进行比较 产生了下列结果 s0.equals(s1): true s0.equals(s2): true s0 == s1: false s0 == s2: true 1 具体类也就是抽象方法所属抽象类的非抽象子类 第 1 部分 语法 6 t String.equals()方法比较的是字符串的内容 使用 equals(...)方法 会对字符串中的所有字 符 一个接一个地进行比较 如果完全相等 那么返回 true 在这种情况下 全部 个字符 串都是相同的 所以当字符串 s0与 s1 或 s2 比较时 我们得到的返回值均为 true 运算符“==” 比较的是 String 实例的引用 在这种情况下 很明显 s0 和 s1 并不是同一个 String 实例 但 s0和 s2 却是同一个 读者也许会问 s0和 s2 怎么是同一个对象呢 这个问题的答案来自于 Java 语言规范中关于字符串常量 String Literals 的章节 本例中 “Programming” “Program” 和“ming”都是字符串常量1 它们在编译期就被确定了 当一个字符串由多个字符串常量连接 而成时 例如 s2 它同样在编译期就被确定为一个字符串常量 Java 确保一个字符串常量只 有一份拷贝 所以当“Programming”和“Program”+“ming”被确定为值相等时 Java 会设置两个 变量的引用为同一个常量的引用 在常量池 constant pool 中 Java 会跟踪所有的字符串常 量 常量池指的是在编译期被确定 并被保存在已编译的.class 文件中的一些数据 它包含了 关于方法 类 接口等等 当然还有字符串常量的信息 当 JVM 装载了这个.class 文件 变 量 s0 和 s2 被确定 JVM 执行了一项名为常量池解析 constant pool resolution 的操作 该 项操作针对字符串的处理过程 包括下列 3 个步骤 摘自 JVM 规范 5.4 节 n 如果另一个常量池入口 constant pool entry 被标记为 CONSTANT_String2 并且指 出 同样的 Unicode 字符序列已经被确定 那么这项操作的结果就是为之前的常量 池入口创建的 String 实例的引用 n 否则 如果 intern()方法已经被这个常量池描述的一个包含同样 Unicode 字符序列的 String 实例调用过了 那么这项操作的结果就是那个相同 String 实例的引用 n 否则 一个新的 String 实例会被创建 它包含了 CONSTANT_String 入口描述的 Unicode 字符序列 这个 String 实例就是该项操作的结果 也就是说 当常量池第一次确定一个字符串 在 Java 内存栈中就创建一个 String 实例 在常量池中 后来的所有针对同样字符串内容的引用 都会得到之前创建的 String 实例 当 JVM 处理到第 6 行时 它创建了字符串常量 Programming 的一份拷贝到另一个 String 实 例中 所以 对 s0 和 s1 的引用的比较结果是 false 因为它们不是同一个对象 这就是为何 s0==s1 的操作在某些情况下与 s0.equals(s1)不同 s0==s1 比较的是对象引用的值 而 s0.equals(s1)实际上执行的是字符串内容的比较 1 摘自 Java 语言规范 字符串常量由 0 个或多个包含在双引号之内的字符组成 每个字符都可以用一个换码序列 escape sequence 表示 2 它在.class 内部使用 用来识别字符串常量 Item 2: String.equals( )方法与 == 运算符的用法比较 u 7 存在于.class 文件中的常量池 在运行期被 JVM 装载 并且可以扩充 此前提到的 intern() 方法针对 String 实例的这个意图提供服务 当针对一个 String 实例调用了 intern()方法 intern() 方法遵守前面概括的第 3 步以外的常量池解析规则 因为实例已经存在 而不需要另外创建 一个新的 所以已存在的实例的引用被加入到该常量池 来看看另一个例子 01: import java.io.*; 02: 03: public class StringExample2 04: { 05: public static void main (String args[]) 06: { 07: String sFileName = "test.txt"; 08: String s0 = readStringFromFile(sFileName); 09: String s1 = readStringFromFile(sFileName); 10: 11: System.out.println("s0 == s1: " + (s0 == s1)); 12: System.out.println("s0.equals(s1): " + (s0.equals(s1))); 13: 14: s0.intern(); 15: s1.intern(); 16: 17: System.out.println("s0 == s1: " + (s0 == s1)); 18: System.out.println("s0 == s1.intern(): " + 19: (s0 == s1.intern())); 20: } 21: 22: private static String readStringFromFile (String sFileName) 23: { 24: //…read string from file… 25: } 26: } 这个例子没有设置 s0 和 s1 的值为字符串常量 取而代之的是在运行期它从一个文件中 读取字符串 并把值分配给 readStringFromFile(...)方法创建的 String 实例 从第 9 行开始 程 序对两个被新创建为具有同样字符值的 String 实例进行处理 当你看到从第 11 行到 12 行的 输出结果时 你会再次注意到 这两个对象并不是同一个 但它们的内容是相同的 输出结 果如下 第 1 部分 语法 8 t s0 == s1: false s0.equals(s1): true s0 == s1: false s0 == s1.intern(): true 第 14 行所做的是将 String 实例的引用 s0 存入常量池 当第 15 行被处理时 对 s1.intern() 方法的调用 会简单地返回引用 s0 这样一来 第 17 行和 18 行的输出结果 正是我们所期 望的 s0与 s1仍旧是截然不同的两个String 实例 因此 s0==s1的结果是 false 而 s1.intern() 返回的是常量池中的引用值 即 s0 所以表达式 s0==s1.intern()的结果是 true 假如我们希 望将实例 s1 存入常量池中 我们必须首先设置 s0 为 null 然后请求垃圾回收器 garbage collector 回收被指向 s0 的 String 实例 在 s0 被回收后 s1.intern()方法的调用 将会把 s1 存入常量池 总的来说 在执行等式比较 equality comparison 时 应该始终使用 String.equals(...)方 法 而不是==运算符 如果你还是习惯性地使用==运算符 那么 intern()方法可以帮助你得 到正确的答案 因为当 n 和 m 均为 String 实例的引用时 语句 n.equals(m)与 n.intern() == m.intern()得到的结果是一致的 假如你打算充分利用常量池的优势 那么你就应该选择 String.intern()方法 Item 3: Java 是强类型语言 每个 Java 开发人员都需要很好地理解 Java 所支持的基本数据类型 primitive type 那 么什么是陷阱呢 它们与你以前所使用的语言有什么不同呢 和大多数语言一样 Java 是强 类型的 strongly type 它支持 8 种基本数据类型 这些基本数据类型算得上是构造对象的 积木 building blocks 了 通过对这些基本数据类型用法的严格检查 Java 编译器能够 及时地在开发过程中捕捉到许多简单细微的错误 大部分的开发人员都很熟悉基本数据类型以及与它们相关的值和操作 不过 你需要了 解的是 在 Java 中仍然有一些微妙之处 与使用其它语言不同的是 Java 的基本数据类型始 终被描述在 JVM 中 因此开发人员就可以编写出不影响移植的代码 这就使得位运算得以较 安全地执行 同时 boolean 型是不可转换的 不像 C 或 C++ Java 不允许你编写在 boolean 型和非 boolean 型之间转换的代码 假如你曾经使用过上述语言 那么你可能编写过一些诸如 0 等于 false 或非 0 等于 true 的“优雅”的代码 Item 3: Java 是强类型语言 u 9 在 C 语言中 你可以像下面这样 编写代码检查一个函数的返回值 value = get_value(); if (value) do_something; 然而 类似的代码在 Java 中是不能编译的 条件语句只能接收 boolean 型的值 所以你 必须给它一个这样的值 value = getValue(); if (value != null) doSomething; 类型转换 在 Java 语言中 由于基本数据类型的转换可以隐性地发生 所以你需要理解类型转换何 时会发生 以及如何工作 非 boolean 型数据之间的转换是合理的 而且一般来说 当你的 代码可能会导致精度损失 loss of precision 时 编译器会向你发出警告 Java 的算术运算 arithmetic operation 也经常会导致与其它语言一样的潜在问题 大多 数开发人员都曾经写过导致数据意外切断 accidental truncation 的代码 举个例子 下面的 Truncation 类的第 10 行将会输出“2.0” 而不是 11 行与 12 行输出的“2.4” 01: public class Truncation 02: { 03: static void printFloat (float f) 04: { 05: System.out.println ("f = " + f); 06: } 07: 08: public static void main (String[] args) 09: { 10: printFloat (12 / 5); // data lost! 11: printFloat ((float) 12 / 5); 12: printFloat (12 / 5.0f); 13: } 14: } 因为 12 和 5 都是 integer 型 第 10 行中表达式的结果类型就是 integer 型 因此小数部 分丢失了 事实上 当 printFloat 方法并没有得到期望的 float 型参数时 切断就已经发生了 修复很简单 只要表达式中的任何一个值是 float 型 那么另一个也会被提升 promote 为 float 型 所以 第 11 行和 12 行操作正常 扩展 当基本类型的值能够在不损失数值的情况下被转换时 转换操作会自动发生 在这些情 第 1 部分 语法 10 t 况下的转换被称作 扩展 之所以这么称呼 是因为它们都会被转换成能够存储较大数据的 类型 例如 可以为 int 变量分配一个 byte 值 因为这不会引起数值或精度的损失 图 1.1 展 示了扩展转换 括号内的数字是用来存储每种类型所需的比特数 当一个类型被转换为具有 更多比特数的类型时 它不会损失任何信息 图 1.1 扩展转换 注意 假如把一个 int 型或 long 型转换成为 float 型 或者把一个 long 型转换成为 double 型 可能会损失一些精度 换句话说就是一些最不重要的位可能会丢失 这种提升是隐性发 生的 下面这个例子的输出结果 -46 显示了在编译器没有任何警告的情况下 发生的精 度损失 public class LostPrecision { public static void main (String[] args) { int orig = 1234567890; float approx = orig; int rounded = (int) approx; // lost precision System.out.println (orig - rounded); } } Java 语言规范 5.1.2 节中 对扩展转换有更详细的描述 “尽管事实上精度损失可能会发 生 但是基本类型之间的扩展转换永远不会导致运行期异常 ” 窄化 窄化 narrowing 转换 即任何不同于图 1.1 中从左到右顺序的转换 能够导致信息损 失 例如 如果将浮点型 包括 float 型和 double 型 数据转换成整型 包括 short int 和 long 型 或者冒着溢出 overflow 的危险将 long 型转换为 short 型 都会得到编译期错误 通 过加入显性的强制转换 cast 可以避免这种错误 它会告诉编译器 “我清楚我在干什么 并且愿意承担可能产生的风险” Item 3: Java 是强类型语言 u 11 隐性类型转换 顾名思义 隐性类型转换不需要显性的强制转换运算符 而是自动发生的 它仅仅发生 在扩展转换的情况下 为了方便 当变量是 byte short 或 char 型且表达式的值一定为 int 型 时 窄化转换也可以是隐性的 当然 这个表达式的值 不能超过变量类型的数值范围 例如 程序 TypeConversion 的第 7 行中的赋值可以编译通过 因为 127 可以存储为 byte 型 范围-128 到 127 但是第 8 行无法编译通过 因为 128 过大了 01: public class TypeConversion 02: { 03: static void convertArg (byte b) { } 04: 05: public static void main (String[] args) 06: { 07: byte b1 = 127; 08: // byte b2 = 128; // won't compile 09: 10: // convertArg (127); // won't compile 11: convertArg ((byte)127); 12: 13: byte c1 = b1; 14: // byte c2 = -b1; // won't compile 15: int i = ++b1; // overflow 16: System.out.println ("i = " + i); 17: } 18: } 隐性类型转换能够在 3 种情况下发生 赋值 方法调用和算术运算 赋值语句将表达式 右边的值存储到变量中 假如表达式两边的类型不同 那就需要转换类型 类似地 调用方法时 参数也有可能需要被转换类型 例如 Math.pow()方法期望的参 数是 double 型 而你可能希望使用 int 型的值 由于这是一个扩展转换 所以你不必显性地 强制转换参数 不过请注意 与赋值语句不同的是 隐性的窄化转换不支持方法调用 因此 上面例子的第 10 行不能被编译 但是第 11 行 即对参数进行显性的强制转换 可以被编译 通过 第 3 种情况被称之为算术运算 只要你使用不同类型的值进行算术运算 例如 假如 你希望将一个 int 型和一个 float 型相加求和 或者当你比较一个 short 型和一个 double 型时 其中较窄的类型总是被转换成较宽的类型 第 1 部分 语法 12 t 同理 对于大多数 但非全部 一元运算符 诸如第 14 行的一元减法运算符 来说 所有的 byte short 和 char 型数据的值 总是至少被提升为 int 型 假如你去掉第 14 行前的注 释并尝试编译这个程序 将会得到下面的错误信息 TypeConversion.java:14: possible loss of precision found : int required: byte 编译器将 b1 提升为 int 型 然后警告你 int 型的值无法赋值给 byte 变量 如此一来 你 可能预料到 也希望 第 15 行中 byte 型的值在增量操作时 会被自动提升为 int 型 那么第 16 行代码将会输出“i = 128”吧 相反 由于溢出的发生 这个输出结果将会显示一个负数 “i = -128” Item 4: 那是构造函数吗 你曾经遇到过这样的情况吗 由于一个简单的错误引起的 Bug 导致你花了一整天的时 间来寻找并解决它 这样的 Bug 的特征就在于 也许你会在一分钟内发现 或许你会因此困 扰几个小时 下列的代码就包含了其中的一种 Bug 看看你要花费多长时间解决它 01: public class IntAdder 02: { 03: private int x; 04: private int y; 05: private int z; 06: 07: public void IntAdder() 08: { 09: x = 39; 10: y = 54; 11: z = x + y; 12: } 13: 14: public void printResults() 15: { 16: System.out.println("The value of 'z' is '" + z + "'"); 17: } 18: 19: public static void main (String[] args) 20: { 21: IntAdder ia = new IntAdder(); 22: ia.printResults(); 23: } 24: } Item 4: 那是构造函数吗 u 13 这个 IntAdder 类相当简单 它由这些成员组成 3 个 private 属性分别为 x y 和 z 一个 构造函数 一个命名为 printResults 的实例方法 以及 main 方法 让我们一行一行地看看代 码 在第 21 行 我们初始化了一个名为 ia 的 IntAdder 对象 位于第 7 行的构造函数设置了 属性 x y 的值 并将它们的和赋给 z 在第 22 行 我们调用 printResults 方法 将 z 的值打 印到屏幕上 执行 IntAdder 类后的输出结果应该是 The value of 'z' is '93' 你觉得这个结果如何呢 如果你认为没有问题 那么你遗漏了那个 Bug 实际的输出应 该是 The value of 'zÉ is '0' 回头看看代码 你是否找出了问题所在呢 仍旧不明白吗 好吧 那让我们再仔细点检 查一下这些代码 第 21 行 我们初始化了 IntAdder 类的实例 ia 第 7 行的 IntAdder 类构造 函数设置了属性 x 和 y 的值 并将它们的和赋给属性 z 难道这个构造函数没有工作吗 如 果我们仔细看看第 7 行代码 就会明白它实际上是一个方法 而不是一个构造函数 刚才你 一定错误地将带有“void”返回类型的 IntAdder 方法当成了 IntAdder 类的构造函数了 这个返 回类型“void”将构造函数转变成了方法 你也许会问 如果第 7 行的 IntAdder 不是构造函数 而是方法 那么当初始化实例 ia 时调用的构造函数是什么 它又在哪里呢 因为代码中没有 构造函数 所以 Java 会给 IntAdder 类配备一个默认的无参数的构造函数 这个默认的构造函 数不用实现 它是隐藏的 但其功能就和下面的这个构造函数一致 public IntAdder() { } 因此 在第 21 行被创建的对象 ia 的属性 x y 和 z 的值均为 0 当第 22 行的 printResults 方法被调用时 我们将会看到如下的输出结果 The value of 'z' is '0'. 这个“简单”的小 Bug 还揭示了几个关键的问题 首先 我们注意到 尽管 IntAdder 类中 有一个与类同名的方法 但编译并运行它的时候 我们不会得到编译期或运行期错误 也就 是说 一个方法名与其类名相同是合法的 但是不推荐使用这样的命名 构造函数必须与类 同名 这点很容易理解 因此 惯例上 只有构造函数的名字才能和类名相同 在你写的代 码中 假如有和类同名的方法 那就很可能迷惑和你一起工作的程序员们 从另一个角度来 说 将方法命名为与类名一致 同样违背了命名规范 方法名一般应该为动词或者动词短语 并且首字母应该小写 后续单词的第一个字母要大写 类名一般应该为名词或者名词短语 并且每个单词的首字母都要大写 关于命名规范 详情可以参阅 Java 语言规范的 6.8 节 在 第 1 部分 语法 14 t 这个例子中 我们注意到的第二个关键问题就是 在一个类中假如没有显式的构造函数存在 那么 Java 会自动为其配备一个默认的无参数的构造函数 而且这个构造函数是空的 这种情 况当且仅当类中没有声明任何一个构造函数时才会发生 假如类中已经存在了一个带任意参 数的构造函数 那么 Java 就不会再自动提供默认的无参数的构造函数了 Item 5: 不能访问被覆盖的方法 设想一下 假如你正在维护一个整合了第三方 Java 文本编辑器的应用程序 而这个编辑 器支持 RTF 文件 且具有语法和文法的检查功能 在这个应用程序自己的代码中 你可以创 建新的文档 或者通过访问编辑器的 DocumentManager 对象来打开新的文档 每次这些方法 之一被调用时 一个 Document 对象被返回 这个 Document 对象提供了实例方法以便进行拼 写检查 文法检查等等 假如有一天 你接到了客户的电话 他告诉你这个应用程序中的文 本编辑器需要支持 HTML 文件的显示和编辑 你觉得实现这个需求没什么问题 因为这个第 三方厂商向你提供了刚刚升级到支持 HTML 显示和编辑的文本编辑器 而且 该厂商保证新 版本的编辑器能够向下兼容 因为所有新加入的特性都被放置在 Document 类的一个名为 HTMLDocument 的子类中 而 Document 类的代码并未改动 无需修改 新版本的编辑器能 够立即整合到应用程序中 而且当需要的 Document 对象被 DocumentManager 类返回后 可 以通过强制类型它为 HTMLDocument 对象 以便使用 HTMLDocument 类的新特性 为了取 悦你的客户 你拿着新版本的编辑器以及一些利用新的 HTML 特性的代码 告诉你的客户 这个新功能将在一个月内实现 正好够时间进行质量评估 然而 当你还自我感觉良好时 你会很吃惊地看见从质量评估中得出的一份报告 上面 指出了关于拼写检查程序的一个问题 你其实知道 HTMLDocument 类中的拼写检查功能是有 问题的 但是你自认为是一个聪明的 Java 程序员 相信自己能够“欺骗”HTMLDocument 类 让它使用 Doucment 类的 spellCheck()方法 毕竟 厂商已经声明了 Document 类的代码未被 改动 那么 你就尝试着总去调用 Document 类的 spellCheck()方法 不论是将 HTMLDocument 对象强制转换为 Document 对象 还是声明一个局部 Document 类型的变量并赋值为传入的 Document 对象 亦或是使用反射来访问 Document 类的 spellCheck()方法 所有的结果都将是 失败的 在经过许多次失败的调用后 厂商的技术支持也明确地告诉你 Document 类没有被 修改过 你决定去查阅一下 Java 语言规范 并最终在 8.4.6.1 章节中看到了下面这段话 Item 5: 不能访问被覆盖的方法 u 15 “可以通过包含关键字 super 的方法调用表达式来访问被覆盖的方法 注意 尝试用全局 名或强制转换为父类型 以求访问一个被覆盖的方法 都是无效的 ” 最终 一切都清楚了 当你从 DocumentManager 类中得到一个文档时 这第三方文本编 辑器的类库总是创建一个 HTMLDocument 型的对象 无论你尝试着对这个 HTMLDocument 对象做什么 强制转换 反射等等 你调用的永远是 HTMLDocument 类的 spellCheck()方法 而不是所期望的 Document 类的 spellCheck()方法 实际上 当你创建了一个覆盖了父类实例 方法的子类时 访问那些被覆盖方法的唯一途径 就是使用 super 关键字 任何使用子类的 外部类 永远不能调用父类的这些被覆盖的实例方法 下面的代码例证了这个概念 01: class DocumentManager 02: { 03: public Document newDocument() 04: { 05: return (new HTMLDocument()); 06: } 07: } 08: 09: class Document 10: { 11: public boolean spellCheck() 12: { 13: return (true); 14: } 15: } 16: 17: class HTMLDocument extends Document 18: { 19: public boolean spellCheck() 20: { 21: System.out.println("Trouble checking these darn hyperlinks!"); 22: return (false); 23: } 24: } 25: 26: public class OverridingInstanceApp 27: { 28: public static void main (String args[]) 29: { 30: DocumentManager dm = new DocumentManager(); 第 1 部分 语法 16 t 31: Document d = dm.newDocument(); 32: boolean spellCheckSuccessful = d.spellCheck(); 33: if (spellCheckSuccessful) 34: System.out.println("No spelling errors where found."); 35: else 36: System.out.println("Document has spelling errors."); 37: } 38: } 在第 32 行中 我们尝试调用了一个“明显”是 Document 对象的 spellCheck()方法 然而 它实际上是一个 HTMLDocument 类的实例 所以这个例子的输出结果是这样的 Trouble checking these darn hyperlinks! Document has spelling errors. 需要注意的是 无法访问父类中被子类覆盖的方法的原则 仅仅适用于实例方法 也就 是非静态方法 即使父类中的静态方法被子类“覆盖” 并非真的覆盖 而是子类也具有与父 类中同名的静态方法 参见 Item1 了 它们仍旧能被访问 强制转换为父类的类型就能达 到这个目的 将第 11 行和第 19 行的代码替换为下面这行 我们就将 spellCheck()方法设置为 静态的了 public static boolean spellCheck(Document d) 然后再将第 32 行代码替换为下面这行 那我们就可以调用改静态方法了 boolean spellCheckSuccessful = d.spellCheck(d); 最后执行修改后的程序 输出结果如下 No spelling errors where found. 你也许会质疑这里所举的例子是否真实地发生过 确实 这个例子是虚构的 但是类似 这样的案例确实是存在的 通常来说 任何支持旧的对象 也就是父类 的类仍然认为它们 得到的是旧类的实例 但是实际上它们得到的是新类 也就是具有新功能的子类 的实例 关于这点 JavaSoft 有一个不错的例子 当它的教材谈及 Java 2D 技术中的 Graphics2D 对象 时介绍到 在旧版本的 JDK 中 所有传递 Graphics 对象的 AWT 方法 例如 paint(...)和 update(...) 它们在新版的 JDK 中传递的都是 Graphics2D 对象 也就是说 Graphics2D 类覆 盖了 Graphics 类中的一些实例方法 例如 draw3DRect() 假设在 Graphics2D 类中的 draw3DRect()方法有一个 Bug 但是 Graphics 类的 draw3DRect()方法工作正常 那么正如你 的判断 你使用的 draw3DRect()方法将是 Graphics2D 类提供的 Item 6: 避免落入 隐藏变量成员 的陷阱 u 17 尽管在子类的外部无法访问父类中被覆盖的实例方法 但你也可以很容易意识到这将会 是一个产生错误的潜在根源 假如你怀疑你所使用的某个对象实际上是一个子类的实例 那 么你可以调用它的 getClass().getName()方法来判断它的真实身份 又如果你是编写增加了新 功能的子类的程序员 那你必须确保进行兼容性测试 或者保证在编写程序时 任何新增的 功能都是通过增加新的方法 而不是覆盖父类方法实现的 Item 6: 避免落入 隐藏变量成员 的陷阱 在 Java 语言中 与理解方法是如何被覆盖同等重要的就是 理解变量成员是如何被隐藏 的 假如你认为自己已经理解了方法是如何被覆盖的 依此类推变量成员是如何被隐藏的也 是同样道理的话 那么你最好仔细地读读本节 在程序中 无意地隐藏了一个变量成员或者 错误地认为已经“覆盖”了一个变量成员 都会导致错误的结果 01: public class Wealthy 02: { 03: public String answer = "Yes!"; 04: public void wantMoney() 05: { 06: System.out.println("Would you like $1,000,000? > "+ answer); 07: } 08: public static void main(String[] args) 09: { 10: Wealthy w = new Wealthy(); 11: w.wantMoney(); 12: } 13: } 输出结果为 Would you like $1,000,000? > Yes! 在上例中 Wealthy 类具有一个名为 answer 的实例变量 一个名为 wantMoney 的方法 以及一个 main 方法 在 main 方法中 一个 Wealthy 类的实例 w 被创建 w 调用它自己的 wantMoney 方法 输出了一个问题以及作为回答的实例变量 answer 的值 上例正确地回答了 问题 现在让我们来看看一个没有正确回答这个问题的例子 第 1 部分 语法 18 t 01: public class Poor 02: { 03: public String answer = "Yes!"; 04: public void wantMoney() 05: { 06: String answer = "No!"; // hides instance variable answer 07: System.out.println("Would you like $1,000,000? > " + answer); 08: } 09: public static void main(String[] args) 10: { 11: Poor p = new Poor(); 12: p.wantMoney(); 13: } 14: } 输出结果为 Would you like $1,000,000? > No! 注意 本例输出中的回答已经变成了“No ” 局部变量 answer 隐藏了实例变量 answer 因此 回答的结果就是局部变量的值 这个例子很简单 也显而易见 局部变量 answer 隐藏 了实例变量 answer 产生了意想不到的结果 然而 在许多复杂的环境下 一个变量成员由 于意外而被覆盖所引起的问题可能就很难被发现了 为了避免“数据隐藏”所带来的问题 理 解下面的知识点就显得非常重要了 l 不同类型的 Java 变量 l 变量的作用范围 l 何种变量能被隐藏 l 变量是如何被隐藏的 l 如何访问被隐藏的变量 l 变量隐藏与方法覆盖的区别 不同类型的 Java 变量 Java 一共有 6 种变量类型 类变量 实例变量 方法参数 构造函数参数 异常处理器 参数 以及局部变量 类变量包括在类体中声明的静态数据成员以及在接口体中声明的静态 或非静态数据成员 实例变量是在类体中声明的非静态变量 术语“变量成员”指的是类变量 和实例变量 方法参数是用来传入一个方法体的 构造函数参数是用来传入一个构造函数体 Item 6: 避免落入 隐藏变量成员 的陷阱 u 19 的 异常处理器参数是用来传入一个 try 语句的 catch 块中的 最后 局部变量是在一个代码 块中或一个 for 语句中声明的变量 下面的例子声明了各种类型的变量 01: public class Types 02: { 03: int x; // instance variable 04: static int y; // class variable 05: public Types(String s) // s is a constructor parameter 06: { 07: // constructor code f. 08: } 09: public createURL(String urlString) //urlString is a method parameter 10: { 11: String name = "example"; // name is a local variable 12: try 13: { 14: URL url = new URL(urlString); 15: } 16: catch(Exception e) // e is a exception-handler parameter 17: { 18: // handle exception 19: } 20: } 21: } 变量的作用范围 变量的作用范围指的其实是一个代码块 在这个代码块中 可以通过该变量的简名来引 用它 简名是一个变量的专一标识符 在上例的第 3 行中 实例变量 x 的简名就是“x” 实例 变量和类变量的作用范围就是声明它们的类或接口的整体 变量成员 x 和 y 的作用范围就是 Types 类的类体 方法参数的作用范围就是整个方法体 构造函数参数的作用范围就是整个 构造函数体 异常处理器参数的作用范围就是 catch 语句块 局部变量的作用范围就是它被 声明的所在代码块 上例的局部变量 name 它在第 11 行也就是 createURL 方法中被声明 那么它的作用范围就是 createURL 方法的整个方法体 第 1 部分 语法 20 t 何种变量能被隐藏 实例变量和类变量能被隐藏 局部变量和各种参数永远不会被隐藏 假如用一个同名的 局部变量去隐藏一个参数 编译器将会报错 同样地 用一个同名的局部变量去隐藏另一个 局部变量 编译器也会报错 01: class Hidden 02: { 03: public static void main(String[] args) 04: { 05: int args = 0; // illegal – results in a compiler error 06: String s = "string"; 07: int s = 10; // illegal – results in a compiler error 08: } 09: } 上例中 第 5 行的局部变量不能和方法参数 args 同名 同样 第 7 行的局部变量 s 也会 引起编译器错误 因为它不能和另一个同名的局部变量存在于相同的作用范围内 实例变量和类变量如何被隐藏 同名的局部变量或者同名的参数 可以隐藏掉变量成员的一部分作用范围 变量成员也 能被子类的同名变量成员隐藏 与一个变量成员同名的局部变量 将在其作用范围内 隐藏 掉这个变量成员 与一个变量成员同名的方法参数 将在方法体中隐藏掉这个变量成员 与 一个变量成员同名的构造函数参数 将在构造函数体中隐藏掉这个变量成员 依此类推 与 一个变量成员同名的异常处理器参数 将在 catch 语句块中隐藏掉这个变量成员 01: public class Bike 02: { 03: String type; 04: public Bike(String type) 05: { 06: System.out.println("type =" + type); 07: } 08: } Item 6: 避免落入 隐藏变量成员 的陷阱 u 21 上例中 构造函数参数 type 隐藏了实例变量 type System.out.println 方法输出的 type 变 量的值将是构造函数参数 type 的值 而不是实例变量 type 的值 子类的变量成员将会隐藏掉父类中同名的变量成员 01: public class Bike 02: { 03: String type = "generic"; 04: } 01: public class MountainBike extends Bike 02: { 03: String type = "All terrain"; 04: } 上例中 Bike 类中的实例变量 type 被子类 MoutainByte 的实例变量 type 隐藏了 子类 的类变量将会隐藏父类中与之同名的类变量和实例变量 同理 子类的实例变量也会隐藏父 类中与之同名的类变量和实例变量 01: public interface Stretchable 02: { 03: int y 04: } 01: public class Line 02: { 03: int x; 04: } 01: public class MultiLine extends Line implements Stretchable 02: { 03: public MultiLine() 04: { 05: System.out.println("x = " + x); 06: } 07: } 上例的程序可以编译通过 假如向 Strectchable 接口中加入一个名为 x 的变量 那么 MultiLine 类将无法通过编译 因为它尝试按简名去引用一个被多重继承的变量 x 第 1 部分 语法 22 t 如何访问被隐藏的变量 通过全局名 可以访问大多数的变量成员 关键字“this”可以限定一个正被局部变量隐藏 的实例变量 关键字“super”可以限定一个正被子类隐藏的实例变量 类变量也可以被限定 只要在该类的名字与该类变量的名字之间加上“.”即可 01: public class Wealthy 02: { 03: public String answer = "Yes!"; 04: public void wantMoney() 05: { 06: String answer = "No!"; 07: System.out.println("Do you want to give me $1,000,000? > " + 08: answer); 09: System.out.println("Would you like $1,000,000? > " + 10: this.answer); 11: } 12: public static void main(String[] args) 13: { 14: Wealthy w = new Wealthy(); 15: w.wantMoney(); 16: } 17: } 输出结果为 Do you want to give me $1,000,000 > No! Would you like $1,000,000? > Yes! 上例中 Wealthy 类具有一个名为 answer 的实例变量 wantMoney 方法也声明了一个名 为 answer 的局部变量 为了对 wantMoney 方法中的每个问题都给出正确的回答 我们需要 访问局部变量 answer 也需要访问实例变量 answer 通过使用关键字“this” 我们告知编译器 我们需要的是实例变量 answer 而非局部变量 answer 正如输出结果所显示的 第一个问题 的回答是局部变量 answer 的值 第二个问题的回答是实例变量 answer 的值 它被关键字“this” 限定了 下例显示了如何限定父类中被隐藏的一个实例变量 01: public class StillWealthy extends Wealthy 02: { 03: public String answer = "No!"; Item 6: 避免落入 隐藏变量成员 的陷阱 u 23 04: public void wantMoney() 05: { 06: String answer = "maybe?"; 07: System.out.println("Did you see that henway? > " + answer); 08: System.out.println("Do you want to give me $1,000,000? > " + 09: this.answer); 10: System.out.println("Would you like $1,000,000? > " + super.answer); 11: } 12: public static void main(String[] args) 13: { 14: Wealthy w = new Wealthy(); 15: w.wantMoney(); 16: } 17: } 输出结果为 Did you see that henway? > maybe? Do you want to give me $1,000,000 > No! Would you like $1,000,000? > Yes! 注意 上例中第 7 行问题的回答 输出的是局部变量 answer 的值 第 8 行问题的回答 输出的是 StillWealthy 子类的实例变量 它被关键字“this”限定了 第 10 行问题的回答 输出 的是父类 Wealthy 的实例变量 它被关键字“super”限定了 变量隐藏与方法覆盖的区别 隐藏变量和覆盖方法有许多区别 也许它们之间最重要的不同就是 一个类的实例 无 法通过使用全局名 或者强制转换自己为其父类的类型 以访问其父类中被覆盖的方法 01: public class Wealthier extends Wealthy 02: { 03: public void wantMoney() 04: { 05: System.out.println("Would you like $2,000,000? > " + answer); 06: } 07: public static void main(String[] args) 08: { 09: Wealthier w = new Wealthier(); 10: w.wantMoney(); 11: ((Wealthy)w).wantMoney(); 12: } 13: } 第 1 部分 语法 24 t 输出结果为 Would you like $2,000,000? > Yes! Would you like $2,000,000? > Yes! 上例中 Wealthier 类继承了 Wealthy 类 并且覆盖了 wantMoney 方法 main 方法创建了 Wealthier 类的一个实例 w 并调用它的 wantMoney()方法 注意 它的结果就是上例输出的 第一行 接着 main 方法又强制将实例 w 转换为它父类的类型 Wealthy 并再次调用它的 wantMoney()方法 它的结果仍旧和之前一样 上面的例子说明 通过强制转换子类的实例为 父类类型 是无法访问父类中被覆盖的方法的 而下面的例子显示了 一个被隐藏的变量与 一个被覆盖的方法的区别 也就是说 强制转换子类的实例为父类类型后 我们就可以访问 父类中被隐藏的变量了 01: public class Poorer extends Wealthier 02: { 03: String answer = "No!"; 04: public void wantMoney() 05: { 06: System.out.println("Would you like $3,000,000? > " + answer); 07: } 08: public static void main(String[] args) 09: { 10: Poorer p = new Poorer(); 11: ((Wealthier)p).wantMoney(); 12: System.out.println("Are you sure? > " + ((Wealthier)p).answer); 13: } 14: } 输出结果为 Do you want $3,000,000? ? No! Are you sure? > Yes! Poorer 类继承了 Wealthier 类 main 方法创建了一个 Poorer 类的实例 p 然后强制转换 p 为其父类类型 正如在前一个例子中解释的 由于 wantMoney 方法被覆盖 通过强制转换 是不能访问父类的 wantMoney 方法的 因此 上例第 11 行调用的仍旧是子类 Poorer 的 wantMoney 方法 它的回答是“No!” 接着 main 方法又问到 “Are you sure?>” 这个问题 答案就不再是子类变量的值 而是父类中被隐藏变量的值了 这种情况的出现 是由于子类 Item 7: 提前引用 u 25 仅仅“隐藏”了父类的变量成员 所以 只要将子类实例强制转换为父类类型 我们就可以访 问父类中被隐藏的变量成员了 数据隐藏与方法覆盖的另外一个不同 就是静态方法不能覆 盖父类的实例方法 而静态变量 却可以隐藏父类的一个同名实例变量 相同地 实例方法 也不能覆盖父类的同名静态方法 而变量成员却可以隐藏父类同名变量成员 不论父类的这 个同名变量成员是类变量还是实例变量 通过理解本节讨论的知识点 避免“隐藏变量”的陷阱 会帮助应用程序得到你所期望的 结果 并且为你节省花费在复杂程序调试上的大量时间 Item 7: 提前引用 类变量以及静态初始化块是在类被加载进 JVM 时执行初始化操作的 Java 语言规范 8.5 节指出 “静态初始化块和类变量是按照其在代码中出现的顺序依次执行初始化操作的 而不 能在类变量声明出现之前就引用它” 换句话说 这些语句被处理的顺序就是它们在代码中出 现的顺序 一般来说 编译器会捕捉到任何的提前引用 看看下面的代码 1: public class ForwardReference 2: { 3: int first = second; // this will fail to compile 4: int second = 2; 5: } 尝试去编译这个类 将会得到一个如下的错误 ForwardReference.java:3: Can't make forward reference to second in class ForwardReference. 所以说 即使变量 first 和 second 都处在同一个作用范围内 Java 语言规范也不允许这种 类型的无效初始化 而且编译器会捕捉到这个错误 可是 绕开这个保护措施还是有可能的 Java 允许方法调用出现在类变量的初始化之前 而且方法内部对类变量的访问不会按照这个原则被检查 下面的程序将会编译通过 01: public class ForwardReferenceViaMethod 02: { 03: static int first = accessTooSoon(); 04: static int second = 1; 05: 06: static int accessTooSoon() 07: { 第 1 部分 语法 26 t 08: return (second); 09: } 10: 11: public static void main (String[] args) 12: { 13: System.out.println ("first = " + first); 14: } 15: } 然而 运行程序 由于在初始化 second 之前 accessTooSoon 方法就访问了它 那么方 法得到的是 second 的默认值 即 0 因此 输出结果 first 的值为 0 而不是 1 问题并非这么简单 假如你使用方法调用来初始化静态变量 那么你必须保证 这些方 法并不依赖于在它们之后声明的其它静态变量 Item 8: 设计可继承的构造函数 包括 Java 在内 大多数面向对象语言的最重要的优点就在于 代码容易被复用 复用一 个类的最通常的做法之一 就是继承它 然后增加或修改功能以适应需求 不幸的是 许多 缺乏经验的开发人员不会编写容易继承的代码 软件开发过程中 有许多情况使得你必须在多个目标之间权衡得失 举例来说 在程序 优化时 你经常会发现代码是如此复杂且难以维护 然而 当程序原本就比较容易扩展时 你再要增强扩展性的话 一般来说代价就要小得多了 在开发类时 你可能会遭遇到许多阻碍甚至是抵触扩展性的陷阱 假如你了解这些陷阱 那么当你设计类以及编写代码时 就可以很轻松地避开它们 我所见过的最常见的陷阱之一 就是构造函数的实现问题 不论你的方法设计得多么完 美 假如你没有提供正确的构造函数 其它开发人员在继承你的类时 就会遇到麻烦 因为 构造函数是不能被覆盖的 所以无论父类的构造函数提供了什么样的实现 子类都必须承受 它 如果一个类的构造函数做了太多的工作 那么它的任何子类都一定会做同样的工作 尽 管这些子类可能并不需要这么做 在编写子类的开发人员无权访问父类的源代码时 这种情 况尤其突出 举例来说 看看下面的提供了一个简单的多选项菜单对象的例子 01: import java.awt.*; 02: import java.awt.event.*; 03: import java.io.*; 04: import java.util.*; Item 8: 设计可继承的构造函数 u 27 05: 06: import javax.swing.*; 07: import javax.swing.event.*; 08: 09: public class ListDialog extends JDialog 10: implements ActionListener, ListSelectionListener 11: { 12: JList model; 13: JButton selectButton; 14: LDListener listener; 15: Object[] selections; 16: 17: public ListDialog (String title, 18: String[] items, 19: LDListener listener) 20: { 21: super ((Frame)null, title); 22: 23: JPanel buttonPane = new JPanel (); 24: selectButton = new JButton ("SELECT"); 25: selectButton.addActionListener (this); 26: selectButton.setEnabled (false);//nothing selected yet 27: buttonPane.add (selectButton); 28: 29: JButton cancelButton = new JButton ("CANCEL"); 30: cancelButton.addActionListener (this); 31: buttonPane.add (cancelButton); 32: 33: this.getContentPane().add (buttonPane, BorderLayout.SOUTH); 34: 35: this.listener = listener; 36: setModel (items); 37: } 38: 39: void setModel (String[] items) 40: { 41: if (this.model != null) 42: this.model.removeListSelectionListener (this); 43: this.model = new JList (items); 44: model.addListSelectionListener (this); 45: 46: JScrollPane scroll = new JScrollPane (model); 47: this.getContentPane().add (scroll, BorderLayout.CENTER); 48: this.pack(); 49: } 50: 51: /** Implement ListSelectionListener. Track user selections. */ 52: 53: public void valueChanged (ListSelectionEvent e) 54: { 55: selections = model.getSelectedValues(); 第 1 部分 语法 28 t 56: if (selections.length > 0) 57: selectButton.setEnabled (true); 58: } 59: 60: /** Implement ActionListener. Called when the user picks the 61: * SELECT or CANCEL button. Generates the LDEvent. */ 62: 63: public void actionPerformed (ActionEvent e) 64: { 65: this.setVisible (false); 66: String buttonLabel = e.getActionCommand(); 67: if (buttonLabel.equals ("CANCEL")) 68: selections = null; 69: if (listener != null) 70: { 71: LDEvent lde = new LDEvent (this, selections); 72: listener.listDialogSelection (lde); 73: } 74: } 75: 76: public static void main (String[] args) // self-testing code 77: { 78: String[] items = (new String[] 79: {"Forest", "Island", "Mountain", "Plains", "Swamp"}); 80: LDListener listener = 81: new LDListener() 82: { 83: public void listDialogSelection (LDEvent e) 84: { 85: Object[] selected = e.getSelection(); 86: if (selected != null) // null if user cancels 87: for (int i = 0; i < selected.length; i++) 88: System.out.println (selected[i] .toString()); 89: System.exit (0); 90: } 91: }; 92: 93: ListDialog dialog = 94: new ListDialog ("ListDialog", items, listener); 95: dialog.show(); 96: } 97: } 出于程序完整性的考虑 下面是 LDListener 和 LDEvent 类 01: public interface LDListener 02: { 03: public void listDialogSelection (LDEvent e); 04: } 01: import java.util.EventObject; Item 8: 设计可继承的构造函数 u 29 02: 03: public class LDEvent extends java.util.EventObject 04: { 05: Object source; 06: Object[] selections; 07: 08: public LDEvent (Object source, Object[] selections) 09: { 10: super (source); 11: this.selections = selections; 12: } 13: 14: public Object[] getSelection() 15: { 16: return (selections); 17: } 18: } ListDialog 类看上去写得相当不错 但是这样的写法使得继承它变得非常困难 假设你 需要开发一个向用户显示一列声音文件的菜单 你希望用户在点击这些文件时 能够听到相 应的声音 由于你不愿意强制客户端去指定所有的声音文件 所以你决定提供一个简单的 API 它接受一个路径名称参数 并测定该路径下的声音文件列表 你觉得似乎通过简单地继承 ListDialog 类 就能够实现这个目的了 你明白你需要监听 ListSelectionEvents 以便播放用户选择的声音 这个非常简单 你可以调用上例第 44 行显示 的 addListSelectionListener(...)方法来解决 假如 model 被声明为 private 而且没有任何访问 它的方法 那么你就不能加载监听器 而且你恐怕要从头开始做起了 不过到目前为止 一 切都还不错 然而 当你尝试去继承 ListDialog 类 它唯一的构造函数 上例第一部分的第 17 行 需 要你传入一个 String 型数组 然而你并没有这个资源 而且 由于构造函数第一件要做的事 就是调用 super(...)语句 因此你无法得到列表的选项并创建 ListDialog 问题出现了 但你也不要灰心丧气 去翻翻 Javadoc 文档 你会注意到一个 setModel(...) 方法 它似乎可以提供帮助 好吧 你首先在子类构造函数中试着实例化一个 items 参数为 空的 ListDialog 类 然后在确定文件列表之后 调用 setModel(...)方法 01: public SoundDialog (String title, LDListener listener, 02: String path) 03: { 04: super (title, null, listener); 第 1 部分 语法 30 t 05: String[] items = getItems (path); 06: setModel (items); 07: model.addListSelectionListener (this); 08: model.setSelectionMode (ListSelectionModel.SINGLE_SELECTION); 09: } 这个解决方法看上去合情合理 但是当你运行这个类时 将会得到下面的错误 Exception occurred during event dispatching: java.lang.NullPointerException . . . at java.awt.Window.pack(Window.java:259) at ListDialog.setModel(ListDialog.java:48) at ListDialog.(ListDialog.java:36) at SoundDialog.(SoundDialog.java:18) at SoundDialog.main(SoundDialog.java:89) 通过检查异常堆栈 你找到了错误的关键 ListDialog 类在它的构造函数中调用自己的 setModel(...)方法时出现了问题 导致了意想不到的结果 经过一些详细的调试后 你终于判 定 NullPointerException 是由于空的 model 尝试调用一个没有 item 的 JList 对象的 pack() 方法 引起的 那么现在该如何是好呢 还好 你还算幸运的 ListDialog 类的 setModel(...)方法不是 private 的 因此你可以用自定义的同名方法覆盖掉它 下面就是最终可以正常工作的版本 01: import java.applet.*; 02: import java.awt.*; 03: import java.io.*; 04: import java.net.*; 05: 06: import javax.swing.*; 07: import javax.swing.event.*; 08: 09: public class SoundDialog extends ListDialog 10: implements FilenameFilter, ListSelectionListener 11: { 12: String selection; 13: 14: public SoundDialog (String title, LDListener ldl, String path) 15: { 16: super (title, null, ldl); 17: String[] items = getItems (path); 18: setModel (items); 19: } Item 8: 设计可继承的构造函数 u 31 20: 21: public void setModel (String[] items) 22: { 23: if (items != null) 24: { 25: super.setModel (items); 26: model.addListSelectionListener (this); 27: model.setSelectionMode 28: (ListSelectionModel.SINGLE_SELECTION); 29: } 30: } 31: 32: public String[] getItems (String path) 33: { 34: File file = new File (path); 35: File soundFiles[] = file.listFiles (this); 36: String[] items = new String [soundFiles.length]; 37: for (int i = 0; i < soundFiles.length; i++) 38: items = soundFiles.getName(); 39: return (items); 40: } 41: 42: // implement FilenameFilter 43: public boolean accept (File dir, String name) 44: { 45: return (name.endsWith (".aiff") || 46: name.endsWith (".au") || 47: name.endsWith (".midi") || 48: name.endsWith (".rmf") || 49: name.endsWith (".wav")); 50: } 51: 52: // implement ListSelectionListener 53: public void valueChanged (ListSelectionEvent e) 54: { 55: super.valueChanged (e); 6: JList items = (JList) e.getSource(); 57: String fileName = items.getSelectedValue().toString(); 58: if (!fileName.equals (selection)) 59: { 60: selection = fileName; 61: play (selection); 62: } 63: } 64: 65: private void play (String fileName) 66: { 67: try 68: { 69: File file = new File (fileName); 70: URL url = new URL ("file://" + file.getAbsolutePath()); 第 1 部分 语法 32 t 71: AudioClip audioClip = Applet.newAudioClip (url); 72: if (audioClip != null) 73: audioClip.play(); 74: } 75: catch (MalformedURLException e) 76: { 77: System.err.println (e + ": " + e.getMessage()); 78: } 79: } 80: 81: public static void main (String[] args) // self-test 82: { 83: LDListener listener = 84: new LDListener() 85: { 86: public void listDialogSelection (LDEvent e) 87: { 88: Object[] selected = e.getSelection(); 89: if (selected != null) // null if user cancels 90: for (int i = 0; i < selected.length; i++) 91: System.out.println (selected[i].toString()); 92: System.exit (0); 93: } 94: }; 95: SoundDialog dialog = 96: new SoundDialog ("SoundDialog", listener, "."); 97: dialog.show(); 98: } 99: } 回味一下 假如原先版本的 ListDialog 提供的是适当的构造函数 那么上述这些工作都 是不需要的 如果你发现自己实现的一个构造函数 调用了一个 private 方法 或者需要许多 的参数 请确信你这么做是必要的 带有大量参数的构造函数可能是适当的 因为它也许会 为其它不太复杂的构造函数提供一个便利的实现 但是假如它是唯一的构造函数 那么你就 要考虑增加额外版本的构造函数 以适应需求的变化了 如果你能够提供一个无参数的构造函数 再配备其它正确的额外方法来实例化你的类 那就再好不过了 这种做法需要一定的额外工作 例如你必须仔细地检查其它方法没有使用 仍未被实例化的变量 假如构造函数不具备足够的信息以便实例化类 那么你就应该考虑限制访问它了 你可 以去掉关键字“public” 当然 这样仍旧允许该类的任何子类调用它 但是你可以不用担心其 它开发人员使用它创建一些无效的对象了 Item 9: 通过引用传递基本类型 u 33 需要注意的是 正如大部分 Swing 组件一样 上例中我们使用的 JList 类确实提供了一 个无参数的构造函数 即使一个空的 JList 对象实际上是无效的 假如你用的是这个构造函数 那么在使用 JList 对象之前 就一定要调用 setListData(...)或 setModel(...)方法 显而易见 Swing 组件是被设计成比较容易继承的 Item 9: 通过引用传递基本类型 假如你曾经是一名 C 或 C++程序员 那么你可能对 Java 感到有些失望 因为它没有指 针的概念 然而 Java 语言没有引入指针 至少预防了两件你通常会利用指针做的事情 指 针运算 pointer arithmetic 和从一个函数中返回多个值 事实上 在 Java 中 实际上只有第 一事是决不允许的 至于第二件事 从一个 Java 方法中返回多个值 在通过引用而不是值来 传递参数的情况下 是有可能的 Java 程序员都是利用引用来访问所有被实例化的对象的 类型为类 接口 数组以及对象的变量 都归为引用类型 Java 语言还有一种名为基本数据 类型的变量类别 基本数据类型被用来存储特定类别的信息 诸如数字或字符等等 Java 语 言一共提供了这些基本数据类型 boolean 型 byte 型 short 型 int 型 long 型 char 型 float 型以及 double 型 一个需要理解的重要概念是 基本数据类型不是对象 因此 它们不 能通过引用来传递 看到这 你一定非常迷惑了吧 假如基本数据类型不能通过引用来传递的话 本节的重 点到底是什么呢 而且 为什么会出现无论如何都要通过引用来传递基本数据类型的情况 呢 虽然基本数据类型不能直接通过引用来传递 但是可以间接地实现这个目的 这也就意 味着 如果你希望通过引用来传递基本数据类型的话 必须使用引用类型来封装基本数据类 型 在这里 我马上就能想到两种需要通过引用来传递基本类型的情况 l 从方法中返回多个值 l 传递基本类型数据到只接受对象参数的方法 例如 Hashtable 让我们看看一个例子 它展示了完成这两个任务的错误做法 注意下面列出的 PassPrimitiveByReference1 类的源代码 这个类试图实现上面列出的两个目标 但是你可以发 现其中有两个明显的问题 第一个问题就是程序根本无法编译通过 因为这个类尝试传递基 本类型到 Hashtable.put()方法 该方法只接受对象类型的参数 第二个问题出现在 getPersonInfo()方法的代码中 该方法的意图是希望通过赋值给方法参数 以便返回多个值给 第 1 部分 语法 34 t 方法调用者 然而 Java 中基本数据类型的传递 只能是“按值传递” 这也就意味着当基本 数据类型被传递给方法时 该方法使用的仅仅是该基本类型数据的一个副本 当 getPersonInfo()方法为这些方法参数分配新的值时 这样做所改变的仅仅是对该方法有效的变 量副本 而不能改变原来的变量 01: import java.util.Hashtable; 02: 03: public class PassPrimitiveByReference1 04: { 05: public static void main (String args[]) 06: { 07: String name = null; 08: int age = 0; 09: float weight = 0f; 10: boolean isMarried = false; 11: 12: getPersonInfo(name, age, weight, isMarried); 13: 14: System.out.println("Name: " + name + 15: "\nAge: " + age + 16: "\nWeight: " + weight + 17: "\nIs Married: " + isMarried); 18: 19: storePersonInfo(name, age, weight, isMarried); 20: } 21: 22: private static void getPersonInfo(String name, int age, 23: float weight, boolean isMarried) 24: { 25: name = "Robert Smith"; 26: age = 26; 27: weight = 182.7f; 28: isMarried = true; 29: } 30: 31: private static void storePersonInfo (String name, int age, 32: float weight, boolean isMarried) 33: { 34: Hashtable h = new Hashtable(); 35: h.put("name", name); 36: h.put("age", age); // produces compile time error 37: h.put("weight", weight); // produces compile time error 38: h.put("isMarried", isMarried); // produces compile time error 39: } 40: } 现在 让我们开始关注这些问题的正确解决方法吧 下面列出了 PassPrimitiveByReference2 类的源代码 它解决了 PassPrimitiveByReference1 类的问题 Java Item 9: 通过引用传递基本类型 u 35 语言中的封装类解决了 Hashtable.put()方法的问题 在 java.lang 包中 你会发现针对所有基 本数据类型的相关封装类 其中在下例里出现的就有针对 int 的 Integer 类 针对 float 的 Float 类 以及针对 boolean 的 Boolean 类 这些封装类顾名思义 都是用来封装基本数据类型的 而且它们都提供有各种各样的有效方法 同样道理 封装类也可以是上例中第二个问题的解 决方法 但是要注意到 这些封装类是“不可变的” 它们不具备类似 set(...)这样的方法 作 为替代 我们创建了一个对应指定参数类型的一维数组 长度为 1 然后将其传入 getPersonInfo()方法 还记得之前我提到过数组是引用类型吗 因此 我可以在 getPersonInfo() 方法中设置数组中的这些基本类型的值 如此一来 第二个问题也就自然地迎刃而解了 01: import java.util.Hashtable; 02: 03: public class PassPrimitiveByReference2 04: { 05: public static void main (String args[]) 06: { 07: String[] name = new String[1]; 08: int[] age = new int[1]; 09: float[] weight = new float[1]; 10: boolean[] isMarried = new boolean[1]; 11: 12: getPersonInfo(name, age, weight, isMarried); 13: 14: System.out.println("Name: " + name[0] + 15: "\nAge: " + age[0] + 16: "\nWeight: " + weight[0] + 17: "\nIs Married: " + isMarried[0]); 18: 19: String name2 = name[0]; 20: Integer age2 = new Integer(age[0]); 21: Float weight2 = new Float(weight[0]); 22: Boolean isMarried2 = new Boolean(isMarried[0]); 23: 24: storePersonInfo(name2, age2, weight2, isMarried2); 25: } 26: 27: private static void getPersonInfo (String[] name, int[] age, 28: float[] weight, boolean[] isMarried) 29: { 30: name[0] = "Robert Smith"; 31: age[0] = 26; 32: weight[0] = 182.7f; 33: isMarried[0] = true; 34: } 第 1 部分 语法 36 t 35: 36: private static void storePersonInfo (String name, Integer age, 37: Float weight, Boolean isMarried) 38: { 39: Hashtable h = new Hashtable(); 40: h.put("name", name); 41: h.put("age", age); 42: h.put("weight", weight); 43: h.put("isMarried", isMarried); 44: } 45: } 运行 PassPrimitiveByReference2 会生成下列输出结果 Name: Robert Smith Age: 26 Weight: 182.7 Is Married: true 虽然这个例子证明了通过数组引用来传递基本数据类型是可行的 但是这决不意味着 Java 语言鼓励这种用法 通常更好的做法是将基本类型封装进对象里 然而 在某些特定情 况下 你也可能会发现唯一可选的方法只有通过数组引用来传递基本数据类型 Item 10: 布尔运算符与 短路 运算符 与 C++ 和 C 相似 Java 也支持位运算符 &和| 但与 C++不同的是 Java 同时支 持布尔运算符 &和| 以及条件运算符 &&和|| 假如你不够仔细的话 这些运算符将会导 致不少问题 假如你曾经在 C++程序中编写过下面这样的代码 那么大多数编译器都会向你发出警告 if (ptr != null & ptr->count > 1) // wrong operator! 举例来说 Gnu C 编译器会生成下列警告信息 warning: suggest parentheses around comparison in operand of & 尽管这行代码是合法的 但是它的实际执行过程也许并不是你所期望的 假如变量 ptr 为 null 那么代码就会尝试去对一个被废弃的指针进行比较操作 程序自然就出问题了 编 Item 10: 布尔运算符与 短路 运算符 u 37 译器能够自动假定你也许并不想在那里做一个位运算 “与” 然而 在 Java 中 编译器不会这么假定 如果表达式两边均为 boolean 型的值 那么这 个运算符“&”将被认为是一个布尔运算符 而不是一个位运算符 参见 Java 语言规范 15.21.2 节 假如你需要在使用一个 Vector 对象之前 检测一下它是否含有任何元素 那么你可能 会无意地写下这样的代码 if ((v != null) & (v.size() > 0)) // wrong operator! 编译器不会报错 因为它认为你是有意这么写的 如果 v 真的为 null 那么这行代码将 会抛出一个 NullPointerException 你所期望的 当然是“短路” short-circuit 运算符 布尔运算符和条件运算符提供的功 能非常类似 它们只有一个重要的不同点 条件运算符 &&和|| 会引起“短路” 也就是说 假如运算符左边表达式的结果 已经足够确定整个条件运算的结果了 那么运算符右边表达 式将会被忽略 下面是正确的代码 if ((v != null) && (v.size() > 0)) 条件运算符||的原理也一样 只要运算符左边表达式的值为 true 那么运算符右边表达式 也将被忽略 最简单的解决方法就是总是使用条件运算符&&和|| 它们更加安全 更有效率 因为需 要处理的操作数更少 假如你需要保证所有的操作数都被处理 最好能够注释上你使用的是 逻辑运算符&或|

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 2 人已下载

下载文档

相关文档