Java之对象的多态性(使用生活中通俗的例子讲解)

Java之对象的多态性

多态概念 (Java)

    多态英语:polymorphism),是指计算机程序运行时相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。简单来说,所谓多态意指相同的消息给予不同的对象会引发不同的动作称之

多态也可定义为“一种将不同的特殊行为和单个泛化记号相关联的能力”。

多态可分为变量多态与函数多态。变量多态是指:基类型的变量(对于C++是引用或指针)可以被赋值基类型对象,也可以被赋值派生类型的对象。函数多态是指,相同的函数调用界面(函数名与实参表),传送给一个对象变量,可以有不同的行为,这视该对象变量所指向的对象类型而定。因此,变量多态是函数多态的基础。


例子

    比如有动物(Animal)之类别(Class),而且由动物继承出类别鸡(Chicken)和类别狗(Dog),并对同一源自类别动物(父类别)之一消息有不同、的响应,如类别动物有“叫()”之动作,而类别鸡会“啼叫()”,类别狗则会“吠叫()”,则称之为多态。


概括

    上面关于多态的概念看起来有那么一点难以理解,但是我们可以把上述一大段话给归纳成为一句话就是:相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为

例如:比如有动物(Animal)之类别(Class),而且由动物继承出类别鸡(Chicken)和类别狗(Dog),并对同一源自类别动物(父类别)之一消息有不同的响应。


解释

        方法的覆盖特性是Java在运行时支持的多态性之一。系统动态的调度方法是由调用一个被覆盖的方法引起的,该调用机制发生在运行时,而不是编译时。

  • 当一个被覆盖的方法通过一个父类引用调用时,Java决定执行哪个版本的方法(父类的方法或者被子类覆盖的方法)取决于方法调用发生时,被引用的对象的类型。因此,这一决定实在运行期间做出来的。
  • 在运行期间,哪个版本的被覆盖的方法将会被执行是由被引用的对象的类型决定的而不是引用的类型(值)。
  • 一个父类的引用可以引用一个子类的对象,这也是一些人口中所说的“向上转型”,Java用这个方法来解决程序运行时覆盖方法被调用的问题。

Java之对象的多态性(使用生活中通俗的例子讲解)_第1张图片


    因此,如果一个父类包含了一个被子类覆盖的方法,接下来通过一个父类的引用值来引用其不同的子类对象,父类引用调用被覆盖的方法时,不同版本的被覆盖的方法将会执行。下面是一个例子,简单的阐述一下这种动态方法分配机制:

Java之对象的多态性(使用生活中通俗的例子讲解)_第2张图片

Java之对象的多态性(使用生活中通俗的例子讲解)_第3张图片

OUTPUT(输出):

Java之对象的多态性(使用生活中通俗的例子讲解)_第4张图片

程序详解

    上面的这个程序创建了一个名为A的父类以及两个分别名为B,C的子类。这两个子类都覆盖了父类A类的m1方法

1.在main()方法里面的Dispatch类中,首先声明了A类,B类,C类的三个对象。

Java之对象的多态性(使用生活中通俗的例子讲解)_第5张图片

    此时的引用及对象之间的关系如下图所示:


Java之对象的多态性(使用生活中通俗的例子讲解)_第6张图片


    其中,A类引用指向了一个A类对象,B类引用指向了一个B类对象,B类对象内包含了一个从A类中继承下来的m1()方法以及自己覆盖m1的方法;C类引用指向了一个C类对象,C类对象内也包含了一个从A类中继承下来的m1()方法以及自己覆盖m1的方法;

2.现在声明一个名为ref的A类的引用,起初它将会指向一个空指针。

Java之对象的多态性(使用生活中通俗的例子讲解)_第7张图片

3.接下来我们将用A类引用来一个一个地指向每一种对象(A类B类或者C类),并且通过这个A类引用ref来调用m1()方法。正如输出所显示的那样,到底执行哪一个版本的m1()方法是由此时引用指向的对象的类型所决定的。



Java之对象的多态性(使用生活中通俗的例子讲解)_第8张图片


Java之对象的多态性(使用生活中通俗的例子讲解)_第9张图片


Java之对象的多态性(使用生活中通俗的例子讲解)_第10张图片




Java之对象的多态性(使用生活中通俗的例子讲解)_第11张图片



亲自动手验证

     

           有兴趣的同学可以点击此处进行试验去这个在线IDE平台自己试一下这串代码,亲自动手验证一下上面这个例子。


Java之对象的多态性(使用生活中通俗的例子讲解)_第12张图片


运行时成员变量的多态性


    在Java中,我们只能够覆盖父类的方法,却不能覆盖父类的成员变量。

所以运行时,成员变量无法实现多态性。举一个例子:

Java之对象的多态性(使用生活中通俗的例子讲解)_第13张图片

    

    在上面这个例子中,子类B类虽然覆盖了父类A类的成员变量X,然而通过A类引用去调用B类对象的X时返回的并不是B类中已经覆盖了A类的那个X的值,返回的仍然是父类A类中的X值,也就是说,成员变量覆盖无法实现像方法覆盖那样的多态性。虽然A类(父类)和B类(子类)都有一个公共的变量x,我们实例化一个B类对象并用一个A类引用a指向它。因为变量没有被覆盖,所以“a.x”这一句话总是代表父类的数据成员。


OUTPUT(输出):



亲自试一试

        

    有兴趣的同学可以点击此处通过在线IDE平台亲自实验一下,更好的理解这个例子。

静态VS动态绑定


    1.静态绑定发生在程序的编译期间,而动态绑定发生在程序的运行期间。

    2.private,final,static方法以及变量通过编译器实现静态绑定,同时覆盖方法与运行时运行的对象类型所绑定。


对于多态的通俗理解

        其实,对于引用的理解,我们可以简单的理解其为一个“遥控器”,每个“遥控器”可以与一个对象进行“配对”,并通过遥控器来控制对象的“行为”。例如,按下空调遥控器的开关键,空调就会开启。就好比调用 空调遥控器.开机()方法使得空调遥控器指向的空调对象执行开机()方法一样。

 

Java之对象的多态性(使用生活中通俗的例子讲解)_第14张图片



多态的误区点


  其中,对象能够执行哪些方法是由引用的类型所决定的,而对象具体怎样去执行某个方法,却是由对象自己决定的。

    例如下面这段程序:

class A {
    public void fun1() {
        System.out.println("A-->public void fun1(){}");
    }

    public void fun2() {
        this.fun1();
    }
}

class B extends A {
    public void fun1() {
        System.out.println("B-->public void fun1(){}");
    }

    public void fun3() {
        System.out.println("B-->public void fun3(){}");
    }
}

public class JavaExample {
    public static void main(String[] args) {
        A a = new B();
        a.fun1();
    }
}

   

执行结果为:B-->public void fun1(){};

 上面的程序中,定义了父类为A类,子类为B类,子类B覆盖了父类的fun1()方法,添加了自己的fun3()类,这是一个对象向上转型的关系,但是A类型的引用a虽然可以调用fun1()方法,但是不能通过A类引用a调用子类对象独有的fun3()方法!!!

因此,我们可以得出下面这个结论:

    引用所能够调用的方法取决于引用的类型,而如何具体的实现该方法取决于对象的类型。

引用a为A类型,所以通过引用a只能调用A类型中包含的方法( fun1()和fun2()),而能调用B类中的fun3(。也就是说A类引用只能向指向的对象发送A类中包含的方法,具体的实现由指向的对象的类型来决定。

   

                    点击此处进入在线IDE实验本案例。

我们可以在上面的实验中,执行a.fun3(),将会提示找不到fun3()方法。

向下转型的要求


    还是上面的这一串代码,我们只更改其main()函数中的代码:

public class JavaExample {
    public static void main(String[] args) {
       A a=new B();
       B b=(B)a;
       b.fun1();
       b.fun2();
       b.fun3();
    }
}

    B类为A类的子类,上面的a首先被声明为一个A类型的引用,并指向一个B类型的对象,接下来把A类引用通过向下转型变成一个B类引用,从而有权利去调用B类中非共有的方法。

    然而,在看下面这一段代码,还是只更改main()函数中的代码:

public class JavaExample {
    public static void main(String[] args) {
       A a=new A();
       B b=(B)a;
       b.fun1();
       b.fun2();
       b.fun3();
    }

    上面的这一段代码也实现了向下转型,然而运行时却报错了:

Exception in thread "main" java.lang.ClassCastException: A cannot be cast to B

at JavaExample.main(JavaExample.java:26)

错误提示说A类引用无法被转换成B类引用,这又是怎么回事呢?

我们发现两端代码的不同之处就在于第一段代码中是

 A a=new B();

而第二段代码中是:

  A a=new A();

    初始化的对象不同而已,因为在对象进行向下转型时,必须首先发生对象向上转型,否则将出现对象转换异常。

第二段代码中,A类型引用指向的是一个A类对象,把A类引用转换成B类引用,此时就可以发送调用B类方法的命令给指向的对象,而指向的对象为A类对象却并不含B类中非共有的方法,对象根本就没有实现这个方法,怎么去执行方法呢?因此就会产生错误。

    可以用上述遥控器的例子来简单的理解一下这个问题:

      假设最开始只有一个老式遥控器,遥控器只有1.打开空调,2.关闭空调,3.制冷三个功能,遥控器向空调发送相应的命令,空调收到命令后执行相应的动作。

    此时类似于执行语句 A a=new A( );

Java之对象的多态性(使用生活中通俗的例子讲解)_第15张图片

  

    接下来,老式空调太旧了,空调厂商在老式空调的基础上,开发了一款新式空调,不仅有老式空调的三个功能,而且还加入了1.制热功能和2.加湿功能这两个功能。

    此时类似于语句 class B extend A;

Java之对象的多态性(使用生活中通俗的例子讲解)_第16张图片

    然而,我们此时只有老式空调的遥控器,但是我们还是可以用老式空调的遥控器与操作新式空调,类似于A a=new B( )这个操作,因为新式空调是继承与老式空调改造而来的,我们仍然能够使用老式空调的遥控去操控新式空调,只不过老式空调上只有最基础的三个功能,不能操纵新式空调执行其新添加的加热和加湿功能,但是仍然还是可以使用的。

Java之对象的多态性(使用生活中通俗的例子讲解)_第17张图片

        如果,我们需要使用新式空调的新增的加热和加湿功能,那么我们理应更换新式空调对应的遥控器就好了。

此时类似于 B b=(B) a;

    Java之对象的多态性(使用生活中通俗的例子讲解)_第18张图片

   此时的情况就类似于调用b.makeCold( )方法了。

    上面是正常的向下转型,然而,如果我们不向上转型而直接向下转型会发生什么样的情况呢?

    例如:A a=new A( );

                B b=(B)a;

            就发生了下面这种情况:

Java之对象的多态性(使用生活中通俗的例子讲解)_第19张图片


    此时,如果新式空调遥控器向老式空调发送一个加热的指令,然而老式空调收到指令后却发现并没有加热的功能,此时就会发生系统故障。

        所以,总结而之一句话:在对象进行向下转型时,必须首先发生对象向上转型,否则将出现对象转换异常。


参考资料

    1.维基百科-多态

    2.https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java

        作者:Gaurav Miglani   翻译:刘扬俊

    3.《Java开发实战经典》 李兴华著  清华大学出版社


博客文章版权说明


第一条 本博客文章仅代表作者本人的观点,不保证文章等内容的有效性。

第二条 本博客部分内容转载于合作站点或摘录于部分书籍,但都会注明作/译者和原出处。如有不妥之处,敬请指出。

第三条 征得本博客作者同意的情况下,本博客的作品允许非盈利性引用,并请注明出处:“作者:____转载自____”字样,以尊重作者的劳动成果。版权归原作/译者所有。未经允许,严禁转载

第四条 对非法转载者,“扬俊的小屋”和作/译者保留采用法律手段追究的权利

第五条 本博客之声明以及其修改权、更新权及最终解释权均属“扬俊的小屋”。

第六条 以上声明的解释权归扬俊的小屋所有。



你可能感兴趣的:(JAVA)