java编程思想_第六章(复用类).docx

上传人:飞****2 文档编号:78787509 上传时间:2023-03-19 格式:DOCX 页数:44 大小:61.64KB
返回 下载 相关 举报
java编程思想_第六章(复用类).docx_第1页
第1页 / 共44页
java编程思想_第六章(复用类).docx_第2页
第2页 / 共44页
点击查看更多>>
资源描述

《java编程思想_第六章(复用类).docx》由会员分享,可在线阅读,更多相关《java编程思想_第六章(复用类).docx(44页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、致读者:我从 2002年7月开始翻译这本书,当时还是第二版。但是翻完前言和介绍部分后,chinapub就登出广告,说要出版侯捷的译本。于是我中止了翻译,等着侯先生的作品。我是第一时间买的这本书,但是我失望了。比起第一版,我终于能看懂这本书了,但是相比我的预期,它还是差一点。所以当Bruce Eckel在他的网站上公开本书的第三版的时候,我决定把它翻译出来。说说容易,做做难。一本1000多页的书不是那么容易翻的。期间我也曾打过退堂鼓,但最终还是全部翻译出来了。从今年的两月初起,到7月底,我几乎放弃了所有的业余时间,全身心地投入本书的翻译之中。应该说,这项工作的难度超出了我的想像。首先,读一本书和

2、翻译一本书完全是两码事。英语与中文是两种不同的语言,用英语说得很畅的句子,翻成中文之后就完全破了相。有时我得花好几分钟,用中文重述一句我能用几秒钟读懂的句子。更何况作为读者,一两句话没搞懂,并不影响你理解整本书,但对译者来说,这就不一样了。其次,这是一本讲英语的人写给讲英语的人的书,所以同很多要照顾非英语读者的技术文档不同,它在用词,句式方面非常随意。英语读者会很欣赏这一点,但是对外国读者来说,这就是负担了。再有,Bruce Eckel这样的大牛人,写了1000多页,如果都让你读懂,他岂不是太没面子?所以,书里还有一些很有“禅意”的句子。比如那句著名的“Thegenesis of the co

3、mputer revolution was in a machine. The genesis of ourprogramming languages thus tends to look like that machine.”我就一直没吃准该怎么翻译。我想大概没人能吃准,说不定Bruce要的就是这个效果。这是一本公认的名著,作者在技术上的造诣无可挑剔。而作为译者,我的编程能力差了很多。再加上上面讲的这些原因,使得我不得不格外的谨慎。当我重读初稿的时候,我发现需要修改的地方实在太多了。因此,我不能现在就公开全部译稿,我只能公开已经修改过的部分。不过这不是最终的版本,我还会继续修订的。本来,我准

4、备到10月份,等我修改完前7章之后再公开。但是,我发现我又有点要放弃了,因此我决定给自己一点压力,现在就公开。以后,我将修改完一章就公开一章,请关注如果你觉得好,请给告诉我,你的鼓励是我工作的动力;如果你觉得不好,那就更应该告诉我了,我会参考你的意见作修改的。我希望能通过这种方法,译出一本配得上原著的书。shhgs2003年9月8日Chapter 6: Reusing Classes6: 复用类Java 最令人心动的特性就是它的代码复用了。但是仅仅拷贝源代码再作修改是不能被称为“革命”的。那是C 之类的过程语言所采用的办法,而且也不怎么成功。就像Java 里的一切,要解决这个问题还要靠类。你可

5、以利用别人写好的、已经测试通过的类来创建新的类,不必一切都从零开始。这么做的诀窍就是,要在不改动原有代码的前提下使用类。本章会介绍两种做法。第一种非常简单:在新的类里直接创建旧的类的对象。这被称为合成(compostion),因为新的类是由旧的类合成而来的。你所复用的只是代码的功能,而不是它的形式。第二种方法更为精妙。它会创建一个新的,与原来那个类同属一种类型的类。你全盘接受了旧类的形式,在没有对它做修改的情况下往里面添加了新的代码。这种神奇的做法就被称为继承(inheritance)。编译器会承担绝大部分的工作。继承是面向对象编程的基石,它还有一些额外的含义,对此我们会在第7章再做探讨。合成

6、与继承在语法和行为上有许多相似之处(这很好理解,因为它们都是在原有类的基础上创建新类)。你会在本章学到这些代码复用的机制。合成所使用的语法实际上我们已经看到很多合成的案例了。只要把对象的reference 直接放到新的类里面就行了。假设,你要创建一个新的类,其中有几个String 对象,几个primitive 数据,以及一个别的什么类型的对象。对于非primitive 的对象,你只要把它的reference 放到类里就行了,但是对于primitive,你就只能直接定义了:/: c06:SprinklerSystem.java/ Composition for code reuse.import

7、 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;private Wa

8、terSource source;private int i;private float f;public String toString() returnvalve1 = + 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 = newSprinklerSystem()

9、;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 而它却是一个对象的时候,编译器就会自动调用这个方法?_酝_庖4。所以当编译器从SprinklerSystem.toString( )的:so

10、urce = + source;中看到,你想把String 同WaterSouce 相加的时候,它就会说“由于String 只能同String 相加,因此我要调用source 的toString( ),因为只有这样才能把它转换成String!”。 于是它就把这两个String 连起来,然后再String 的形式把结果返还给System.out.println( )。如果你想让你写的类也具备这个功能,只要写一个toString( )方法就行了。我们已经在第2 章讲过,当primitive 数据作为类的成员的时候,会被自动地初始化为零。而对象的reference 则会被初始化为null,如果这时,

11、你去调用这个对象的方法,就会得到异常。能把它打印出来而不抛出异常,这真是太好了(而且也很实用)。Chapter 6: Reusing Classes “编译器不为reference 准备默认对象”的这种做法,实际上也是很合乎逻辑的。因为在很多情况下,这么做会引发不必要的性能开销。如果你想对reference 进行初始化,那么可以在以下几个时间进行:1. 在定义对象的时候。这就意味着在构造函数调用之前,它们已经初始化完毕了。2. 在这个类的构造函数里。3. 在即将使用那个对象之前。这种做法通常被称为“偷懒初始化(lazyinitialization)”。如果碰到创建对象的代价很高,或者不是每次都

12、需要创建对象的时候,这种做法就能降低程序的开销了。下面这段程序把这三种办法都演示一遍:/: 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 Tes

13、t monitor = new Test();private String / Initializing at point ofdefinition: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 toStrin

14、g() if(s4 = null) / Delayed initialization:s4 = new String(Joy);returns1 = + 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();System.out.println(b);monitor.expect(new String Inside Bath()

15、,Soap(),s1 = Happy,s2 = Happy,s3 = Joy,s4 = Joy,i = 47,toy = 3.14,castille = Constructed); /:注意,Bath 的构造函数会先打印一条消息再进行初始化。如果你不在定义对象的时候进行初始化,那么没人可以担保,在向这个对象的reference 发送消息的时候,它已经被初始化了反倒是会有异常来告诉你,它还没有初始化,。调用toString( )的时候它会先为s4 赋一个值,这样它就不会未经初始化而被使用了。继承所使用的语法继承是Java(也是所有OOP 语言)不可分割的一部分。实际上当你创建类的时候,你就是在继

16、承,要么是显式地继承别的什么类,要么是隐含地继承了标准Java 根类,Object。合成的语法很平淡,但继承就有所不同了。继承的时候,你得先声明“新类和旧类是一样的。”跟平常一样,你得先在程序里写上类的名字,但是在开始定义类之前,你还得加上extends 关键词和基类(base class)的名字。做完这些之后,新类就会自动获得基类的全部成员和方法。下面就是一个例子:/: c06:Detergent.java/ Inheritance syntax & properties.import com.bruceeckel.simpletest.*;class Cleanser protected

17、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) Clean

18、ser x = new Cleanser();x.dilute(); x.apply(); x.scrub();Chapter 6: Reusing ClassesSystem.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

19、 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 d

20、ilute() apply() +Detergent.scrub() scrub() foam(),Testing base class:,);Cleanser.main(args); /:这段程序能告诉我们很多东西。首先Cleanser 的append( )方法用+= 运算符将Sting 同s 联接起来。Java 的设计者们“重载”了这个操作符,使之能作用于String。第二,Cleanser 和Detergent 都有一个main( )方法。你可以为每个类都创建一个main( ),而且这也是一种值得提倡的编程方法,因为这样一来,测试代码就能都放进类里了。即使程序包括了很多类,它也只会调用你

21、在命令行下给出的那个类的main( )方法。(只要main( )是public 的就行了,至于类是不是public 的,并不重要。)于是,当你输入java Detergent 的时候,它就会调用Detergent.main( )。虽然Cleanser 不是public 的,但是你也可以用java Cleanser 来调用Cleanser.main( )。这种往每个类里都放一个main( )的做法,能让类的单元测试变得更容易一些。做完测试之后,你也不必移除main( );留下它可以供以后的测试用。这里,Detergent.main( )直接调用了Cleanser.main( ),并且把命令行参数

22、原封不动地传给了它(实际上可以使用任何String 数组)。有一点很重要,那就是Cleanser 的方法都是public 的。记住,如果你不写访问控制符,成员就会被默认地赋予package 权限,于是同一个package 内的任何类就都能访问这些方法了。Detergent 没问题。但是,如果别的package 里有一个继承了Cleanser 的类,那它就只能访问Cleanser 的public 的成员了。(我们以后会讲,派生类可以访问基类的protected 的成员。)所以继承设计方面有一条通用准则,那就是把数据都设成private 的,把方法都设成public 的。当然碰到特殊情况还要进行调

23、整,但是这还是一条非常有用的准则。注意,Cleanser 的接口包括了一组方法:append(),dilute(), apply(),scrub(),以及toString()。由于Detergent 是由Cleanser 派生出来的(通过extends 关键词),所以尽管它没有明确地定义这些方法,它还是自动获得了这个接口的所有方法。由此,你可以将继承理解成类的复用。正如scrub( )所揭示的,你可以在派生类里修改一个在基类里的定义的方法。这时,你有可能要在新方法里调用基类的方法。但是你不能在scrub( )里面直接调用scrub( ),因为这是递归,你要的应该不是这个吧。为解决这个问题,Ja

24、va 提供了一个表示当前类所继承的那个“超类(superclass)”的super 关键词。于是super.scrub( )就会调用基类的scrub( )方法了。继承并不会限定你只能使用基类的方法。你也可以往派生类里加进新的方法,就像往普通的类里加方法一样:直接定义就是了。foam( )就是一例。从Degergent.main( )可以看出,Detergent 对象既有Cleanser的方法,也有它自己的方法(就是foam()。基类的初始化现在要创建派生类对象已经不是一个类的事情了,它会牵涉到两个类基类和派生类,因此要搞清楚它究竟是怎么创建的,就有点难度了。从局外人的角度来看,新类具备了和旧类

25、完全相同的接口,并且还有可能会有一些它自己的方法和数据。但继承并不仅仅是拷贝基类的接口。当你创建一个派生类对象的时候,这个对象里面还有一个基类的子对象(subobject)。这个子对象同基类自己创建的对象没什么两样。只是从外面看来,这个子对象被包裹在派生类的对象里面。当然,基类子对象的正确初始化也是非常重要的,而且只有一个办法能保证这一点:调用基类的构造函数来进行初始化,因为只有它才掌握怎样才能正确地进行初始化的信息和权限。Java 会让派生类的构造函数自动地调用基类的构造函数。下面这段程序就演示了它在三级继承体系下是如何运作的:Chapter 6: Reusing Classes/: c06

26、: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

27、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( )的构造函数,编译器也会为你造一个默认的构造函数,然后再由它去调用基类的构造函

28、数。带参数的构造函数在上述例程中,构造函数都是默认的;也就是不带参数的。对编译器来说,调用这种构造函数会非常简单,因为根本就没有要传哪些参数的问题。但是如果类没有默认的构造函数(也就是无参数的构造函数),或者你要调用的基类构造函数是带参数的,你就必须用super 关键词以及合适的参数明确地调用基类的构造函数:/: c06:Chess.java/ Inheritance, constructors and arguments.import com.bruceeckel.simpletest.*;class Game Game(int i) System.out.println(Game cons

29、tructor);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();m

30、onitor.expect(new String Game constructor,BoardGame constructor,Chess constructor); /:如果你不在BoardGame( )里面调用基类的构造函数,编译器就会报错说它找不到Game( ) 形式(译者注:即默认)的构造函数。此外,对派生类构造函数而言,调用基类的构造函数应该是它做的第一件事。(如果你做错了,编译器就会提醒你。)捕获基类构造函数抛出的异常我们刚说了,编译器会强制你将基类构造函数的调用放在派生类的构造函数的最前面。也就是说,在它之前不能有任何东西。等到第9章你就会知道,这么做会妨碍派生类的构造函数捕获基

31、类抛出的异常。这一点有时会很不方便。把合成和继承结合起来同时使用合成和继承的现象是很普遍的。下面这段程序演示了,怎样使用合成和继承,以及利用构造函数来进行初始化这一必不可少的步骤,来创建一个较为复杂的类:/: c06:PlaceSetting.java/ Combining composition & inheritance.import com.bruceeckel.simpletest.*;class Plate Plate(int i) System.out.println(Plate constructor);Chapter 6: Reusing Classesclass Dinner

32、Plate 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);Sys

33、tem.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 T

34、est();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) Plac

35、eSetting x = new PlaceSetting(9);monitor.expect(new String Custom constructor,Utensil constructor,Spoon constructor,Utensil constructor,Fork constructor,Utensil constructor,Knife constructor,Plate constructor,DinnerPlate constructor,PlaceSetting constructor); /:虽然编译器会强制你对基类进行初始化_,并且会要求你在构造函数的开始部分完成初

36、始化,但是它不会检查你是不是进行了成员对象的初始化,因此你只能自己留神了。确保进行妥善地清理拆构函数(destructor)是C+里面的概念,它是一种能在清理对象的时候自动调用的方法,Java 里面没有这种概念。原因可能是Java 处理这类问题的时候,只是简单地把对象放到一边,然后留给垃圾回收器去处理,它不会去主动地进行清理。在大多数情况下,这种做法也很不错,但是有时候,会遇到一些特殊的类,在清理它们的对象的时候会需要进行一些额外的操作。正如第4章所说的,你既不知道垃圾回收器什么时候启动,也不知道它会不会启动。所以如果要进行清理,你就必须明确地写一个专门干这件事的方法,然后告诉客户程序员们去调

37、用这个方法。做了这些还不够到第9章(“用异常处理错误”)还要讲为了应付异常,你还要把它放到finally 子句里面。就拿计算机辅助设计系统举例,我们要在屏幕上画一点东西:/: c06:CADSystem.java/ Ensuring proper cleanup.package c06;import com.bruceeckel.simpletest.*;import java.util.*;class Shape Shape(int i) System.out.println(Shape constructor);void dispose() System.out.println(Shape

38、 dispose);class Circle extends Shape Circle(int i) super(i);Chapter 6: Reusing ClassesSystem.out.println(Drawing Circle);void dispose() System.out.println(Erasing Circle);super.dispose();class Triangle extends Shape Triangle(int i) super(i);System.out.println(Drawing Triangle);void dispose() System.

39、out.println(Erasing Triangle);super.dispose();class Line extends Shape private int start, end;Line(int start, int end) super(start);this.start = start;this.end = end;System.out.println(Drawing Line: + start+ ,+ end);void dispose() System.out.println(Erasing Line: + start+ ,+ end);super.dispose();pub

40、lic class CADSystem extends Shape private static Test monitor = new Test();private Circle c;private Triangle t;private Line lines = new Line5;public CADSystem(int i) super(i + 1);for(int j = 0; j = 0; i-)linesi.dispose();super.dispose();public static void main(String args) CADSystem x = new CADSyste

41、m(47);try / Code and exception handling. finally x.dispose();monitor.expect(new String Shape constructor,Shape constructor,Drawing Line: 0, 0,Shape constructor,Drawing Line: 1, 1,Shape constructor,Drawing Line: 2, 4,Shape constructor,Drawing Line: 3, 9,Shape constructor,Drawing Line: 4, 16,Shape con

42、structor,Drawing Circle,Shape constructor,Drawing Triangle,Combined constructor,CADSystem.dispose(),Erasing Triangle,Shape dispose,Erasing Circle,Shape dispose,Erasing Line: 4, 16,Shape dispose,Erasing Line: 3, 9,Shape dispose,Erasing Line: 2, 4,Shape dispose,Erasing Line: 1, 1,Shape dispose,Erasing

43、 Line: 0, 0,Shape dispose,Shape dispose); /:这个系统里的所有东西都是Shape(而Shape 本身又是Object,因为它是隐含地继承自根类) 。各个类在覆写Shape 的dispose( )方法的时候,除了用super 调用基类的dispose( ) 之外,还要完成它自己的清理活动。具体的Shape 类Circle,Triangle 以及Line都有会把自己“画出来”的构造函数,但实际上,对象的生命周期内调用的任何方法,都可能会造成一些需要进行清理的后果。每个类都有它自己的,用来恢复内存以外的资源状态的dispose( )方法。main( )里面有两个我们要到第9章才

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 教案示例

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com