Thinking in Java 3Edition 致读者: 我从2002年7月开始翻译这本书,当时还是第二版。但是翻完前言和介绍部分 后, chinapub就登出广告,说要出版侯捷的译本。于是我中止了翻译,等着侯 先生的作品。 我是第一时间买的这本书,但是我失望了。比起第一版,我终于能看懂这本书 了,但是相比我的预期,它还是差一点。所以当 Bruce Eckel在他的网站上公开 本书的第三版的时候,我决定把它翻译出来 说说容易,做做难。一本1000多页的书不是那么容易翻的。期间我也曾打过退 堂鼓,但最终还是全部翻译出来了。从今年的两月初起,到7月底,我几乎放 弃了所有的业余时间,全身心地投入本书的翻译之中。应该说,这项工作的难 度超出了我的想像。 首先,读一本书和翻译一本书完全是两码事。英语与中文是两种不同的语言, 用英语说得很畅的句子,翻成中文之后就完全破了相。有时我得花好几分钟 用中文重述一句我能用几秒钟读懂的句子。更何况作为读者,一两句话没搞 懂,并不影响你理解整本书,但对译者来说,这就不一样了。 其次,这是一本讲英语的人写给讲英语的人的书,所以同很多要照顾非英语读 者的技术文档不同,它在用词,句式方面非常随意。英语读者会很欣赏这 点,但是对外国读者来说,这就是负担了 再有, Bruce Eckel这样的大牛人,写了1000多页,如果都让你读懂,他岂不是 太没面子?所以,书里还有一些很有“禅意”的句子。比如那句著名的“The genesis of the computer revolution was in a machine. The genesis of our programming languages thus tends to look like that machine.”我就一直没吃准该怎 么翻译。我想大概没人能吃准,说不定 Bruce要的就是这个效果。 这是一本公认的名著,作者在技术上的造诣无可挑剔。而作为译者,我的编程 能力差了很多。再加上上面讲的这些原因,使得我不得不格外的谨慎。当我重 读初稿的时候,我发现需要修改的地方实在太多了。因此,我不能现在就公开 全部译稿,我只能公开已经修改过的部分。不过这不是最终的版本,我还会继 续修订的。 本来,我准备到10月份,等我修改完前7章之后再公开。但是,我发现我又有 点要放弃了,因此我决定给自己一点压力,现在就公开。以后,我将修改完 章就公开一章,请关注www.wgqgh.com/shhgs/tii.html 如果你觉得好,请给告诉我,你的鼓励是我工作的动力;如果你觉得不好,那 就更应该告诉我了,我会参考你的意见作修改的。我希望能通过这种方法,译 出一本配得上原著的书 2003年9月8日 第1页共30页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 1 页 共 30 页 致读者: 我从 2002 年 7 月开始翻译这本书,当时还是第二版。但是翻完前言和介绍部分 后,chinapub 就登出广告,说要出版侯捷的译本。于是我中止了翻译,等着侯 先生的作品。 我是第一时间买的 这本书,但是我失望了。比起第一版,我终于能看懂这本书 了,但是相比我的预期,它还是差一点。所以当 Bruce Eckel 在他的网站上公开 本书的第三版的时候,我决定把它翻译出来。 说说容易,做做难。一本 1000 多页的书不是那么容易翻的。期间我也曾打过退 堂鼓,但最终还是全部翻译出来了。从今年的两月初起,到 7 月底,我几乎放 弃了所有的业余时间,全身心地投入本书的翻译之中。应该说,这项工作的难 度超出了我的想像。 首先,读一本书和翻译一本书完全是两码事。英语与中文是两种不同的语言, 用英语说得很畅的句子,翻成中文之后就完全破了相。有时我得花好几分钟, 用中文重述一句我能用几秒钟读懂的句子。更何况作为读者,一两句话没搞 懂,并不影响你理解整本书,但对译者来说,这就不一样了。 其次,这是一本讲英语的人写给讲英语的人的书,所以同很多要照顾非英语读 者的技术文档不同,它在用词,句式方面非常随意。英语读者会很欣赏这一 点,但是对外国读者来说,这就是负担了。 再有,Bruce Eckel 这样的大牛人,写了 1000 多页,如果都让你读懂,他岂不是 太没面子?所以,书里还有一些很有“禅意”的句子。比如那句著名的“The genesis of the computer revolution was in a machine. The genesis of our programming languages thus tends to look like that machine.”我就一直没吃准该怎 么翻译。我想大概没人能吃准,说不定 Bruce 要的就是这个效果。 这是一本公认的名著,作者在技术上的造诣无可挑剔。而作为译者,我的编程 能力差了很多。再加上上面讲的这些原因,使得我不得不格外的谨慎。当我重 读初稿的时候,我发现需要修改的地方实在太多了。因此,我不能现在就公开 全部译稿,我只能公开已经修改过的部分。不过这不是最终的版本,我还会继 续修订的。 本来,我准备到 10 月份,等我修改完前 7 章之后再公开。但是,我发现我又有 点要放弃了,因此我决定给自己一点压力,现在就公开。以后,我将修改完一 章就公开一章,请关注 www.wgqqh.com/shhgs/tij.html。 如果你觉得好,请给告诉我,你的鼓励是我工作的动力;如果你觉得不好,那 就更应该告诉我了,我会参考你的意见作修改的。我希望能通过这种方法,译 出一本配得上原著的书。 shhgs 2003 年 9 月 8 日
6:复用类 Java最令人心动的特性就是它的代码复用了。但是仅仅拷贝源代码再作 修改是不能被称为“革命”的 那是C之类的过程语言所采用的办法,而且也不怎么成功。就像Java里 的一切,要解决这个问题还要靠类。你可以利用别人写好的、已经测试通 过的类来创建新的类,不必一切都从零开始 这么做的诀窍就是,要在不改动原有代码的前提下使用类。本章会介绍两 种做法。第一种非常简单:在新的类里直接创建旧的类的对象。这被称为 合成( compostion),因为新的类是由旧的类合成而来的。你所复用的只 是代码的功能,而不是它的形式。 第二种方法更为精妙。它会创建一个新的,与原来那个类同属一种类型的 类。你全盘接受了旧类的形式,在没有对它做修改的情况下往里面添加了 新的代码。这种神奇的做法就被称为继承( inheritance)。编译器会承担 绝大部分的工作。继承是面向对象编程的基石,它还有一些额外的含义, 对此我们会在第7章再做探讨 合成与继承在语法和行为上有许多相似之处(这很好理解,因为它们都是 在原有类的基础上创建新类)。你会在本章学到这些代码复用的机制 合成所使用的语法 实际上我们已经看到很多合成的案例了。只要把对象的 reference直接 放到新的类里面就行了。假设,你要创建一个新的类,其中有几个 String对象,几个 primitiⅳve数据,以及一个别的什么类型的对象。对 于非 primitive的对象,你只要把它的 reference放到类里就行了,但是 对于 primitive,你就只能直接定义了 //: c06: Sprinklersystem java // Composition for code reuse import com. bruceeckel. simpletest. class WaterSource private string s WaterSource i System. out. println("WaterSource()")i w String( "Constructed")i public string tostring()( return s public class Sprinklersystem t private static Test monitor new Test()i private string valvel, valve2, valve, valve 第2页共30页 www.wgqqh.com/shhgs/tij.html emailshhgs(@sohu.com
Chapter 6: Reusing Classes www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 2 页 共 30 页 6: 复用类 Java 最令人心动的特性就是它的代码复用了。但是仅仅拷贝源代码再作 修改是不能被称为“革命”的。 那是 C 之类的过程语言所采用的办法,而且也不怎么成功。就像 Java 里 的一切,要解决这个问题还要靠类。你可以利用别人写好的、已经测试通 过的类来创建新的类,不必一切都从零开始。 这么做的诀窍就是,要在不改动原有代码的前提下使用类。本章会介绍两 种做法。第一种非常简单:在新的类里直接创建旧的类的对象。这被称为 合成(compostion),因为新的类是由旧的类合成而来的。你所复用的只 是代码的功能,而不是它的形式。 第二种方法更为精妙。它会创建一个新的,与原来那个类同属一种类型的 类。你全盘接受了旧类的形式,在没有对它做修改的情况下往里面添加了 新的代码。这种神奇的做法就被称为继承(inheritance)。编译器会承担 绝大部分的工作。继承是面向对象编程的基石,它还有一些额外的含义, 对此我们会在第 7 章再做探讨。 合成与继承在语法和行为上有许多相似之处(这很好理解,因为它们都是 在原有类的基础上创建新类)。你会在本章学到这些代码复用的机制。 合成所使用的语法 实际上我们已经看到很多合成的案例了。只要把对象的 reference 直接 放到新的类里面就行了。假设,你要创建一个新的类,其中有几个 String 对象,几个 primitive 数据,以及一个别的什么类型的对象。对 于非 primitive 的对象,你只要把它的 reference 放到类里就行了,但是 对于 primitive,你就只能直接定义了: //: c06:SprinklerSystem.java // Composition for code reuse. import com.bruceeckel.simpletest.*; class WaterSource { private String s; WaterSource() { System.out.println("WaterSource()"); s = new String("Constructed"); } public String toString() { return s; } } public class SprinklerSystem { private static Test monitor = new Test(); private String valve1, valve2, valve3, valve4;
Thinking in Java 3Edition private WaterSource source; pr⊥ vate int i public String tostring()( return "valve =" valve +"\n "valve2 ="+ valve2 "\n" Ive "valve ="+ valve +"\n" i i+"\ source =t sourcei public static void main(String[] args) f SprinklerSystem sprinklers =new System. out. println(sprinklers) monitor. expect (new string[] i valve= null valve2=nullr valves= null valve null" }) /// 这两个类都定义了一个特殊的方法: tostring()。以后你就会知道,所 有非 primitive对象都有一个 tostring〔)方法,当编译器需要一个 String而它却是一个对象的时候,编译器就会自动调用这个方法。所以 当编译器从 Sprin klersystem tostring()的 sour ce t source 中看到,你想把 String同 WaterSource相加的时候,它就会说“由 于 String只能同 String相加,因此我要调用 source的 tostring(),因为只有这样才能把它转换成 String!”。于是它就把 这两个 String连起来,然后再 String的形式把结果返还给 System. out println()。如果你想让你写的类也具备这个功能,只要 写一个 tostring()方法就行了。 我们已经在第2章讲过,当 primitive数据作为类的成员的时候,会被自 动地初始化为零。而对象的 reference则会被初始化为nu,如果这 时,你去调用这个对象的方法,就会得到异常。能把它打印出来而不抛出 异常,这真是太好了(而且也很实用)。 第3页共30页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 3 页 共 30 页 private WaterSource source; private int i; private float f; public String toString() { return "valve1 = " + valve1 + "\n" + "valve2 = " + valve2 + "\n" + "valve3 = " + valve3 + "\n" + "valve4 = " + valve4 + "\n" + "i = " + i + "\n" + "f = " + f + "\n" + "source = " + source; } public static void main(String[] args) { SprinklerSystem sprinklers = new SprinklerSystem(); System.out.println(sprinklers); monitor.expect(new String[] { "valve1 = null", "valve2 = null", "valve3 = null", "valve4 = null", "i = 0", "f = 0.0", "source = null" }); } } ///:~ 这两个类都定义了一个特殊的方法:toString( )。以后你就会知道,所 有非 primitive 对象都有一个 toString( )方法,当编译器需要一个 String 而它却是一个对象的时候,编译器就会自动调用这个方法。所以 当编译器从 SprinklerSystem.toString( )的: "source = " + source; 中看到,你想把 String 同 WaterSouce 相加的时候,它就会说“由 于 String 只能同 String 相加,因此我要调用 source 的 toString( ),因为只有这样才能把它转换成 String!”。 于是它就把 这两个 String 连起来,然后再 String 的形式把结果返还给 System.out.println( )。如果你想让你写的类也具备这个功能,只要 写一个 toString( )方法就行了。 我们已经在第 2 章讲过,当 primitive 数据作为类的成员的时候,会被自 动地初始化为零。而对象的 reference 则会被初始化为 null,如果这 时,你去调用这个对象的方法,就会得到异常。能把它打印出来而不抛出 异常,这真是太好了(而且也很实用)
“编译器不为 reference准备默认对象”的这种做法,实际上也是很合 乎逻辑的。因为在很多情况下,这么做会引发不必要的性能开销。如果你 想对 reference进行初始化,那么可以在以下几个时间进行: 1.在定义对象的时候。这就意味着在构造函数调用之前,它们已经初始化 完毕了。 2.在这个类的构造函数里。 3.在即将使用那个对象之前。这种做法通常被称为“偷懒初始化( (lazy initialization)”。如果碰到创建对象的代价很高,或者不是每次都需要 创建对象的时候,这种做法就能降低程序的开销了。 下面这段程序把这三种办法都演示一遍 //: c06: Bath. j // Constructor initialization with composition import com. bruceeckel. simpletest* Syst s new String("Constructed")i pub g tostring ()t return si public class Bath i private static Test monitor new Test()i private String / Initializing at point of definition s1 s3,s4; private Soap castille private int l public Bath()t System. out. println("Inside Bath()")i 3= new string( Joy toy =3.14f public String tostring()( null)// De layed return "s1="+s1+"\n"+ 2 castille =w+ castille public static void main(String[] args)i 第4页共30页 www.wgqqh.com/shhgs/tij.html emailshhgs(@sohu.com
Chapter 6: Reusing Classes www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 4 页 共 30 页 “编译器不为 reference 准备默认对象”的这种做法,实际上也是很合 乎逻辑的。因为在很多情况下,这么做会引发不必要的性能开销。如果你 想对 reference 进行初始化,那么可以在以下几个时间进行: 1. 在定义对象的时候。这就意味着在构造函数调用之前,它们已经初始化 完毕了。 2. 在这个类的构造函数里。 3. 在即将使用那个对象之前。这种做法通常被称为“偷懒初始化(lazy initialization)”。如果碰到创建对象的代价很高,或者不是每次都需要 创建对象的时候,这种做法就能降低程序的开销了。 下面这段程序把这三种办法都演示一遍: //: c06:Bath.java // Constructor initialization with composition. import com.bruceeckel.simpletest.*; class Soap { private String s; Soap() { System.out.println("Soap()"); s = new String("Constructed"); } public String toString() { return s; } } public class Bath { private static Test monitor = new Test(); private String // Initializing at point of definition: s1 = new String("Happy"), s2 = "Happy", s3, s4; private Soap castille; private int i; private float toy; public Bath() { System.out.println("Inside Bath()"); s3 = new String("Joy"); i = 47; toy = 3.14f; castille = new Soap(); } public String toString() { if(s4 == null) // Delayed initialization: s4 = new String("Joy"); return "s1 = " + s1 + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" + "i = " + i + "\n" + "toy = " + toy + "\n" + "castille = " + castille; } public static void main(String[] args) { Bath b = new Bath();
Thinking in Java 3Edition System. out. println (b) monitor. expect(new String[] t Inside Bath( s1 Happy", 234 Hapy toy=3.14" castillo Constructed" }///:~ 注意,Bath的构造函数会先打印一条消息再进行初始化。如果你不在定 义对象的时候进行初始化,那么没人可以担保,在向这个对象的 reference发送消息的时候,它已经被初始化了—反倒是会有异常来告 诉你,它还没有初始化,。 调用 tostring()的时候它会先为s4赋一个值,这样它就不会未经初始 化而被使用了 继承所使用的语法 继承是Java(也是所有OOP语言)不可分割的一部分。实际上当你创建类 的时候,你就是在继承,要么是显式地继承别的什么类,要么是隐含地继 承了标准Java根类, Object 合成的语法很平淡,但继承就有所不同了。继承的时候,你得先声明“新 类和旧类是一样的。”跟平常一样,你得先在程序里写上类的名字,但是 在开始定义类之前,你还得加上 extends关键词和基类( base class)的 名字。做完这些之后,新类就会自动获得基类的全部成员和方法。下面就 是一个例子 //: c06: Detergent. java // Inheritance syntax properties import com. bruceeckel. simpletest class Cleanser protected static Test monitor new Test ()i private string s= new String(" Cleanser public void append(string a) s+= ai public void dilute() append(" dilute ())i) public void apply()( append(" apply()") public void scrub()( append(" scrub()") public string tostring()t return s; public static void main (String[ args)i Cleanser x new Cleanser xdilute()x apply ();x scrub()i 第5页共30页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 5 页 共 30 页 System.out.println(b); monitor.expect(new String[] { "Inside Bath()", "Soap()", "s1 = Happy", "s2 = Happy", "s3 = Joy", "s4 = Joy", "i = 47", "toy = 3.14", "castille = Constructed" }); } } ///:~ 注意,Bath 的构造函数会先打印一条消息再进行初始化。如果你不在定 义对象的时候进行初始化,那么没人可以担保,在向这个对象的 reference 发送消息的时候,它已经被初始化了——反倒是会有异常来告 诉你,它还没有初始化,。 调用 toString( )的时候它会先为 s4 赋一个值,这样它就不会未经初始 化而被使用了。 继承所使用的语法 继承是 Java(也是所有 OOP 语言)不可分割的一部分。实际上当你创建类 的时候,你就是在继承,要么是显式地继承别的什么类,要么是隐含地继 承了标准 Java 根类,Object。 合成的语法很平淡,但继承就有所不同了。继承的时候,你得先声明“新 类和旧类是一样的。”跟平常一样,你得先在程序里写上类的名字,但是 在开始定义类之前,你还得加上 extends 关键词和基类(base class)的 名字。做完这些之后,新类就会自动获得基类的全部成员和方法。下面就 是一个例子: //: c06:Detergent.java // Inheritance syntax & properties. import com.bruceeckel.simpletest.*; class Cleanser { protected static Test monitor = new Test(); private String s = new String("Cleanser"); public void append(String a) { s += a; } public void dilute() { append(" dilute()"); } public void apply() { append(" apply()"); } public void scrub() { append(" scrub()"); } public String toString() { return s; } public static void main(String[] args) { Cleanser x = new Cleanser(); x.dilute(); x.apply(); x.scrub();
System. out. println(x) monitor.expect (new String[] t Cleanser dilute() apply() scrub( public class Detergent extends Cleanser / Change a method: public void scrub()( append(" Detergent scrub()") super scrub();// Call base-class version / Add methods to the interface public void foam()[ append("foam()")i) / Test the new class public static void main(String[] args) i Detergent x new Detergent( xapply()i xscrub o foam( System. out. println("Testing base class: monitor. expect(new String[] Cleanser dilute() apply()"+ Detergent scrub() scrub() foam()", Testing base class: Cl 这段程序能告诉我们很多东西。首先 Cleanser的 append()方法用 十〓运算符将 Sting同s联接起来。Java的设计者们“重载”了这个操 作符,使之能作用于 String 第二, Cleanser和 Detergent都有一个main()方法。你可以为每 个类都创建一个main(),而且这也是一种值得提倡的编程方法,因为 这样一来,测试代码就能都放进类里了。即使程序包括了很多类,它也只 会调用你在命令行下给出的那个类的main()方法。(只要main()是 public的就行了,至于类是不是 public的,并不重要。)于是,当你输 入 java Detergent的时候,它就会调用 Detergent. main()。虽 然 Cleanser不是 public的,但是你也可以用 java Cleanser来调 用 Cleanser, main()。这种往每个类里都放一个main()的做法,能 让类的单元测试变得更容易一些。做完测试之后,你也不必移除 main():留下它可以供以后的测试用。 这里, Detergent. main()直接调用了 Cleanser. main(),并且把 命令行参数原封不动地传给了它(实际上可以使用任何 String数组)。 第6页共30页 www.wgqqh.com/shhgs/tij.html emailshhgs(@sohu.com
Chapter 6: Reusing Classes www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 6 页 共 30 页 System.out.println(x); monitor.expect(new String[] { "Cleanser dilute() apply() scrub()" }); } } public class Detergent extends Cleanser { // Change a method: public void scrub() { append(" Detergent.scrub()"); super.scrub(); // Call base-class version } // Add methods to the interface: public void foam() { append(" foam()"); } // Test the new class: public static void main(String[] args) { Detergent x = new Detergent(); x.dilute(); x.apply(); x.scrub(); x.foam(); System.out.println(x); System.out.println("Testing base class:"); monitor.expect(new String[] { "Cleanser dilute() apply() " + "Detergent.scrub() scrub() foam()", "Testing base class:", }); Cleanser.main(args); } } ///:~ 这段程序能告诉我们很多东西。首先 Cleanser 的 append( )方法用 += 运算符将 Sting 同 s 联接起来。Java 的设计者们“重载”了这个操 作符,使之能作用于 String。 第二,Cleanser 和 Detergent 都有一个 main( )方法。你可以为每 个类都创建一个 main( ),而且这也是一种值得提倡的编程方法,因为 这样一来,测试代码就能都放进类里了。即使程序包括了很多类,它也只 会调用你在命令行下给出的那个类的 main( )方法。(只要 main( )是 public 的就行了,至于类是不是 public 的,并不重要。)于是,当你输 入 java Detergent 的时候,它就会调用 Detergent.main( )。虽 然 Cleanser 不是 public 的,但是你也可以用 java Cleanser 来调 用 Cleanser.main( )。这种往每个类里都放一个 main( )的做法,能 让类的单元测试变得更容易一些。做完测试之后,你也不必移除 main( );留下它可以供以后的测试用。 这里,Detergent.main( )直接调用了 Cleanser.main( ),并且把 命令行参数原封不动地传给了它(实际上可以使用任何 String 数组)
Thinking in Java 3Edition 有一点很重要,那就是 Cleanser的方法都是 public的。记住,如果 你不写访问控制符,成员就会被默认地赋予 package权限,于是同一个 ackage内的任何类就都能访问这些方法了。 Detergent没问题。但 是,如果别的 package里有一个继承了 Cleanser的类,那它就只能访 问 Cleanser的 public的成员了。(我们以后会讲,派生类可以访问基 类的 protected的成员。)所以继承设计方面有一条通用准则,那就是 把数据都设成 private的,把方法都设成 public的。当然碰到特殊情 况还要进行调整,但是这还是一条非常有用的准则 注意, Cleanser的接口包括了一组方法: append(), dilute( apply(), scrub(),以及 tostring。由于 Detergent是由 Cleanser派生出来的(通过 extends关键词),所以尽管它没有明确地 定义这些方法,它还是自动获得了这个接口的所有方法。由此,你可以将 继承理解成类的复用。 正如 scrub()所揭示的,你可以在派生类里修改一个在基类里的定义的 方法。这时,你有可能要在新方法里调用基类的方法。但是你不能在 scrub()里面直接调用 scrub(),因为这是递归,你要的应该不是这 个吧。为解决这个问题,Java提供了一个表示当前类所继承的那个“超 类( superclass)”的 super关键词。于是 super. scrub()就会调用基 类的 scrub()方法了 继承并不会限定你只能使用基类的方法。你也可以往派生类里加进新的方 法,就像往普通的类里加方法一样:直接定义就是了。foam()就是 例 从 Degergent. main()可以看出, Detergent对象既有 Cleanser 的方法,也有它自己的方法(就是foam() 基类的初始化 现在要创建派生类对象已经不是一个类的事情了,它会牵涉到两个类 基类和派生类,因此要搞清楚它究竟是怎么创建的,就有点难度了。从局 外人的角度来看,新类具备了和旧类完全相同的接口,并且还有可能会有 些它自己的方法和数据。但继承并不仅仅是拷贝基类的接口。当你创建 个派生类对象的时候,这个对象里面还有一个基类的子对象 ( subobject)。这个子对象同基类自己创建的对象没什么两样。只是从外 面看来,这个子对象被包裹在派生类的对象里面。 当然,基类子对象的正确初始化也是非常重要的,而且只有一个办法能保 证这一点:调用基类的构造函数来进行初始化,因为只有它才掌握怎样才 能正确地进行初始化的信息和权限。Java会让派生类的构造函数自动地 调用基类的构造函数。下面这段程序就演示了它在三级继承体系下是如何 运作的 第7页共30页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 7 页 共 30 页 有一点很重要,那就是 Cleanser 的方法都是 public 的。记住,如果 你不写访问控制符,成员就会被默认地赋予 package 权限,于是同一个 package 内的任何类就都能访问这些方法了。Detergent 没问题。但 是,如果别的 package 里有一个继承了 Cleanser 的类,那它就只能访 问 Cleanser 的 public 的成员了。(我们以后会讲,派生类可以访问基 类的 protected 的成员。)所以继承设计方面有一条通用准则,那就是 把数据都设成 private 的,把方法都设成 public 的。当然碰到特殊情 况还要进行调整,但是这还是一条非常有用的准则。 注意,Cleanser 的接口包括了一组方法:append(),dilute(), apply(),scrub(),以及 toString()。由于 Detergent 是由 Cleanser 派生出来的(通过 extends 关键词),所以尽管它没有明确地 定义这些方法,它还是自动获得了这个接口的所有方法。由此,你可以将 继承理解成类的复用。 正如 scrub( )所揭示的,你可以在派生类里修改一个在基类里的定义的 方法。这时,你有可能要在新方法里调用基类的方法。但是你不能在 scrub( )里面直接调用 scrub( ),因为这是递归,你要的应该不是这 个吧。为解决这个问题,Java 提供了一个表示当前类所继承的那个“超 类(superclass)”的 super 关键词。于是 super.scrub( )就会调用基 类的 scrub( )方法了。 继承并不会限定你只能使用基类的方法。你也可以往派生类里加进新的方 法,就像往普通的类里加方法一样:直接定义就是了。foam( )就是一 例。 从 Degergent.main( )可以看出,Detergent 对象既有 Cleanser 的方法,也有它自己的方法(就是 foam())。 基类的初始化 现在要创建派生类对象已经不是一个类的事情了,它会牵涉到两个类—— 基类和派生类,因此要搞清楚它究竟是怎么创建的,就有点难度了。从局 外人的角度来看,新类具备了和旧类完全相同的接口,并且还有可能会有 一些它自己的方法和数据。但继承并不仅仅是拷贝基类的接口。当你创建 一个派生类对象的时候,这个对象里面还有一个基类的子对象 (subobject)。这个子对象同基类自己创建的对象没什么两样。只是从外 面看来,这个子对象被包裹在派生类的对象里面。 当然,基类子对象的正确初始化也是非常重要的,而且只有一个办法能保 证这一点:调用基类的构造函数来进行初始化,因为只有它才掌握怎样才 能正确地进行初始化的信息和权限。Java 会让派生类的构造函数自动地 调用基类的构造函数。下面这段程序就演示了它在三级继承体系下是如何 运作的:
C //: c06: Cartoon. java // Constructor calls during inheritant import com. bruceeckel simpletest *i Cl Art(( System. out. p class Drawing extends Art Drawing()( System. out. println(" Drawing constructor)i public class Cartoon extends Drawing ew Test( public Cartoon()[ System. out. println("Cartoon constructor)i public static void main(String[] args) t new cart monitor.expect (new String[] t Drawing constructor Cartoon constructor 可以看到,构造行为是从基类“向外”发展的,所以基类会在派生类的构 造函数访问它之前先进行初始化。即便你不创建 Cartoon()的构造函 数,编译器也会为你造一个默认的构造函数,然后再由它去调用基类的构 造函数 带参数的构造函数 在上述例程中,构造函数都是默认的:也就是不带参数的。对编译器来 说,调用这种构造函数会非常简单,因为根本就没有要传哪些参数的问 题。但是如果类没有默认的构造函数(也就是无参数的构造函数),或者你 要调用的基类构造函数是带参数的,你就必须用 super关键词以及合适 的参数明确地调用基类的构造函数: /: c06: Chess. 3 Inheritance, constructors and argu import com. bruceeckel. simpletest. class Game Game (int 1)( System. out. println("Game constructor") 第8页共30页 www.wgqqh.com/shhgs/tij.html emailshhgs(@sohu.com
Chapter 6: Reusing Classes www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 8 页 共 30 页 //: c06:Cartoon.java // Constructor calls during inheritance. import com.bruceeckel.simpletest.*; class Art { Art() { System.out.println("Art constructor"); } } class Drawing extends Art { Drawing() { System.out.println("Drawing constructor"); } } public class Cartoon extends Drawing { private static Test monitor = new Test(); public Cartoon() { System.out.println("Cartoon constructor"); } public static void main(String[] args) { Cartoon x = new Cartoon(); monitor.expect(new String[] { "Art constructor", "Drawing constructor", "Cartoon constructor" }); } } ///:~ 可以看到,构造行为是从基类“向外”发展的,所以基类会在派生类的构 造函数访问它之前先进行初始化。即便你不创建 Cartoon( )的构造函 数,编译器也会为你造一个默认的构造函数,然后再由它去调用基类的构 造函数。 带参数的构造函数 在上述例程中,构造函数都是默认的;也就是不带参数的。对编译器来 说,调用这种构造函数会非常简单,因为根本就没有要传哪些参数的问 题。但是如果类没有默认的构造函数(也就是无参数的构造函数),或者你 要调用的基类构造函数是带参数的,你就必须用 super 关键词以及合适 的参数明确地调用基类的构造函数: //: c06:Chess.java // Inheritance, constructors and arguments. import com.bruceeckel.simpletest.*; class Game { Game(int i) { System.out.println("Game constructor"); } }
Thinking in Java 3Edition class boardgame extends game BoardGame (int i)( super (i) System. out. println("BoardGame constructor") public class Chess extends BoardGame i private static Test monitor new Test()i Chess i super(11)i System. out. println ("Chess constructor " public static void main(String[] args) i Chess x new Chess( monitor.expect(new String[] i Game constructor, wchess constructors orn BoardGame construct }///:~ 如果你不在 Board game()里面调用基类的构造函数,编译器就会报错 说它找不到Game()形式(译者注:即默认)的构造函数。此外,对派生 类构造函数而言,调用基类的构造函数应该是它做的第一件事。(如果你 做错了,编译器就会提醒你。) 捕获基类构造函数抛出的异常 我们刚说了,编译器会强制你将基类构造函数的调用放在派生类的构造函 数的最前面。也就是说,在它之前不能有任何东西。等到第9章你就会知 道,这么做会妨碍派生类的构造函数捕获基类抛出的异常。这一点有时会 很不方便。 把合成和继承结合起来 同时使用合成和继承的现象是很普遍的。下面这段程序演示了,怎样使用 合成和继承,以及利用构造函数来进行初始化这一必不可少的步骤,来创 建一个较为复杂的类 c06: Placesetting. java // Combining composition inheritance import com. bruceeckel. simpletest class plate Plate(int i)( System. out. printin("Plate constructor) 第9页共30页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 9 页 共 30 页 class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame constructor"); } } public class Chess extends BoardGame { private static Test monitor = new Test(); Chess() { super(11); System.out.println("Chess constructor"); } public static void main(String[] args) { Chess x = new Chess(); monitor.expect(new String[] { "Game constructor", "BoardGame constructor", "Chess constructor" }); } } ///:~ 如果你不在 BoardGame( )里面调用基类的构造函数,编译器就会报错 说它找不到 Game( ) 形式(译者注:即默认)的构造函数。此外,对派生 类构造函数而言,调用基类的构造函数应该是它做的第一件事。(如果你 做错了,编译器就会提醒你。) 捕获基类构造函数抛出的异常 我们刚说了,编译器会强制你将基类构造函数的调用放在派生类的构造函 数的最前面。也就是说,在它之前不能有任何东西。等到第 9 章你就会知 道,这么做会妨碍派生类的构造函数捕获基类抛出的异常。这一点有时会 很不方便。 把合成和继承结合起来 同时使用合成和继承的现象是很普遍的。下面这段程序演示了,怎样使用 合成和继承,以及利用构造函数来进行初始化这一必不可少的步骤,来创 建一个较为复杂的类: //: c06:PlaceSetting.java // Combining composition & inheritance. import com.bruceeckel.simpletest.*; class Plate { Plate(int i) { System.out.println("Plate constructor"); } }
class DinnerPlate extends plate DinnerPlate(int i)( super (i) System. out. println("DinnerPlate constructor") class Utensil Utensil (int i)( System. class Spoon extends Utensil super (i)i System. out. println("Spoon constructor ") class Fork extends Utensil Fork(int i) I super(i)i System. out. println("Fork constructor") class Knife extends Utensil Knife(int i) super (i) System. out. println("Knife constructor ") //A cultural way of doing something class Custom Custom(int i) System. out. println("Custom constructor") public class Placesetting extends Custom t private static Test monitor new Test()i private Spoon s private Fork frk private Knife kn rivate Dinnerplate pl oublic PlaceSetting (int i)t frk new Fork(i +3) (i+4); 1ate(i+5); System. out. println("PlaceSetting constructor")i public static void (String [I Place setting x laceset t(new string[] t Custom constructor, "Utensil constructor"t 第10页共30页 www.wgqqh.com/shhgs/tij.html emailshhgs(@sohu.com
Chapter 6: Reusing Classes www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 第 10 页 共 30 页 class DinnerPlate extends Plate { DinnerPlate(int i) { super(i); System.out.println("DinnerPlate constructor"); } } class Utensil { Utensil(int i) { System.out.println("Utensil constructor"); } } class Spoon extends Utensil { Spoon(int i) { super(i); System.out.println("Spoon constructor"); } } class Fork extends Utensil { Fork(int i) { super(i); System.out.println("Fork constructor"); } } class Knife extends Utensil { Knife(int i) { super(i); System.out.println("Knife constructor"); } } // A cultural way of doing something: class Custom { Custom(int i) { System.out.println("Custom constructor"); } } public class PlaceSetting extends Custom { private static Test monitor = new Test(); private Spoon sp; private Fork frk; private Knife kn; private DinnerPlate pl; public PlaceSetting(int i) { super(i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3); kn = new Knife(i + 4); pl = new DinnerPlate(i + 5); System.out.println("PlaceSetting constructor"); } public static void main(String[] args) { PlaceSetting x = new PlaceSetting(9); monitor.expect(new String[] { "Custom constructor", "Utensil constructor", "Spoon constructor