向上类型转换
对象既可以作为它自己本身的类型使用,也可以作为它的基本类型使用。这种吧对某个对象的引用视为对其基类型的引用的做法被称为向上类型转换—因为在继承树的画法中,基类放置在上方。
看下面的例子:
enum Note{
MIDDLE_C,C_SHARP,B_FLAT;
}
class Instrument{
public void Play(Note n) {
System.out.println("Instrument.play()");
}
}
class Wind extends Instrument{
@Override
public void Play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
i.Play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
}
}
Misic.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument的类,当Wind引用传递到tune中时,会出现向上类型转换。因为Wind从Instrumen继承而来,所以Instrumemt的接口必定存在于Wind中,从Wind向上转型到Instrument可能会“缩小”接口。
在向上类型转换时,如果让tune直接接受一个Wind引用作为自己的参数,似乎会更为直视,但如果那样做,就需要为系统内Instrument的每种类型都编写一个新的tune方法,如下所示:
class Stringed extends Instrument{
@Override
public void Play(Note n) {
System.out.println("Stringed.play()");
}
}
class Brass extends Instrument{
@Override
public void Play(Note n) {
System.out.println("Brass.play()");
}
}
public class Music {
public static void tune(Wind w) {
w.Play(Note.MIDDLE_C);
}
public static void tune(Stringed s) {
s.Play(Note.MIDDLE_C);
}
public static void tune(Brass b) {
b.Play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
}
}
这样做的缺点是必须为每一个新的Instrument类编写特定类型的方法,这意味着在开始就需要更多的编程,也意味着如果以后想添加类似tune的新方法或者增加Instrument导出的新类,仍需要做大量的工作。而多态的实现就可以实现只写一个方法,仅接收基类作为参数,而不是那些特殊的导出类。
观察该方法:
public static void tune(Instrument i) {
i.Play(Note.MIDDLE_C);
}
不禁要问,tune方法接收一个Instrument类型的引用,但是编译器如何知道该引用指向的是Wind对象,而不是Brass对象或Stringed对象呢?这也是“绑定”所研究的问题。
方法调用绑定
将一个方法调用和一个方法主体关联起来称作绑定,分为前期绑定和后期绑定。
C语言中,只有一种方法调用,那就是前期绑定,在程序执行前已完成了绑定。
而前文中提到的问题,在编译期间无法知道Instrument类型引用的指向,解决该问题的关键就是后期绑定,也就是在程序运行时根据对象的类型进行绑定,也叫做动态绑定或运行时绑定。
java中除了static和final方法,其余均为后期绑定。但是final方法可以有效地关闭动态绑定,让编译器不需要对其进行动态绑定。
请看下面的例子,基类(shape),导出类(Circle、Square、Triangle)
向上类型转换
Shape s = new Shape();
创建了一个Circle对象,并把得到的引用赋值给shape,当调用s.draw方法时,可能任务调用的是Shape中的draw,而由于动态绑定的机制,实际调用的是Circle.draw方法。
class Shape{
public void draw(){}
public void erase() {}
}
class Circle extends Shape{
public void draw(){System.out.println("Circle.draw()");}
}
class Square extends Shape{
public void draw(){System.out.println("Square.draw()");}
}
class Triangle extends Shape{
public void draw(){System.out.println("Triangle.draw()");}
}
class RandomShapeGenerator{
private Random random = new Random(5);
public Shape nextsShape() {
switch (random.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
public class Shapes {
private static RandomShapeGenerator shape = new RandomShapeGenerator();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Shape s = shape.nextsShape();
s.draw();
}
}
}
执行结果:
Triangle.draw()
Square.draw()
Triangle.draw()
Triangle.draw()
Circle.draw()
注意我们在调用shape.nextsShape()时,是不可能知道具体类型是什么的,只有程序运行时才会明确,在调用时我们只是获得了一个Shape引用,当程序运行至s.draw,与类型相关的方法被神奇的调用。编译器不需要获得任何类型信息就能进行正确地调用,对draw方法的所有调用都是通过动态绑定进行的。
可扩展性
在上面的Instrument实例中,由于多态机制,我们可根据自己的需求对系统添加任意多的新类型,而不需要更改tune方法。当我们向基类中添加更多方法或加入一些新的导出类后,不需修改tune方法,也可以正常运行。
class Instrument2{
void play(Note n){System.out.println("Instrument.play "+n);}
String what(){return "Instrument";}
void adjust() {System.out.println("adjusting Instrument");}
}
class Wind2 extends Instrument2{
void play(Note n){System.out.println("Wind.play "+n);}
String what(){return "Wind";}
void adjust() {System.out.println("adjusting Wind");}
}
class Percussion extends Instrument2{
void play(Note n){System.out.println("Percussion.play "+n);}
String what(){return "Percussion";}
void adjust() {System.out.println("adjusting Percussion");}
}
class Stringed2 extends Instrument2{
void play(Note n){System.out.println("Stringed.play "+n);}
String what(){return "Stringed";}
void adjust() {System.out.println("adjusting Stringed");}
}
class Woodwind extends Wind2{
void play(Note n){System.out.println("Woodwind.play "+n);}
void adjust() {System.out.println("adjusting Woodwind");}
}
class Brass2 extends Wind2{
void play(Note n){System.out.println("Brass.play "+n);}
void adjust() {System.out.println("adjusting Brass");}
}
public class Music2 {
public static void main(String[] args) {
Instrument2[] is = {new Wind2(),new Percussion(),new Stringed2(),new Brass2(),new Woodwind()};
tuneAll(is);
}
public static void tune(Instrument2 i) {
i.play(Note.MIDDLE_C);;
}
public static void tuneAll(Instrument2[] is) {
for (Instrument2 i : is) {
tune(i);
}
}
}
执行结果:
Wind.play MIDDLE_C
Percussion.play MIDDLE_C
Stringed.play MIDDLE_C
Brass.play MIDDLE_C
Woodwind.play MIDDLE_C
在继承中,基类中被命名为private的方法不可以被override,但有时导出类中的方法与基类中的private方法名称相同,此时不是override,而是两个不同的方法,为避免这种情况,在导出类中,对于基类中的private方法,最好采用不同的名字。
我们所说的多态只是针对普通的方法调用,而非域,加入直接访问某个域,这个访问就将在编译期间进行解析,下面的例子很好的说明这个问题:
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);
System.out.println("sup.field = " + sup.field + " sup.getField() = "+ sup.getField());
}
}
结果:
com.wkx.eight.polymorphism.Sub@1fc4bec
sup.field = 0 sup.getField() = 1
疑惑:
- sup.field为什么不是Sub类中的1?
因为Super sup = new Sub();被声明为Super类型,在编译和运行期间,所访问的域为Super类,而多态机制仅是针对普通的方法调用,在访问域时不存在动态绑定。 - 假如将Super类中的field变量设置为private会出现什么问题?
如上面所说,sup 为Super类型的引用,如果把Super类中的field设置为private,则在Super类外不可见,此时编译报错。 - 去掉Super类中的getSuperField()方法会出现什么问题?
由于动态绑定,我们知道sup.getField在执行时调用的是Sub类中的方法,但是在编译期间是不存在动态绑定的,此时如果Super类中没有getField方法时,也会出现编译不通过的错误,因为在编译期间,编译器只知道这个地方访问的是Super类中的getField方法。
另外,如果某个方法是静态的,则它的行为就不具有多态性,因为静态方法是与类关联的,而非单个对象。
构造器和多态
构造器不具有多态性,构造器实际为static方法。
基类的构造总是在导出类构造时被调用,按照继承顺序依次向上调用,保证每个基类的构造器均能被调用。
一个复杂对象调用构造器的顺序如下:
- 调用基类构造器,不断反复递归,首先是构造这种层次结构的根,然后是导出类,直到最低层的导出类;
- 按声明顺序调用成员的初始化方法;
- 调用导出类构造器的主体;
类清理的顺序应该与初始化顺序相反,对于成员,意味着与声明的顺序相反。对于基类,应该首先对其导出类进行清理,然后才是基类,因为导出类的清理可能会调用基类中的某些方法。虽然平时在编码时不需要执行清理动作,但是一旦选择要清理时必须按上述规则清理。
一个问题:基类的构造方法中调用了该基类中的动态绑定方法问题,示例:
class Glyph{
int i = 4;
public Glyph() {
run();
show();
}
void show(){
System.out.println("Glyph.show " + i);
}
void run(){
System.out.println("Glyph.run " + i);
}
}
class RoundGlyph extends Glyph{
int i =5;
RoundGlyph(){
show();
}
void show(){
System.out.println("RoundGlyph.show " + i);
}
}
public class PolyConstructors {
public static void main(String[] args) {
Glyph g = new RoundGlyph();
g.show();
}
}
执行结果:
Glyph.run 4
RoundGlyph.show 0
RoundGlyph.show 5
RoundGlyph.show 5
- 首先初始化Glyph类,加载Glyph()方法,调用run()方法,该方法在导出类中没有被重写,所以显示为Glyph.run 4
- 由于Glyph中的show方法被RoundGlyph类覆写,所以调用RoundGlyph类的show()方法,而此时RoundGlyph类中的i还没有被初始化为5,仍然为默认初始化值0,所以打印为RoundGlyph.show 0
- 接着调用RoundGlyph类中的show()方法,此时i被初始化为5,RoundGlyph.show 5
- 调用RoundGlyph类的show方法,i已经被初始化为5了,所以打印RoundGlyph.show 5