【Java】 多态

一、在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。

二、转机

1: 方法调用绑定

1.1  前期绑定:将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前期绑定(如果有的话,由编译器和连接程序实现)。

1.2  后期绑定:运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同。Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。通常情况下,它会自动发生。

Java中所有方法都是通过动态绑定实现多态。

多态是一项让程序员“将改变的事物与未变的事物分离开来” 的重要技术。

同一个方法被不同的对象调用,并且能自动后期绑定匹配就叫多态。下面这属于两个不同的方法,所以就会调用所属类型的方法。

1.3   缺陷1:“覆盖“ 私有方法

//: polymorphism/PrivateOverride.java
// Trying to override a private method.
package polymorphism;

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()");
    }
}
/* Output
private f()
 */

我们希望输出的是public f ( ),但是由于private 方法被自动认为是final方法,而且对导出类是屏蔽的。因此,Derived类中的f()方法就是一个全新的方法;既然基类中f( )方法在子类Derived中不可见,因此甚至也不能被重载。

结论:只有非private方法才可以被覆盖;但是需要密切注意覆盖private方法的现象,虽然编译器不会报错,但是也不会按照我们所期望的来执行。所以,在导出类中,对于基类中的private方法,最好采用不同的名字。 

1.4   缺陷2:域

//: polymorphism/FieldAccess.java
// Direct field access is determind at compile time.

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();  // Upcast  域访问不具有多态,但是方法具有
        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());

    }
}
/* Output
sup.field = 0, sup.getField = 1
sub.field = 1, sub.getField = 1, sub.getSuperField = 0
 */
        

对于第一行的输出:会产生向上转型,域访问操作由编译器解析,不是多态的,所以会输出基类的域值。但是方法是多态的,所以会调用导出类的方法,也就输出了导出类的域值。

当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。在上述代码中,为Super.field 和 Sub.field分配了不同的存储空间。这样,Sub实际上包含了两个称为field的域:它自己的和它从Super处得到的。然而在引用Sub中的field时所产生的默认域并非Super版本中的field域。因此,为了得到Super.field,必须显示地指明super.field。

尽管这看起来好像会成为一个容易令人混淆的问题,但是在实践中,它实际上从来不会发生。首先,你通常会将所有的域都设置成private,因此不能直接访问它们,其副作用时只能调用方法来访问。另外,你可能不会对基类中的域和导出类中的域赋予相同的名字,因为这种做法容易令人混淆。

1.5   缺陷3:静态方法

//: polymorphism/StaticPolymorphism.java
// Static methods are not polymorphic

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();  // Upcast
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}
/*Output
Base staticGet()
Derived dynamicGet()
 */

静态方法是与类,而并非单个的对象相关联的。

如果某个方法是静态的,它的行为就不具有多态性。

所以对于第一行的输出,首先static 方法不具有多态性,所以当创建一个导出类对象赋值给基类引用后,会产生向上转型,由于static方法不具有多态性,所以导出类的static方法和基类的static方法不是同一个方法。所以最后会调用基类的静态方法。

三、构造器和多态

3.1   创建对象调用构造器的顺序:

1:调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次的根,然后是下一层导出类,等等,直到最低层的导出类。

2:按声明顺序调用成员的初始化方法。

3:调用导出类构造器的主体。

3.2   构造器内部的多态方法的行为

//: polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect

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);
    }
}
/* Output
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
 */

Glyph.draw( ) 方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw( )的调用。但是Glyph的构造器调用draw()方法时,radius 不是默认值1,而是0。

3.3  初始化的实际过程

1):在任何事物发生之前,将分配给对象的存储空间初始化为二进制的零。

2):如前所述调用基类构造器。此时调用被覆盖的draw()方法(要在调用RoundGlyph构造器之前调用),又由于步骤1的缘故,所以radius的值为0。

3):按照声明的顺序调用成员的初始化方法。

4):调用导出类构造器主体。

编写构造器时有一条有效的准则:”用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法, 它们自动属于final方法)。这些方法不能被覆盖,因此也就不会出现上述问题。

四  协变返回类型

Java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类基类方法的返回类型的某种导出类型。

五、用继承进行设计

//: polymorphism/Transmogrify.java
//Dynamically changing the behavior of an object
//via composition ( the "State" design pattern

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();
    }
}
/* Output
HappyActor
SadActor
 */

组合使得我们在运行期间获得了动态灵活性。与此相反,我们不能在运行期间决定继承不同的对象,因为它要求在编译期间完全确定下来。

一条通用的准则是:”用继承表达行为间的差异,并用字段表达状态上的变化“。在上述代码中:通过继承得到了两个不同的类,用于act()方法的差异;而Stage通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。

5.1  纯继承与扩展

采取”纯粹“的方式来创建继承层次结构似乎是最好的方式。也就是说只有在基类中已经建立的方法才可以在导出类中被覆盖。

基类方法与导出类方法相同,这被称作是纯粹的"is a"(是一种)关系,因为一个类的接口已经确定了它应该是什么。继承可以确保所有的导出类具有基类相同的接口,且绝对不会少。也可以认为这是一种纯代替,因为导出类可以完全代替基类,而在使用它们时,完全不需要知道关于子类的任何信息:也就是说,基类可以接收发送给导出类的任何信息,因为二者有着完全相同的接口。我们只需从导出类向上转型,永远不需要知道正在处理的对象的确切类型。所有这一切都是通过多态来处理的。似乎只有纯粹的is a 关系才是唯一明智的做法,而所有其他的设计都只会导致混乱和注定会失败。扩展接口(遗憾的是,extends关键字似乎在怂恿我们这样做)才是解决特定问题的完美方案。这可以称为”is like a “(像一个)关系,因为导出类就像是一个基类——它有着相同的基本接口,但是它还具有由额外方法实现的其他特性。虽然这也是一种有用且明智的方法(依赖于具体情况),但是它也有缺点。导出类中接口的扩展部分不能被基类访问,因此,一旦向上转型,就不能调用那些方法了。

5.2  向下转型与运行时类型识别

由于向上转型会丢失具体的类型信息,所以我们就想,通过向下转型应该能够获取类型信息。我们知道向上转型是安全的,但是对于向下转型,我们无法知道一个几何形状确实是圆、三角形、或其他一些类型。

我们必须执行一个特殊的操作来获得安全的向下转型。在Java语言中,所有的转型都会得到检查!所有即使我们只是进行一次普通的加括弧形式的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就会返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别“(RTTI)。

//: polymorphism/RTTI.java
//Downing & Runtime type information (RTTI)
//{ThrowsException}

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:
         //! x[1].u();
        ((MoreUseful)x[1]).u();  // Downcast/RTTI  向下转型/运行时类别识别
        //x[1] 是 MoreUseful 类型, 但 x[0] 不是
     //   ((MoreUseful)x[0]).u();  // Exception thrown  抛出异常
    }
}

多态意味着”不同的形式“。在面向对象的程序设计中,我们持有从基类继承而来的相同的接口,以及在使用该接口的不同形式:不同版本的绑定方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值