在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三张基本特征。
多态:分离,做什么和怎么做,消除类型间的耦合性
封装:合并,创造新的数据类型
实现隐藏:分离,接口和实现,将细节私有化
继承允许将对象视为它自己本身的类型或其基类来加以处理,这种能力极为重要,因为它允许将多种类型视为同一类型处理,而同一份代码也就可以毫无差别地运行在不同类型之上。多态方法调用允许一种类型表现出与其他相似类型之间的区别,这种区别根据方法行为的不同而表现出来,虽然这些方法都可以通过同一个基类调用。
public enum Note {
MIDDLE_C,C_SHARP,B_FLAT
}
public class Instrument {
public void play(Note n){
System.out.println("Instrument.play()");
}
}
public class Wind extends Instrument{
@Override
public void play(Note n){
System.out.println("Wind.play() " + n);
}
}
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);
}
}
输出:
Wind.play() MIDDLE_C
tune方法接受了Instrument引用,同时也接受了导出自Instrument的类。Wind调用了tune方法,由于继承,Insturment的接口必定存在于wind中,从wind向上转型可能会缩小接口,但不会比Instrument的全部接口更窄。
如果让tune直接接收wind,似乎更直观,如果这样做,就需要为系统内Instrument的每种类型都编写tune方法。但是很麻烦。
我们只写一个简单的方法,仅接受基类做为参数,这种情况下,编译器怎么知道这个Instrument引用指向的是Wind对象,而不是其他呢?实际上,编译器无法得知。
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行前进行绑定,叫做前期绑定。在运行时根据对象的类型进行绑定,后期绑定(动态绑定,运行时绑定)。机制:编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。
Java中除了static和final方法,其他方法都是后期绑定,我们不必判定是否应该进行后期绑定,它会自动发生。
可扩展性:Instrument,大多数或者所有方法都会遵循tune的模型,而且只与基类接口通信,这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能,那些操作基类接口的方法不需要任何的改动就可以应用于新类。
public class PrivateOverride {
private void f(){
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride p = new Derived();
p.f();
}
}
public class Derived extends PrivateOverride {
public void f(){
System.out.println("public f()");
}
}
输出:
private f()
由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此derived中的f方法就是一个全新的方法。
只有非private方法才会被覆盖,在导出类中,对于基类中的private方法,最好采用不同的名字。
只有普通方法调用可以是多态的。
public class Super {
public int field = 0;
public int getField(){return field;}
}
public class Sub extends Super{
public int field = 1;
@Override
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 = " + sup.getField() +
", sub.getSuperField() = " + sub.getSuperField());
}
}
任何域访问操作都是由编译器解析,因此不是多态的。
上例中为Super.field和Sub.field分配了不同的存储空间,Sub实际包含了两个field域,一个是自己的一个是Supper处得到的,然而在引用Sub的field时所产生的默认域并非Super版本的field,因此,为了得到Super.field,必须显示指明super.field。
同样的,静态方法的行为也不具多态性,静态方法是与类,而非单个的对象相关联。
只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化(导出类不鞥访问基类成员(基类成员通常为private)),因此必须令所有构造器得到调用,否则就不可能正确构造完整的对象,这正是编译器为什么要强制每个导出类都必须调用构造器的原因。
public class Meal {
Meal(){
System.out.println("Meal()");
}
}
public class Bread {
Bread(){
System.out.println("Bread()");
}
}
public class Cheese {
Cheese(){
System.out.println("Cheese()");
}
}
public class Lettuce {
Lettuce(){
System.out.println("Lettuce()");
}
}
public class Lunch extends Meal{
Lunch(){
System.out.println("Lunch()");
}
}
public 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()
顺序:
1、调用基类构造器
2、按声明顺序调用成员的初始化方法
3、调用导出类构造器主体
通过组合和继承来创建新类时,不用担心对象的清理工作,子对象通常都会留给垃圾回收器处理。如果新类遇到特殊清理问题,并为新类创建了dispose()方法,就必须在导出类中覆盖dispose方法。当覆盖被继承类的dispose方法时,务必记住调用基类版本的dispose方法,否则,基类的清理动作就不会发生。
public class Glyph {
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
public class RoundGlyph extends Glyph{
private int radius =1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.RoundGlyph(). radius = " + radius);
}
@Override
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
Glyph.draw()方法被覆盖,覆盖在RoundGlyph中发生。但Glyph构造器调用了这个方法,导致了对RoundGlyph.draw()的调用。但是radius不是默认初始值1,而是0。前面讲的初始化过程并不完整,初始化的实际过程:
1、在任何事物发生之前,将分配给对象存储空间初始化二进制的0。
2、如前所述那样调用基类构造器。此时,调用覆盖后的draw,由于步骤1的缘故,此时radius值为0
3、按照声明顺序调用成员初始化方法
4、调用导出类的构造器主体
协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型。
public class Grain {
@Override
public String toString() {
return "Grain";
}
}
public class Wheat extends Grain{
@Override
public String toString() {
return "Wheat";
}
}
public class Mill {
Grain process(){
return new Grain();
}
}
public class WheatMill extends Mill{
@Override
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
准则:用继承表示行为间的差异,用组合表示状态(对象)上的变化
只有在基类中已经建立的方法才可以在导出类中被覆盖,纯粹的is-a关系。也就是说基类可以接收发送给导出类的任何消息,因为二者有着相同的接口。
is-like-a(扩展接口),导出类就像一个基类,它有着相同的基本接口,但是它还具有额外方法实现的其他特性。
在某些程序设计语言中(如C++),我们必须执行一个特殊的操作来获得安全的向下转型。但是在Java中,所有转型都会得到检查!所以即使我们只是进行一次普通的加括弧形式的类型转换,在进入运行期仍然会对其进行检查,一边保证它的确是我们希望的类型。如果不是,就会返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别(RTTI)”。
public class Useful {
public void f(){}
public void g(){}
}
public class MoreUseful extends Useful{
@Override
public void f(){}
@Override
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();
//! x[1].u();
((MoreUseful)x[1]).u();
((MoreUseful)x[0]).u();
}
}
如果想访问MoreUseful对象的扩展接口,就可以尝试进行向下转型。如果所撰类型是正确的类型,那么转型成功((MoreUseful)x[1]).u();否则,就会返回一个ClassCastException异常((MoreUseful)x[0]).u()。
RTTI的内容不仅仅包括转型处理。例如它还提供了一种方法,使你可以在试图向下转型之前查看你所要处理的类型。