其他分享
首页 > 其他分享> > 多态

多态

作者:互联网

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

多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能创建可扩展的程序。
"封装"通过合并特征和行为来创建新的数据类型,"实现隐藏"则是通过将细节"私有化",把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。

再论向上转型

对象既可以作为它自己本身的类型使用,也可以作为它的基类类型使用。这种把某个对象的引用视为其基类对象的引用的做法称为向上转型。

enum Note{
    MIDDLE_C, C_SHARP, S_FLAT;
}

class Instrument{
    public void play(Note n){
        System.out.println("Instrument.play()");
    }
}

class Wind extends Instrument{
    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);
    }
}

tune()接收一个Instrument类型的参数,而很神奇的是它竟然也能接收Wind类型的参数。这样做是被允许的,因为Wind继承于Instrument。我们称这种做法为向上转型。

忘记对象类型

为什么所有人都故意忘记对象类型呢?
在进行向上转型时就会出现这种情况。但让tune()接收一个Wind类型的参数好像更为直观。但这样做引发一个问题:
如果让tune()接收一个Wind类型的参数,则需要为系统内Instrument的每种类型都编写一个tune().
假设你想再加入Stringed和Brass两种Instrument。

enum Note{
    MIDDLE_C, C_SHARP, S_FLAT;
}

class Instrument{
    public void play(Note n){
        System.out.println("Instrument.play()");
    }
}

class Wind extends Instrument{
    public void play(Note n){
        System.out.println("Wind.play() " + n);
    }
}

class Stringed extends Instrument{
    public void play(Note n){
        System.out.println("Stringed.play() " + n);
    }
}

class Brass extends Instrument{
    public void play(Note n){
        System.out.println("Brass.play() " + n);
    }
}

public class Music{
    public static void tune(Wind i){
        i.play(Note.MIDDLE_C);
    }
    public static void tune(Stringed i){
        i.play(Note.MIDDLE_C);
    }
    public static void tune(Brass i){
        i.play(Note.MIDDLE_C);
    }

    public static void main(String[] args) {
        Wind flute = new Wind();
        Stringed stringed = new Stringed();
        Brass brass = new Brass();
        tune(flute);
        tune(stringed);
        tune(brass);
    }
}

这样做行得通,但有一个主要缺点,每添加一个新的Instrument类就要为它添加新的tune()。此外,如果我们忘记重载某个方法(你添加了一个新的Instrument类,但你没有为它编写tune()方法),编译器不会返回任何错误信息。

如果我们只写这样一个简单方法,它仅接收基类类型作为参数,而不是那些导出类。这样做是不是会更好?反正导出类可以向上转型成基类。

enum Note{
    MIDDLE_C, C_SHARP, S_FLAT;
}

class Instrument{
    public void play(Note n){
        System.out.println("Instrument.play()");
    }
}

class Wind extends Instrument{
    public void play(Note n){
        System.out.println("Wind.play() " + n);
    }
}

class Stringed extends Instrument{
    public void play(Note n){
        System.out.println("Stringed.play() " + n);
    }
}

class Brass extends Instrument{
    public void play(Note n){
        System.out.println("Brass.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();
        Stringed stringed = new Stringed();
        Brass brass = new Brass();
        tune(flute);
        tune(stringed);
        tune(brass);
    }
}

这样你就可以只添加新类而不必为每个新类都定义tune()方法了。

转机

但问题又来了,

  public static void tune(Instrument i){
        i.play(Note.MIDDLE_C);
    }

tune()接收一个Instrumen类型的引用,那么编译器是怎么知道你传入的是Wind类型的引用还是Stringed类型的引用亦或是其他类型的引用?事实上编译器不知道,那么它是如何输出正确结果呢?为了深入理解这个问题,有必要研究以下绑定这个话题。

方法调用绑定

将一个方法调用同一个方法主题关联起来被称作绑定。

若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现)叫做前期绑定。这是面向过程语言中不需要选择就默认的绑定方式。
而解决上述问题的办法就是后期绑定:在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定和运行时绑定。

如果一种语言想实现后期绑定,就必须具有某种机制(Java使用RTTI机制来实现运行时类型检查),以便在运行时判断对象类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。

Java中除了static和final方法(private 属于final方法),其他方法都是动态绑定。

产生正确的行为

一旦知道Java所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有导出类都可以正确运行。

面向对象程序设计中有一个经典的例子:"几何形状"。在这个例子中有一个基类Shape,以及多个导出类:Circle,Square等等。
我们可以定义这样的语句:

Shape shape = new Circle();

创建一个Circle对象,并把得到的引用赋值给Shape,向上转型允许我们这么做。假如你调用基类的一个方法(在导出类中已经被重写)

shpae.draw();

由于动态绑定,它不会调用Shape的draw()而是正确的调用了Circle的draw()。

缺陷:"重写"私有方法

我们试图像下面这样做也是无可厚非的:

public class A{
   private void f(){
       System.out.println(" private void f()");
   }
    public static void main(String[] args) {
       A a = new B();
       a.f();
    }
}
class B extends A{
    public void f(){
        System.out.println("public void f()");
    }
}

我们期望它能输出public void f(),但却输出的是private void f()。由于private方法被认为是final方法,而且对导出类是屏蔽的。因此B中的f()是一个全新的方法。A类中的私有方法在B类中不可见所以也不能被重写。

缺陷:域与静态方法

任何域访问操作都将由编译器解析,因此不是多态的。
静态方法是与类,而非与单个对象相关联的,因此也不具有多态性。

协变返回类型

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

class Grain{
    @Override
    public String toString() {
        return "Grain{}";
    }
}

class Wheat extends Grain{
    @Override
    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);
    }
}

class Mill{
    Grain process(){
        return new Grain();
    }
}

class WheatMill{
    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);
        g = new WheatMill().process();
        System.out.println(g);
    }
}

Java较早的版本将强制process()覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的。协变返回类型允许返回更具体的Wheat值。

标签:play,void,多态,public,Instrument,class,tune
来源: https://www.cnblogs.com/xxgbl/p/13669859.html