前言

在面向对象程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。多态的含义是什么,有什么作用以及在Java中是怎么实现的?下面将做介绍。

什么是多态

简单点说就是“一个接口,多种实现”,不同类对同一操作体现出不同效果。设想有一个性质,一个引用变量所指向的确切类型和该引用变量调用的方法是哪个类中的,这个两个问题在编译期间是不确定的,在程序运行期间才可确定。于是,一份代码就可以适用于多个不同的类,只要这份代码中有一个引用变量可以指向这些不同的类的对象。在程序运行期间,就可以动态选择多个不同的对象或者多个不同的方法运行,这就是多态性

也可以简单的使用Java核心卷中的一句话说:一个引用变量可以指示多种实际类型的现象被称为多态。

要什么样的引用变量才可以指向多种不同类的对象呢?那就需要是基类引用变量。这就涉及到向上转型

向上转型

简单一句话就是:基类(父类)引用指向子类对象。取这个术语属于也是有历史原因的,以传统的类继承图的绘制方法为基础:将根置于页面的顶端,然后逐渐向下。

为什么基类引用就可以指向子类对象呢?

这可以以生活中的一个例子来说,狗是对所有品种狗的统称,具体的品种又有哈士奇,拉布拉多犬等。假设我们在路上遇见了一只不知道名字的狗(比如牧羊犬),我们也许会说:那儿有一只狗。此时,我们就做了向上转型,以狗指向了具体的牧羊犬。以范围较大的基类引用去指向范围小的子类。这是以生活中的例子解释,在语言层面其实也可以说明:子类是继承基类而来,所以基类中的所有方法子类也有,可以发送给基类的消息同样也可以发送给子类。使用基类引用也就可以指向子类对象调用这些方法。

可以使用基类引用指向子类对象,那么可不可以使用子类引用指向基类对象呢?

是可以的,这就叫做向下转型,但是这存在风险。因为子类中可能会有新增方法,而基类中是没有这些方法的,若是调用这些方法就会抛出ClassCastException异常。

介绍了什么是多态,那么就了解它的作用有什么。

多态的作用

多态的一个好处就是可以实现统一管理,需要注意父类不能调用子类特有的方法即父类中没有的方法子类有的方法,若要调用需要向下转型。

多态如何实现

Java中实现多态的三个要求为:

  • 要有继承关系

  • 方法要被重写

  • 基类引用指向子类对象

可以看一个简单的多态例子

class Animal{
    public void eat() { System.out.println("吃东西"); }
}

class Dog extends Animal{
    public void eat() { System.out.println("吃狗粮");}
}

class Cat extends Animal{
    public void eat() { System.out.println("吃小鱼干");}
}

public class PloyTest {
    //使用父类引用指向子类对象 调用子类中被重写的方法
    public static void printEatingFood(Animal a) { a.eat();}

    public static void main(String[] args) {
        printEatingFood(new Dog());
        printEatingFood(new Cat());
    }
}
/*
output:
吃狗粮
吃小鱼干
*/

当我们传入Dog对象时,a.eat()调用的是Dog类中被重写的eat方法;传入Cat对象时,a.eat()调用的是Cat类中被重写的方法。程序在运行过程中,依据我们传入的对象自动地为我们寻找到正确的方法调用。这就是多态技术的实现依据:动态绑定。

动态绑定

《Java编程思想》上这样说:运行时父类引用根据其指向的对象,绑定到相应的对象方法上。

那么这个过程的具体实现是怎样的呢?《Java核心技术卷1》上是这样解释的解释:

先是要清楚调用对象方法的过程:

  1. 搜索过程)编译器查看对象的声明类型和方法名。可能会有同名的重载方法。

若是调用x.f(param)

获得当前类和超类中为public的名为f的方法,即获可能被调用的候选方法

  1. 匹配过程)然后,编译器查看调用方法时提供的参数类型,与候选方法进行匹配,此过程也就是重载解析。

完全匹配,则选择调用该方法,这其中还会存在类型转换,所以过程会比较复杂。最后若是没有找到匹配的,编译器则会报错。

​ -------------------------------------------------------------------------------------------

静态绑定:
若方法是private, static, final或者构造器,那么编译器会知道调用哪个方法,这种方式为静态绑定。

动态绑定:
依赖于隐式参数的实际类型,在运行时才可以确定调用方法,则为动态绑定。
当程序运行,并且采用动态绑定调用方法时,虚拟机会调用与x所引用对象的实际类型最合适的那个类的方法。并且为了避免每次搜索浪费时间,虚拟机会为每个类创建一个方法表。其中包含所有方法的签名和实际调用的方法(包括继承来方法)。

一些陷阱和建议

域和静态方法

我们需要注意只有普通方法调用才可以是多态的,对域的访问将在编译时期进行解析。如下面这个例子:

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.getField();}
}

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.getField="+sub.field+" sub.getField()="+sub.getField()+
                " sub.getSuperField()="+sub.getSuperField());
    }
}
/*
output:
sup.field=0 sup.getField()=1
sub.getField=1 sub.getField()=1 sub.getSuperField()=0
*/

当使用父类Super的引用sup指向子类Sub类对象,输出域,发现是父类的值。因此,域的访问是编译器解析,不是多态的。

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

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 OverloadingTest {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub();  //向上转型
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}
/*
output:
Base staticGet()
Derived dynamicGet()
*/

由输出可以看出,静态方法是不具有多态性的。静态方法是与类,而非与单个的对象相关联的。

小结

简要介绍了对于多态的理解,其中存在的不足,希望各位看官不吝赐教。

参考:

《Java编程思想》第四版

《Java核心技术卷1》第九版

Java多态性理解,好处及精典实例:https://blog.csdn.net/Jian_Yun_Rui/article/details/52937791

02-20 09:32