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