在Java中一个对象既可以作为它自己本身的类型使用,也可以作为它的父类类型使用。而把这种对某个对象的引用视为对其父类类型引用的做法被称作向上转型。
一 向上转型
下面我们看一个例子,有一个父类Instrument,派生的子类中Wind、Stringed、Brass。
package polymorphism.music; class Instrument{ public void play() { System.out.println("Instrument.play()"); } } class Wind extends Instrument{ public void play() { System.out.println("Wind.play()"); } } class Stringed extends Instrument{ public void play() { System.out.println("Stringed.play()"); } } class Brass extends Instrument{ public void play() { System.out.println("Brass.play()"); } } public class Music { public static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Wind w = new Wind(); Stringed s = new Stringed(); Brass b = new Brass(); tune(w); tune(s); tune(b); } }
可以看到输出为:
Wind.play() Stringed.play() Brass.play()
在Music类中tune方法接受的是一个Instrument引用类型,但是运行结果却是调用的实际子类对象中的play()方法。编译器是怎样知道这个Instrument引用指向的是Wind对象、还是Brass、Stringed对象呢?实际上,编译器无法得知。为了理解这个问题,我们介绍一下绑定。
1、方法调用绑定
将一个方法调用与一个方法主体关联起来被称作绑定、如果在程序执行前就进行绑定,叫做前期绑定。如果是在运行时根据对象的类型进行绑定就是后期绑定(动态绑定)。
Java中除了static和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
因此tune函数传入的是Wind对象引用,则tune函数调用时会绑定到Wind类中的play()函数。
二 多态案例
创建一个父类Shape,以及多个子类Circle、Square、Triangle。
package polynorphism.shape; import java.util.*; class Shape{ public void draw() {} public void erase() {} } class Circle extends Shape{ public void draw() { System.out.println("Circle.draw()"); } public void erase(){ System.out.println("Circle.erase()"); } } class Square extends Shape{ public void draw() { System.out.println("Square.draw()"); } public void erase(){ System.out.println("Square.erase()"); } } class Triangle extends Shape{ public void draw() { System.out.println("Triangle.draw()"); } public void erase(){ System.out.println("Triangle.erase()"); } } public class RandomShapeGenerator { private Random rand = new Random(47); public Shape next() { switch(rand.nextInt(3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } } public static void main(String[] args) { RandomShapeGenerator gen = new RandomShapeGenerator(); Shape[] s = new Shape[9]; for(int i=0;i<9;i++) { s[i] = gen.next(); s[i].draw(); } } }
输出:
Triangle.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() Triangle.draw() Circle.draw()
可以看到当调用一个父类方法s[i].draw();虽然s[i]是Shape类型的引用,由于后期绑定的原因,它能够正确调用s[i]所指向对象(比如Circle对象)的draw()方法。
Shape类为所有从它那里继承而来的子类都建立了一个公用接口--也就是说,所有形状都可以描绘和擦除。子类通过覆盖这些定义,来为每种特殊类型的几何形状提供单独的行为。
1、缺陷:”覆盖“私有方法
我们尝试运行一下方法:
public class PrivateOverride { private void f() { System.out.println("private f()"); } public static void main(String[] args) { PrivateOverride po = new Derived(); po.f(); } } class Derived extends PrivateOverride{ public void f() { System.out.println("public f()"); } }
运行结果:
private f()
我们所期望的是输出public f(),但是由于private方法自动被识别认为是final方法,而且对子类是屏蔽的,因此在这种情况下,Derived 类中的f()方法是一个新的方法,和父类的f()方法没有任何关系;既然父类中的f()方法在子类中不可见,因此不能被重载和覆盖(重写),调用po.f()时则执行的是父类的f()方法。
因此总结下来:只有非private方法才可以被覆盖,但是还需要密切注意覆盖private方法的现象,这时编译器虽然不会报错,但是也不会按照我们所期望的来执行。确切来说,在子类中,对于父类的private方法,最好采用不同的名字。
2、缺陷:字段与静态方法
一旦你了解了多态,你可能就会开始认为所有事物都可以多态地发生,然而,只有普通的方法调用可以是多态的。例如:如果你直接访问某个字段,这个访问就将在编译期进行解析,由于多态是动态绑定的体现,所以就无法实现多态,如下程序:
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("sup.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() ); } }
运行结果如下:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
当Sub对象转换为Super引用时,任何字段访问操作都将由编译器解析(即根据对象引用的类型判断调用哪个字段,所以sup.field指的是Super中的field,sub.field指的是Sub中的field),因此不是多态的。在本例中,为Super.field和Sub.field分配了不同的存储空间,这样Sub实际上包含了两个称为field的字段:它自己和它从Super处继承来的。然而,在引用Sub中的field时所产生的默认字段并非Super中的field,因此为了得到Super.field,必须显示的指明super.field。
如果某个方法是静态的,那么它的行为将不具有多态性。这是因为静态方法是与类,而并非单个对象相关联的。
class StaticSuper{ public static String staticGet() { return "Base staticGet"; } public String dynamicGet() { return "Base dynamicGet"; } } class StaticSub extends StaticSuper{ public static String staticGet() { return "Derived staticGet"; } public String dynamicGet() { return "Derived dynamicGet"; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } }
输出结果:
Base staticGet Derived dynamicGet
三 构造器和多态
1、构造器调用顺序
父类的构造器总是在子类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个父类的构造器都能被调用。
如果创建一个子类对象,调用顺序:
- 初始化父类static字段;
- 初始化子类static字段;
- 初始化父类非static字段;
- 调用父类构造函数;
- 初始化子类非static字段;
- 掉用子类构造函数;
下面我们来看一个案例:
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 ProtableLunch extends Lunch{ ProtableLunch(){ System.out.println("ProtableLunch()"); } } public class Sandwish extends ProtableLunch{ private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwish() { System.out.println("Sandwish()"); } public static void main(String[] args) { new Sandwish(); } }
运行结果如下:
Meal() Lunch() ProtableLunch() Bread() Cheese() Lettuce() Sandwish()
2、构造器内部的多态方法行为
构造器调用的层次结构带来了一个有趣的两难问题,如果在一个父类构造器的内部调用某个动态绑定方法(即在子类中进行覆盖的函数),那么将会发生什么?
在一般的方法内部,动态绑定的调用是在运行时才决定。如果要在父类构造器内部调用一个动态绑定方法,就要调用那个方法的被覆盖后的定义(即子类方法重写的方法),然而这个调用的结果是无法预料的,因为被覆盖的方法在对象被完全覆盖之前就会被调用(此时,子类字段还没有初始化,该方法就会被调用)。
class Glyph{ void draw() { System.out.println("Glyph.draw()"); } Glyph(){ System.out.println("Glyph() before draw()"); //多态方法、子类进行了重写(动态绑定) draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph{ private int radius = 1; RoundGlyph(int r){ radius = r; System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius); } void draw() { System.out.println("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.RoundGlyph(), radius = 5
new RoundGlyph(5)的执行顺序:
- 调用父类Glyph构造函数
- 执行System.out.println("Glyph() before draw()");
- 执行draw()函数,由于该函数在子类中进行了重写,是一个动态绑定方法,所以调用的是子类的draw()函数,由于此时子类的字段还没有初始化,所以radius不是1,而是0;
- System.out.println("Glyph() after draw()");
- 初始化子类字段radius = 1;
- 调用子类构造函数;
因此为了避免上面这种情况出现:在编写父类构造器的时候遵循一个规则,"用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法"。在父类构造器内唯一能够安全调用的那些方法是父类中的final方法(或者private方法),这些方法在子类中不能被覆盖,因此也不会出现上面radius=0的结果。
四 协变返回类型
Java SE5中添加了协变返回类型,它表示在子类中的被覆盖方法 可以返回 父类方法的返回类型的某种子类类型。
//协变返回类型 https://www.cnblogs.com/zyly/p/10550725.html 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
Java SE5与Java较早版本之间的主要差异就是较早版本将强制process()的覆盖版本必须返回是Grain,而不能返回Wheat,尽管Wheat是从Grain继承来的,因而也是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。
对于编译无法通过的,可能是由于版本选择有问题,点击项目下的JRE System Library右键属性 -> Execution environment 选择J2SE-1.5以上版本。
参考文献:
[1]Java编程思想