




已阅读5页,还剩40页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第1 部分 语法程序员们总是被层出不穷的复杂问题所困扰假如我们最基本的开放工具 设计和编写程序的语言本身就是复杂的那么这个语言自己也会成为这些复杂问题的一部分而非它们的解决方案了 C. A. R. Hoare, The Emperors Old ClothesJava 语言从C+派生而来并借鉴了Objective C Eiffel Smalltalk Mesa 和Lisp 这些语言的一些特性当使用其他语言的程序员转用Java 来编程时他们很快就会发现Java 的一些特性和自己以前所熟悉的语言非常相似因此这些程序员通常会认为这些特性在Java 中和在以前所使用的语言中表现一致其实完全不是这样这些想法在C+程序员中尤其普遍这一部分重点强调了Java 语言上经常会绊倒新手的陷阱和语言行为本部分包括以下 10 个单元Item 1 什么时候被覆盖的 方法并非真的被覆盖了本单元解释了调用子类的实例方法和静态方法之间的微妙差别Item 2 String.equals()方法与= 运算符的用法比较”本单元解释了这两种方法比较字符串的不同之处并指出了常量池是如何混淆这两种用法的。第1 部分语法2 tItem 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 语言规范 节01: class Super02: 03: static String greeting()04: 05: return Goodnight;06: 07:08: String name()09: 10: return Richard;11: 12: 01: class Sub extends Super02: 03: static String greeting()04: 05: return Hello;06: 07:08: String name()09: 10: return Dick;11: 12: 01: class Test02: 03: public static void main(String args)04: 05: Super s = new Sub();06: System.out.println(s.greeting() + , + ();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()返回的值加上一个字符串紧随其后的是()的返回值关键问题就在这里我们调用的到底是Super 类的方法还是Sub 类的方法让我们首先判断调用的是哪个类的name()方法两个类中的name()方法都不是静态方法而是实例方法因为Sub 类继承了Super 类而且有一个和它父类同样标识的name()方法所以Sub 类中的name()方法覆盖了Super 类中的name()方法那么前面提到的变量s 又是Sub 类的一个实例这样一来()的返回值就是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 5l 试图用子类的静态方法隐藏父类中同样标识的实例方法是不合法的编译器将会报错l 试图用子类的实例方法覆盖父类中同样标识的静态方法也是不合法的编译器同样会报错l 静态方法和最终方法 带关键字final 的方法不能被覆盖l 实例方法能够被覆盖l 抽象方法必须在具体类1中被覆盖Item 2: String.equals( )方法与= 运算符的用法比较具有C+经验的读者们肯定会对何时使用等于运算符= = 以及何时使用string 类感到困惑这种困惑主要是String.equals(.)方法和= =运算符的混淆尽管它们有时产生的结果一样然而实际上它们的用法是不同的看看下面的例子01: public class StringExample02: 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): trues0.equals(s2): trues0 = s1: falses0 = s2: true1 具体类也就是抽象方法所属抽象类的非抽象子类第1 部分语法6 tString.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 StringExample204: 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: ern();15: ern();16:17: System.out.println(s0 = s1: + (s0 = s1);18: System.out.println(s0 = ern(): +19: (s0 = ern();20: 21:22: private static String readStringFromFile (String sFileName)23: 24: /read string from file25: 26: 这个例子没有设置 s0 和s1 的值为字符串常量取而代之的是在运行期它从一个文件中读取字符串并把值分配给readStringFromFile(.)方法创建的String 实例从第9 行开始程序对两个被新创建为具有同样字符值的String 实例进行处理当你看到从第11 行到12 行的输出结果时你会再次注意到这两个对象并不是同一个但它们的内容是相同的输出结果如下第1 部分语法8 ts0 = s1: falses0.equals(s1): trues0 = s1: falses0 = ern(): true第 14 行所做的是将String 实例的引用s0 存入常量池当第15 行被处理时对ern()方法的调用会简单地返回引用s0 这样一来第17 行和18 行的输出结果正是我们所期望的s0 与s1 仍旧是截然不同的两个String 实例因此s0=s1 的结果是false 而ern()返回的是常量池中的引用值即s0 所以表达式s0=ern()的结果是true 假如我们希望将实例s1 存入常量池中我们必须首先设置s0 为null 然后请求垃圾回收器garbagecollector 回收被指向s0 的String 实例在s0 被回收后ern()方法的调用将会把s1存入常量池总的来说 在执行等式比较equality comparison 时应该始终使用String.equals(.)方法而不是=运算符如果你还是习惯性地使用=运算符那么intern()方法可以帮助你得到正确的答案因为当n 和m 均为String 实例的引用时语句n.equals(m)与ern() =ern()得到的结果是一致的假如你打算充分利用常量池的优势那么你就应该选择Sern()方法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 Truncation02: 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 型因此小数部分丢失了事实上当intFloat 方法并没有得到期望的float 型参数时切断就已经发生了修复很简单只要表达式中的任何一个值是float 型那么另一个也会被提升promote 为float 型所以第11 行和12 行操作正常扩展当基本类型的值能够在不损失数值的情况下被转换时 转换操作会自动发生在这些情况下的转换被称作 扩展之所以这么称呼是因为它们都会被转换成能够存储较大数据的类型例如 可以为int 变量分配一个byte 值因为这不会引起数值或精度的损失图1.1 展示了扩展转换括号内的数字是用来存储每种类型所需的比特数当一个类型被转换为具有更多比特数的类型时它不会损失任何信息图 1.1 扩展转换注意 假如把一个int 型或long 型转换成为float 型或者把一个long 型转换成为double型可能会损失一些精度换句话说就是一些最不重要的位可能会丢失这种提升是隐性发生的下面这个例子的输出结果-46 显示了在编译器没有任何警告的情况下发生的精度损失public class LostPrecisionpublic static void main (String args)int orig = 1234567890;float approx = orig;int rounded = (int) approx; / lost precisionSystem.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 TypeConversion02: 03: static void convertArg (byte b) 04:05: public static void main (String args)06: 07: byte b1 = 127;08: / byte b2 = 128; / wont compile09:10: / convertArg (127); / wont compile11: convertArg (byte)127);12:13: byte c1 = b1;14: / byte c2 = -b1; / wont compile15: int i = +b1; / overflow16: 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 precisionfound : intrequired: byte编译器将b1 提升为int 型然后警告你int 型的值无法赋值给byte 变量如此一来你可能预料到也希望第15 行中byte 型的值在增量操作时会被自动提升为int 型那么第16 行代码将会输出“i = 128”吧相反由于溢出的发生这个输出结果将会显示一个负数“i= -128”Item 4: 那是构造函数吗你曾经遇到过这样的情况吗由于一个简单的错误引起的Bug 导致你花了一整天的时间来寻找并解决它这样的Bug 的特征就在于也许你会在一分钟内发现或许你会因此困扰几个小时下列的代码就包含了其中的一种Bug 看看你要花费多长时间解决它01: public class IntAdder02: 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 对象亦或是使用反射来访问cument 类的spellCheck()方法所有的结果都将是失败的在经过许多次失败的调用后厂商的技术支持也明确地告诉你Document 类没有被修改过你决定去查阅一下Java 语言规范并最终在 章节中看到了下面这段话Item 5: 不能访问被覆盖的方法u 15“可以通过包含关键字super 的方法调用表达式来访问被覆盖的方法注意尝试用全局名或强制转换为父类型以求访问一个被覆盖的方法都是无效的”最终 一切都清楚了当你从DocumentManager 类中得到一个文档时这第三方文本编辑器的类库总是创建一个HTMLDocument 型的对象无论你尝试着对这个HTMLDocument对象做什么强制转换反射等等你调用的永远是HTMLDocument 类的spellCheck()方法而不是所期望的Document 类的spellCheck()方法实际上当你创建了一个覆盖了父类实例方法的子类时访问那些被覆盖方法的唯一途径就是使用super 关键字任何使用子类的外部类永远不能调用父类的这些被覆盖的实例方法下面的代码例证了这个概念01: class DocumentManager02: 03: public Document newDocument()04: 05: return (new HTMLDocument();06: 07: 08:09: class Document10: 11: public boolean spellCheck()12: 13: return (true);14: 15: 16:17: class HTMLDocument extends Document18: 19: public boolean spellCheck()20: 21: System.out.println(Trouble checking these darn hyperlinks!);22: return (false);23: 24: 25:26: public class OverridingInstanceApp27: 28: public static void main (String args)29: 30: DocumentManager dm = new DocumentManager();第1 部分语法16 t31: Document d = dm.newDocument();32: boolean spellCheckSuccessful = d.spellCheck();33: if (spellCheckSuccessful)34: System.out.println(No spelling errors where found.);35: else36: System.out.println(Document has spelling errors.);37: 38: 在第 32 行中我们尝试调用了一个“明显”是Document 对象的spellCheck()方法然而它实际上是一个MLDocument 类的实例所以这个例子的输出结果是这样的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 Wealthy02: 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 t01: public class Poor02: 03: public String answer = Yes!;04: public void wantMoney()05: 06: String answer = No!; / hides instance variable answer07: 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 产
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 人教部编版道德与法治八年级上册:9.1 认识总体国家安全观 教学设计
- 初中英语Unit 5 Do you have a soccer ball综合与测试教案及反思
- 2024唐山人才发展集团为中国移动合作商妙音科技有限公司发布招聘笔试参考题库附带答案详解
- 云南省德宏州梁河县高中地理 第四单元 人类活动的地域联系 4.2 农业与区域可持续发展教学设计 鲁教版必修2
- 人教部编版 (五四制)二年级下册课文 516 雷雨教案
- 六年级下册北京的春节教案
- 二年级上册科学教学设计-13.我们的衣服 青岛版(六年制)
- 七年级数学上册 第3章 一次方程与方程组3.1 一元一次方程及其解法第3课时 用去括号解一元一次方程教学设计 (新版)沪科版
- 妇联主席培训述学
- 九年级历史下册 第11课 战争的扩大和转折教学设计 川教版
- 石家庄市桥西区第四十一中学2022-2023学年七年级下学期期中数学试题
- 高一地理必修-1.4-地球的圈层结构课件
- 2024年吉林省考公务员面试题及参考答案
- 2024年4月贵州省高三年级适应性考试地理试卷
- 农村公路安全生命防护工程可行性研究报告
- 金属废料加工的废料分类与分拣技术
- 2024城镇燃气智能调压箱技术规范
- 中国制造业及发展-课件
- 工程造价咨询服务方案及工程造价咨询服务方案完整版
- 北海旅游攻略自驾行
- 社区矫正法专题知识讲座
评论
0/150
提交评论