第八章 多态(下)
8.2.4 缺陷:“覆盖”私有方法
package com.example.demo; public class PrivateOverride { private void f() {System.out.println("private f()");} public static void main(String[] args) { PrivateOverride po = new PrivateOverride(); po.f(); } } class Derived extends PrivateOverride { public void f() {System.out.println("public f()");} }
我们所期望的输出是public f(),但是由于private方法被自动认为是final方法,而且对导出类是屏蔽的,所以在这种情况下,Derive类中的f()方法就是一个全新的方法,既然基类的f()方法在子类中不可见,因此也就不能被重载。
结论就是:只有非private方法才可以被覆盖。
8.2.5 缺陷:域与静态方法
一旦你了解了多态机制,可能会开始认为所有事物都可以发生多态,然而只有普通的方法调用是可以多态的,例如如果你直接访问某个域,这个访问就将在编译器进行解析,如下:
package com.example.demo; class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } } public class FieldAccess { public static void main(String[] args) { Super sup = new Sub() System.out.println("sub.field = " + sup.field + ". sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ". sub.getField() = " + sub.getField() + ". sub.getSuperField() = " + sub.getSuperField()); } }
输出结果如下:
sub.field = 0. sup.getField() = 1
sub.field = 1. sub.getField() = 1. sub.getSuperField() = 0
当Sub对象转型为Super时,任何域访问操作都将由编译器解析,因此不是多态的,在本例中,为Super.ield和Sub.field分配了不同的存储空间,这样Sub实际上包含两个称为field的城,它自己和从Super得到的,然而在引用Sub中的field时所产生的默认城并非Super版本的field域,因此要得到Super.field就必须显式指明super.field。
8.3 构造器和多态
通常构造器不同于其他种类的方法,涉及到多态时扔是如此,尽管构造器并不具有多态性,但还是有必要理解构造器怎么样通过多态在复杂的层次结构中运作。
8.3.1 构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。如果没有明确指定调用某个基类的构造器,它会调用默认构造器,如果不存在默认构造器,那么编译器就会报错。
package com.example.demo; class Meal { Meal() { System.out.println("Meal()"); } } class Bread { Bread() { System.out.println("Bread()"); } } class Cheese { Cheese() { System.out.println("Cheese()");} } class Lettuce { Lettuce() { System.out.println("Lettuce()"); } } class Lunch extends Meal { Lunch() { System.out.println("Lunch()"); } } class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()");} } public class Sandwich extends PortableLunch{ private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwich() {System.out.println("Sandwich()");} public static void main(String[] args) { new Sandwich(); } }
输出结果为:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
在上面这个例子汇总,用其他类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器,其中最重要的类是Sandwich,它反映了三层继承已经三个成员对象,当在main()中创建一个Sandwich对象后没救可以看到输出结果。
这一系列复杂对象调用结构要遵照下面的顺序:
1、调用基类构造器
2、按声明顺序调用成员初始化方法
3、调用导出类构造器的主体
8.3.2 继承与清理
通过组合和继承方法来创建新类时,不必担心对象的清理文理,子对象通常都会留给垃圾回收器进行处理,如果清理确实遇到了问题,那就必须为新类创建dispose方法,由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose方法,当覆盖被继承类的dispose方法时,记住要调用基类版本的dispose方法,否则基类的清理动作就不会发生。
8.3.3 构造器内部的多态方法的行为
构造器调用的层次结构带来一个两难的问题:如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,会发生什么情况?
在一般的方法内部,动态绑定的调用是在y/unxing时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义,然而这个调用的效果可能难于预料,被覆盖的方法在对象完全被构造之前就会被调用,这可能会造成以下难于发现的隐蔽错误。
从概念上讲,构造器的工作实际上就是创建对象,在任何构造器内部,整个对象可能只是部分形成,我们只知道基类对象已经进行初始化。如果构造器只是在构件对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么到导出部分在当前构造器正在被调用的时候仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调出导出类里的方法。
8.4 协变返回类型
Java SE5中添加了协变返回类型,它表示在导出类中被覆盖的方法可以返回基类方法的返回类型的某种导出类型:
package com.example.demo; class Grain { public String toString() { return "Grain"; } } class Wheat extends Grain { public String toString() { return "Wheat"; } } class Mill { Grain process() { return new Grain(); } } class WheatMill extends Mill { Wheat process() { return new Wheat(); } } public class CovariantReturn { public static void main(String[] args) { Mill m = new Mill(); Grain g = m.process(); System.out.println(g); m = new WheatMill(); g = m.process(); System.out.println(g); } }
输出结果如下:
Grain
Wheat
8.5 用继承进行设计
学习多态之后,看起来似乎所有的东西都可以被继承,多态是一种巧妙的工具,但事实上当我们使用现成的类来建立新类的时候,如果首先考虑使用继承技术,反而会加重我们的设计负担。更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构中,而且组合更加灵活。
package com.example.demo; class Actor { public void act() {} } class HappyActor extends Actor{ public void act() { System.out.println("HappyActor"); } } class SadActor extends Actor{ public void act() { System.out.println("SadActor"); } } class Stage { private Actor actor = new HappyActor(); public void change() { actor = new SadActor(); } public void performPlay() { actor.act(); } } public class Transmogrify { public static void main(String[] args) { Stage stage = new Stage(); stage.performPlay(); stage.change(); stage.performPlay(); } }
输出结果如下:
HappyActor
SadActor
在这里,Stage对象包含一个队Actor的引用,而Actor被初始化为HappyActor对象。
8.5.1 纯继承与扩展
采取纯粹的方式来创建继承层次结构似乎是最好的方式,只有在基类中已经建立的方法才可以在导出类中被覆盖,如下图所示:
这被称作是纯粹的继承“is-a”关系,因为一个类的接口已经确定了它应该是什么,继承可以确保所有的导出类具有基类的接口,按上图那么做,导出类也将具有和基类一样的接口。也可以认为这是一种纯替代,因为导出类可以完全替代基类,而在使用它们时完全不需要知道子类的任何额外信息:
基类可以接受发送给导出类的任何消息,因为二者有着完全相同的接口,我们只需从导出类向上转型,而不需要知道正在处理对象的确切类型,这一切都是通过多态来实现的。
我们还有一种“is-like-a”的关系,导出类有着相同的基本接口,但是它还具有额外方法实现的其他特性。
虽然这是一种有用的方法但是也有缺点,导出类中接口的扩展部分不能被基类访问,因此,一旦我们向上转型,就不能调用那些新方法:
8.5.2 向下转型与运行时类型识别
由于向上转型会丢失具体的类型信息,所以我们就像通过向下转型获取类型信息,然而我们知道向上转型是安全的,但是对于向下转型,我们无法确定。
在Java语言中,所有转型都会得到检查,所以即使我们只是进行一次普通的加括弧形式的类型转换,在进行运行期间仍然会对其进行检查,以保证它的确是我们希望的那种类型,如果不是,就会返回一个ClassCastException类转型异常,这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)