第八章 多态
- 多种类型(从同一基类导出的)视为同一类型来处理
- 同一份代码也就可以毫无差别地运行在这些不同类型之上了
8.1 再论向上转型
- 对象可以作为自己本身使用,也可以作为它的基类型使用
- 把对某个对象的引用视为对基类型的引用的做法被称作向上转型
8.1.1 忘记对象类型public class Car { public void move() { System.out.println("嘟嘟嘟~只要是个车就能跑的嘟嘟嘟"); } } public class Jeep extends Car{ @Override public void move() { System.out.println("嘟嘟嘟~直接上山了"); } } public class Person { public void drive(Car car) { car.move(); } public static void main(String[] args) { // Jeep继承自Car,不需要任何转换,可以运行 Jeep jeep = new Jeep(); drive(jeep); } }
练习1、创建一个Cycle类,它具有子类Unicycle、Bicycle、Tricycle,演示它们都可以经由iride()方法向上转型为Cycle
- 如果让drive(Car car)方法接受Jeep的引用,看起来更直观
- 但是如果我新添加了若干种车,比如:宝马、奔驰、奥迪、比亚迪等等
- 那么还得再为它们写对应的drive方法,那么就需要更多的编程,做大量的工作
public class Cycle { } public class Unicycle extends Cycle { } public class Bicycle extends Cycle { } public class Tricycle extends Cycle { } public class E01_Upcasting { public static void ride(Cycle c) {} public static void main(String[] args) { ride(new Cycle()); // No upcasting ride(new Unicycle()); // Upcast ride(new Bicycle()); // Upcast ride(new Tricycle()); // Upcast } }
8.2 转机
8.2.1 方法调用绑定
- 编译器如何知道这个Car引用指向的就是Jeep对象,而不是奥迪、宝马等对象呢,其实编译器无法得知
8.2.2 产生正确的行为
- 将一个方法调用同一个方法主体关联起来被称作绑定
- 程序执行前进行绑定,叫做前期绑定
- 程序运行时进行绑定,叫做后期绑定,在对象中安置了某种信息
- Java中除了static和final方法(包括private)以外所有方法都是后期绑定
- 将某个方法声明为final,可以防止其他人覆盖该方法,“关闭”动态绑定,生成更有效的代码
- 大多数情况下并不会对程序的性能有什么提升,所以最好是根据设计而不是性能来使用final
// 向上转型可以这么简单 Car car = new Jeep(); // 如果调用一个基类的方法,可能认为是调用的父类对象,实际上是正确的调用了Jeep.move(); car.move();
Car基类为所有的导出类都建立了一个公共接口,所有车都可以移动,导出类通过覆盖这些定义,为每种不同的车型提供单独的move行为
练习2、在几何图形的示例中添加@Override注解
略
练习3、在基类中添加一个新方法,导出类不覆盖,其中一个覆盖,最后都覆盖,看看发生了什么
// 车的父类 public class Car { public void move() { System.out.println("是个车子就能动弹"); } } // 人 public class Person { public static void main(String[] args) { Audi audi = new Audi(); Jeep jeep = new Jeep(); audi.move(); jeep.move(); } } ———————————————————————————————————————————————————————————— // 奥迪不覆盖move方法 public class Audi extends Car { // @Override // public void move() { // System.out.println("小奥迪,嗖嗖快"); // } } // Jeep不覆盖move方法 public class Jeep extends Car { // @Override // public void move() { // System.out.println("大Jeep,直接跑上山"); // } } // 运行结果 是个车子就能动弹 是个车子就能动弹 —————————————————————————————————————————————————————————————— // 奥迪覆盖move方法 public class Audi extends Car { @Override public void move() { System.out.println("小奥迪,嗖嗖快"); } } // Jeep中依旧不覆盖 public class Jeep extends Car { // @Override // public void move() { // System.out.println("大Jeep,直接跑上山"); // } } // 运行结果 小奥迪,嗖嗖快 是个车子就能动弹 ——————————————————————————————————————————————————————————————— // 奥迪覆盖move方法 public class Audi extends Car { @Override public void move() { System.out.println("小奥迪,嗖嗖快"); } } // Jeep覆盖move方法 public class Jeep extends Car { @Override public void move() { System.out.println("大Jeep,直接跑上山"); } } // 运行结果 小奥迪,嗖嗖快 大Jeep,直接跑上山 ——————————————————————————————————————————————————————————————— 结论: 如果不覆盖也可以调,调的是基类的方法 如果覆盖了,调的就是覆盖后的方法
练习4、向Shape.java中添加一个新的Shape类型,并在main()方法中验证,多态对于新类型的作用是否和旧类型中的一样
略
练习5、以练习1为基础,在Cycle中添加wheels()方法,返回轮子的数量,修改ride()方法,调用wheels()方法,证明多态起作用了
8.2.3 可扩展性public class Cycle { public int wheels() { return 0; } } public class Unicycle extends Cycle { public int wheels() { return 1; } } public class Bicycle extends Cycle { public int wheels() { return 2; } } public class Tricycle extends Cycle { public int wheels() { return 3; } } public class E01_Upcasting { public static void ride(Cycle c) { System.out.println("车轮子数为:" + c.wheels()); } public static void main(String[] args) { ride(new Cycle()); // No upcasting ride(new Unicycle()); // Upcast ride(new Bicycle()); // Upcast ride(new Tricycle()); // Upcast } } // 运行结果 车轮子数为:0 车轮子数为:1 车轮子数为:2 车轮子数为:3
- 如果上面的Car的例子中添加一个drift()漂移的方法,我们添加新的方法并不会影响drive()方法去调用move()方法
- 我们所做的代码修改,不会对程序中其他不应受到影响的部分产生破坏
- 多态让程序员将“改变的事物与未变的事物分离开来”
练习6、修改Music3.java,使what()方法成为根Object的toString()方法,并打印出Instrument对象
证明了每一个对象调用了它们自己相应的toString()方法
练习7、向Music3.java添加一个新的类型Instrument,并验证多态性是否作用于所添加的新类型
毫无疑问,肯定作用于新类型
练习8、修改Music3.java,使其可以像Shape.Java中的方法那样随机创建Instrument对象
略
练习9、创建Rodent:老鼠,鼹鼠,大颊鼠等等这样一个继承结构
略了,和上文提到的Car的例子一样
练习10、创建一个包含两个方法的基类,第一个方法中可以调用第二个方法,然后产生一个继承自该基类的导出类,且覆盖基类中的第二个方法,为该导出类创建一个对象,将它向上转型到基类并调用第一个方法,解释发生的情况
8.2.4 缺陷:“覆盖”私有方法public class Car { public void move() { System.out.println("是个车子就能动弹"); brokeDown(); } public void brokeDown() { System.out.println("车抛锚了,尼玛币车胎炸了!!!"); } } public class Jeep extends Car { @Override public void brokeDown() { System.out.println("大Jeep上山被大石头给干废了!"); } } public class Person { public static void main(String[] args) { Jeep jeep = new Jeep(); jeep.move(); } } // 运行结果 是个车子就能动弹 大Jeep上山被大石头给干废了! // 这个例子我要专门写一下,因为我当初并不理解是如何调用的 对于 Car car = new Jeep(); car.move(); 我可以理解,一定是调用的Jeep.move()方法 而对于现在这个例子,一开始我是捉摸不透的,以为move()中调用的还是基类的brokeDown()方法, 但是明显结果并不是,结果是调用的子类的方法 ——————————————————————————————————————————————————————————————————— 当时是这么一个场景,BaseListFragment类中的上拉加载中调用了appendData()方法, 但是基类中的appendData()并不能满足需求,需要被重写,我当时并不了解这些基础, 有一个疑问:父类的这个上拉加载会不会调用我重写的appendData()方法呢?所以我陷入了困境, 当时的想法竟然是不行那我就在实现类中再写一个上拉加载的监听,然后调用我重写的appendData(), 那么这两个方法都是在同一个类中的方法就肯定可以调用了, 那么当时的想法真是幼稚,简直是没事给自己多加负担,Java还需要你写这么复杂吗? Java总是使用派生最多的方法作为对象类型 说白了就是父类中各种调用方法,会使用你当前对象所能感知到的最新的覆盖过的方法
- 基类中的private方法无法被覆盖,在子类中,对于基类中的private方法,最好采用不同的名字
8.2.5 缺陷:域或静态方法public class Car { private void turnOnTheLight() { System.out.println("把灯开啦"); } } public class Audi extends Car { public void turnOnTheLight() { System.out.println("小奥迪打开了个好看的大灯"); } } ———————————————————————————————————————————————————————————————————— // 声明为基类的引用 public class Person { public static void main(String[] args) { Car car = new Audi(); // eclipse直接报错,不能这么写 car.turnOnTheLight(); } } // 运行结果 按书中说的结果应该是去执行基类的方法,输出:把灯开啦 但是我用eclipse编译直接报错,无法运行,提示你必须更改private访问权限 ——————————————————————————————————————————————————————————————————— 声明为子类的引用 public class Person { public static void main(String[] args) { Audi car = new Audi(); car.turnOnTheLight(); } } // 运行结果 执行了子类的方法: 小奥迪打开了个好看的大灯
- 域不是多态的,和方法是不一样的,举个例子
public class Car { public int price; public int getPrice() { return price; } } public class Audi extends Car { public int price = 300; public int getPrice() { return price; } } public class Person { public static void main(String[] args) { Car car = new Audi(); System.out.println("car.price=" + car.price); System.out.println("car.getPrice=" + car.getPrice()); Audi audi = new Audi(); System.out.println("audi.price=" + audi.price); System.out.println("audi.getPrice=" + audi.getPrice()); } } // 运行结果 car.price=0 car.getPrice=300 audi.price=300 audi.getPrice=300 // 如果是声明父类的引用,会调用父类的变量,由此可见域是没有多态的,方法有多态 // 如果是直接声明子类的引用,那么跟父类就没什么关系了 // 这里还有一个要注意的就是,声明父类的引用,就不能直接调用子类新添加的方法了,如果实在想调,就强制转型
- 静态方法也不具有多态性
- 静态属性,静态方法,和非静态属性都不具有多态性
- 简单说,域和静态方法都不具有多态性
public class Car { public static void fly() { System.out.println("车子能不能飞,得看你是啥车"); } public void move() { System.out.println("是个车子就能动弹"); } } public class Audi extends Car { public static void fly() { System.out.println("我这个奥迪车,好像够呛能飞"); } @Override public void move() { System.out.println("小奥迪跑的嗖嗖的"); } } public class Person { public static void main(String[] args) { Car car = new Audi(); car.fly(); car.move(); Audi audi = new Audi(); audi.fly(); car.move(); } } // 运行结果 车子能不能飞,得看你是啥车 小奥迪跑的嗖嗖的 我这个奥迪车,好像够呛能飞 小奥迪跑的嗖嗖的 // 可以看出来静态方法没有多态,静态方法是与类,而非与单个对象绑定的
8.3 构造器和多态
8.3.1 构造器调用顺序
- 基类的构造器总是在导出类的构造过程中被调用,按照继承层次逐渐向上链接
- 如果没有明确指定调用某个基类的构造器,它就会默默地调用默认构造器
class Bread { Bread() { print("Bread()"); } } class Cheese { Cheese() { print("Cheese()"); } } class Lettuce { Lettuce() { print("Lettuce()"); } } class Meal { Meal() { print("Meal()"); } } class Lunch extends Meal { Lunch() { print("Lunch()"); } } class PortableLunch extends Lunch { PortableLunch() { print("PortableLunch()"); } } class Sandwich extends PortableLunch { private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); private Sandwich() { print("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } // 运行结果 Meal() Lunch(); PortableLunch(); Bread(); Cheese(); Lettuce(); Sandwich(); // 这题刚看的时候突然忘了继承这一回事了,想想这章讲的多态,也是醉了
练习11、向Sandwish().java中添加Pickle类
没啥说的,直接看上面的例子吧
8.3.2 继承与清理
- 销毁的顺序应该与初始化的顺序相反
练习12、修改练习9,使其能够演示基类和导出类的初始化顺序,然后向基类和导出类中添加成员对象,说明构建起见的初始化顺序
略
练习13、在ReferenceCounting.java中添加一个finalize()方法,用来校验终止条件
略
练习14、修改练习12,使得其某个成员变为具有引用计数的共享对象,并证明它可以正确运行
略
8.3.3 构造器内部的多态方法的行为
- 如果构造器的内部调用正在构造的对象的某个动态绑定方法
- 比如或基类的构造器中调用一个被覆盖的方法
- 被覆盖的方法在对象完全构造之前就被调用,可能会造成一些难于发现的隐藏错误
class Glyph { Glyph() { print("Glyph() before draw()"); draw(); print("Glyph() after draw()"); } void draw( ) { print("Glyph.draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; public RoundGlyph(int r) { radius = r; print("RoundGlyph(),radius = " + radius); } void draw() { print("RoundGlyph.draw(),radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } // 运行结果 Glyph() before draw() RoundGlyph.draw(),radius = 0 Glyph() after draw() RoundGlyph(),radies = 5; // 这个结果导致了对RoundGlyph的调用,看起来似乎是我们的目的, // 但是输出结果并不正确,出现了bug
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零
- 由于步骤1的缘故,我们发现radius的值为0
- 按照声明的顺序调用成员的初始化方法
- 调出导出类的构造器主体
尽可能的用简单的方法使对象进入正常状态
避免调用其他方法,能安全调用的是final方法(private方法)
练习15、在PolyConstructors.java中添加一个RectangularGlyph,并证明会出现本节所描述的问题
略
8.4 协变返回类型
这个书中说的有点绕,大体意思就是class Shop { Audi buyCar() { return new Audi(); } } Shop shop = new Shop(); Car car = shop.buyCar(); // Java SE5之前的版本必须返回Car的对象,尽管Audi是Car的子类也不允许返回 // 那么现在可以了,正常shop.buyCar()返回了一个Audi对象
8.5 用继承进行设计
组合更加灵活,优先选择组合
练习16、遵循Transmogrify,java这个例子,创建一个Starship类,包含一个AlertStatus引用,此引用可以指示三种不同的状态,纳入一些可以改变这些状态的方法
8.5.1 纯继承与扩展class AlertStatus { public String getStatus() { return "None"; } } class RedAlertStatus extends AlertStatus { public String getStatus() { return "Red"; }; } class YellowAlertStatus extends AlertStatus { public String getStatus() { return "Yellow"; }; } class GreenAlertStatus extends AlertStatus { public String getStatus() { return "Green"; }; } class Starship { private AlertStatus status = new GreenAlertStatus(); public void setStatus(AlertStatus istatus) { status = istatus; } public String toString() { return status.getStatus(); } } public class E16_Starship { public static void main(String args[]) { Starship eprise = new Starship(); System.out.println(eprise); eprise.setStatus(new YellowAlertStatus()); System.out.println(eprise); eprise.setStatus(new RedAlertStatus()); System.out.println(eprise); } } // 运行结果 Green Yellow Red // 完全可以体现出尽量用组合的观点
8.5.2 向下转型
- 纯继承就是完全和基类一样,是一个(is-a)的关系
- 扩展就是在基类的基础上增加额外信息,像一个(like-a)的关系
- 扩展导致扩展部分不能被基类所访问
练习17、使用练习1中的Cycle的层次结构,在Unicycle和Bicycle中添加balance()方法,而Tricycle中不添加,创建所有这三种类型的实例,并将它们向上转型为Cycle数组,数组的每一个元素上都尝试调用balance(),并观察结果,然后将它们向下转型,再次调用balance(),并观察将发生什么public class Car { public void move() { System.out.println("是个车子就能动弹"); } } public class Audi extends Car { @Override public void move() { System.out.println("小奥迪跑的嗖嗖的"); } public static void fly() { System.out.println("我这个奥迪车,好像够呛能飞"); } } public class Person { public static void main(String[] args) { Car car = new Car(); Car audi = new Audi(); // 转型失败,返回一个ClassCastException异常 ((Audi)car).fly(); // 转型成功 ((Audi)audi).fly(); } } // 如果是声明父类的引用,创建子类的实例,那么可以向下转型 // 如果声明父类的引用,创建了父类的实例,那么就是一个父类的对象,无法向下转型
public class E17_RTTI { public static void main(String[] args) { Cycle[] cycles = new Cycle[]{ new Unicycle(), new Bicycle(), new Tricycle() }; // Compile time: method not found in Cycle: // cycles[0].balance(); // cycles[1].balance(); // cycles[2].balance(); ((Unicycle)cycles[0]).balance(); // Downcast/RTTI ((Bicycle)cycles[1]).balance(); // Downcast/RTTI ((Unicycle)cycles[2]).balance(); // Exception thrown } } public class Unicycle extends Cycle { public void balance() {} } public class Bicycle extends Cycle { public void balance() {} } ———————————————————————————————————————————————————————————————————— // 上面是官方答案,我再用我写的车子写个例子 // 车的基类 public class Car { } // 奥迪 public class Audi extends Car { public void move() { System.out.println("小奥迪跑的嗖嗖的"); } } // Jeep public class Jeep extends Car { public void move() { System.out.println("大Jeep直接开上山"); } } // 宝马 public class Bmw extends Car { } public class Person { public static void main(String[] args) { Car[] cars = new Car[] {new Audi(),new Jeep(),new Bmw()}; // cars[0].move(); // cars[1].move(); // cars[2].move(); ((Audi)cars[0]).move(); ((Jeep)cars[1]).move(); ((Jeep)cars[2]).move(); } } // 运行结果分析 如果直接调用,因为是声明的父类的引用,根本就找不到move()方法 cars[0]和cars[1]转为Audi和Jeep没问题 但是cars[2]本身是Bmw的对象,尽管声明为父类的引用,但是想要转成Jeep,那指定不可以
8.6 总结
多态意味着“不同的形式”,多态可以带来很多的成效,更快的程序开发过程、更好的代码组织、更好扩展的程序以及更容易的代码维护等