多态:也称动态绑定、后期绑定或运行时绑定。
对象既可以作为它本身的类型使用,也可以作为它的基类使用。
把对象的引用视为对其基类的引用的做法称为“向上转型”。
对于“向上转型”,参看下面的例子:
//: Music.java // Inheritance & upcasting package c07; class Note { private int value; private Note(int val) { value = val; } public static final Note middleC = new Note(0), cSharp = new Note(1), cFlat = new Note(2); } // Etc. class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } } // Wind objects are instruments // because they have the same interface: class Wind extends Instrument { // Redefine interface method: public void play(Note n) { System.out.println("Wind.play()"); 161 } } public class Music { public static void tune(Instrument i) { // ... i.play(Note.middleC); } public static void main(String[] args) { Wind flute = new Wind(); tune(flute); // Upcasting } } ///:~
方法 Music.tune()接收一个Instrument句柄,同时也接收从Instrument衍生出来的所有东西。
绑定:将一个方法调用同一个方法主体关联起来被称作“绑定”。
后期绑定(动态绑定、运行时绑定):在运行时根据对象的类型进行绑定。在后期绑定中:编译器一直不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。
Java中除了static方法和final方法(private方法属于final方法)之外,所有的方法都是后期绑定。后期绑定是自动发生的。
对于上图的继承关系:
如果有这样的向上转型:Shape s = new Circle();
当我们调用s.draw()的时候,由于后期绑定,还是会正确调用Circle.draw()。
可扩展性:
多态允许我们当向基类添加更多的方法,或是加入一些新类的时候,并不需要对原本的代码进行修改。
多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。
看下面的例子,基类新添加了两个方法,但以基类对象为参数的tune()方法完全不需要修改。
//: Music3.java // An extensible program import java.util.*; class Instrument3 { public void play() { System.out.println("Instrument3.play()"); } public String what() { return "Instrument3"; } public void adjust() {} } class Wind3 extends Instrument3 { public void play() { System.out.println("Wind3.play()"); } public String what() { return "Wind3"; } public void adjust() {} } class Percussion3 extends Instrument3 { public void play() { System.out.println("Percussion3.play()"); } public String what() { return "Percussion3"; } public void adjust() {} } class Stringed3 extends Instrument3 { public void play() { System.out.println("Stringed3.play()"); } public String what() { return "Stringed3"; } public void adjust() {} } class Brass3 extends Wind3 { public void play() { System.out.println("Brass3.play()"); } public void adjust() { System.out.println("Brass3.adjust()"); } } class Woodwind3 extends Wind3 { public void play() { System.out.println("Woodwind3.play()"); } public String what() { return "Woodwind3"; } } public class Music3 { // Doesn't care about type, so new types // added to the system still work right: static void tune(Instrument3 i) { // ... i.play(); } static void tuneAll(Instrument3[] e) { for (int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument3[] orchestra = new Instrument3[5]; int i = 0; // Upcasting during addition to the array: orchestra[i++] = new Wind3(); orchestra[i++] = new Percussion3(); orchestra[i++] = new Stringed3(); orchestra[i++] = new Brass3(); orchestra[i++] = new Woodwind3(); tuneAll(orchestra); } } ///:~
输出:
Wind3.play() Percussion3.play() new Stringed3.play() Brass3.play() Woodwind3.play()
注意: 只有非private方法才可以被覆盖。
在实践中,通常将所有域都设置成private。
静态的方法不具有多态性。
构造器与多态
构造器不具有多态性(实际上是隐式static方法)。
调用构造器的顺序:父静态->子静态->父非静->父构造方法->子非静->子构造方法。
清理:如果有需要手动销毁对象,销毁顺序应该和初始化顺序相反。因为导出类的清理可能会调用基类中的某些方法,所以需要使基类中的构件仍起作用而不应过早地销毁它们。
如果存在共享对象的情况,可以使用引用计数来跟踪仍旧访问着共享对象的对象的数量,来确定何时进行清理。
构造器内部的多态行为:
看下面例子:
//: PolyConstructors.java // Constructors and polymorphism // don't produce what you might expect. abstract class Glyph { 203 abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { 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
出现这样的结果是因为初始化顺序:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构建器的主体。
因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。
协变返回类型:
Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型。
import java.io.ByteArrayInputStream; import java.io.InputStream; class Base { //子类Derive将重写此方法,将返回类型设置为InputStream的子类 public InputStream getInput() { return System.in; } } public class Derive extends Base { @Override public ByteArrayInputStream getInput() { return new ByteArrayInputStream(new byte[1024]); } public static void main(String[] args) { Derive d = new Derive(); System.out.println(d.getInput().getClass()); } }
输出:
class java.io.ByteArrayInputStream
向下转型与运行时类型识别:
但在Java中,所有转型都会自动得到检查和核实!
所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫无留情地对这个转型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类转型异常)。在运行期间对类型进行检查的行为叫作“运行时类型识别”(RTTI)。
看下面的例子:
//: RTTI.java // Downcasting & Run-Time Type // Identification (RTTI) import java.util.*; class Useful { public void f() {} public void g() {} } class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {} } public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Compile-time: method not found in Useful:208 //! x[1].u(); ((MoreUseful)x[1]).u(); // Downcast/RTTI ((MoreUseful)x[0]).u(); // Exception thrown } } ///:~