世界上最美丽的东西,看不见也摸不着,要靠心灵去感受。 ——海伦·凯勒

面向对象编程有三大特性:封装、继承、多态。

封装隐藏了类的内部实现机制,可以在不影响类使用的情况下改变类的内部结构,并保护数据。对于外部世界,其内部细节是隐藏的,并且只有其访问方法向外部世界公开。

继承就是重用父代码。如果两个类之间存在IS-A关系,则可以使用继承。同时,继承为多态性铺平了道路。那么什么是多态性?多态性的实现机制是什么?

1  定义

所谓多态性是指在程序中定义的特定类型的引用变量和通过引用变量发出的方法调用,这在编程中不确定,但在程序运行期间确定,即,引用变量将指向哪个类的实例对象和方法调用m。通过引用变量实现ade的类必须由程序运行。只有在这个时期我们才能作出决定。

因为只有当程序运行时才确定特定的类,所以可以在不修改源代码的情况下将引用变量绑定到不同的类实现,这导致引用调用的特定方法的改变。也就是说,在不修改程序代码的情况下,可以改变程序运行时绑定的特定代码,从而使程序可以选择多种运行状态,即多态性。。

2  多态的实现

2.1 实现的条件

Java实现多态有三个必要条件:继承、重写、向上转型(父类引用指向子类对象)

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

对于Java,多态的实现机制遵循一个原则:当超类对象引用子类对象时,引用对象的类型而不是引用变量的类型决定要调用的成员方法,但是要调用的方法必须在超类中定义,也就是说,由t重写的方法。他属于亚类。

【注】向上转换存在一些缺点,即不可避免地会导致一些方法和属性的丢失,并导致无法访问它们。因此,对父类型的引用可以调用父类中定义的所有属性和方法,对于仅存在于子类中的方法和属性来说已经太晚了。

2.2 实现的方式

2.2.1、基于继承实现的多态

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

 public class Wine {
         private String name;

         public String getName() {
             return name;
         }

         public void setName(String name) {
             this.name = name;
         }

         public Wine(){
         }

         public String drink(){
             return "喝的是 " + getName();
         }

         /**
          * 重写toString()
          */
         public String toString(){
             return null;
         }
     }

     public class JNC extends Wine{
         public JNC(){
             setName("JNC");
         }

         /**
          * 重写父类方法,实现多态
          */
         public String drink(){
             return "喝的是 " + getName();
         }

         /**
          * 重写toString()
          */
         public String toString(){
             return "Wine : " + getName();
         }
     }

     public class JGJ extends Wine{
         public JGJ(){
             setName("JGJ");
         }

         /**
          * 重写父类方法,实现多态
          */
         public String drink(){
             return "喝的是 " + getName();
         }

         /**
          * 重写toString()
          */
         public String toString(){
             return "Wine : " + getName();
         }
     }

     public class Test {
         public static void main(String[] args) {
             //定义父类数组
             Wine[] wines = new Wine[2];
             //定义两个子类
             JNC jnc = new JNC();
             JGJ jgj = new JGJ();

             //父类引用子类对象
             wines[0] = jnc;
             wines[1] = jgj;

             for(int i = 0 ; i < 2 ; i++){
                 System.out.println(wines[i].toString() + "--" + wines[i].drink());
             }
             System.out.println("-------------------------------");

         }
     }

OUTPUT:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ


在上面的代码中 JNC、JGJ 继承 Wine,并且重写了 drink()、toString() 方法,程序运行结果是调用子类中方法,输出 JNC、JGJ 的名称,这就是多态的表现。不同的对象可以执行相同的行为,但是他们都需要通过自己的实现方式来执行,这就要得益于向上转型了。

我们都知道所有的类都继承自超类 Object,toString() 方法也是Object 中方法,当我们如下这样写时,输出的结果是 Wine : JGJ。

Object、Wine、JGJ 三者继承链关系是:JGJ—>Wine—>Object。所以我们可以这样说:当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用。但是注意如果这样写:

输出的结果应该是 Null,因为 JGJ 并不存在于该对象继承链中。

所以基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。

如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。

2.2.2、基于接口实现的多态

继承是通过重写父类的同一方法的几个不同子类来体现的,那么也就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。

在接口的多态中,指向接口的引用必须是指定实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。

3  经典实战

 public class A {
     public String show(D obj) {
         return ("A and D");
     }
     public String show(A obj) {
         return ("A and A");
     }
 }

 public class B extends A {
     public String show(B obj){
         return ("B and B");
     }
     public String show(A obj){
         return ("B and A");
     }
 }

 public class C extends B{}

 public class D extends B{}

 public class Test {
     public static void main(String[] args) {
         A a1 = new A();
         A a2 = new B();
         B b = new B();
         C c = new C();
         D d = new D();
         System.out.println(a1.show(b));   ①
         System.out.println(a1.show(c));   ②
         System.out.println(a1.show(d));   ③
         System.out.println(a2.show(b));   ④
         System.out.println(a2.show(c));   ⑤
         System.out.println(a2.show(d));   ⑥
         System.out.println(b.show(b));    ⑦
         System.out.println(b.show(c));    ⑧
         System.out.println(b.show(d));    ⑨
    }
 }

输出结果

分析

①②③ 比较好理解,一般不会出错。④⑤ 就有点糊涂了,为什么输出的不是「B and B」呢?!!先来回顾一下多态性。

运行时多态性是面向对象程序设计代码重用的一个最强大机制,动态性的概念也可以被说成「一个接口,多个方法」。Java 实现运行时多态性的基础是动态方法调度,它是一种在运行时而不是在编译期调用重载方法的机制。

方法的重写 Overriding 和重载 Overloading 是 Java 多态性的不同表现。重写 Overriding 是父类与子类之间多态性的一种表现,重载 Overloading 是一个类中多态性的一种表现。

如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。

如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载 (Overloading)。Overloaded 的方法是可以改变返回值的类型。

当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。(但是如果强制把超类转换成子类的话,就可以调用子类中新添加而超类没有的方法了。)

实际上这里涉及方法调用的优先问题 ,优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。让我们来看看它是怎么工作的。

比如 ④,a2.show(b),a2 是一个引用变量,类型为 A,则 this为 a2,b 是 B 的一个实例,于是它到类 A 里面找 show(B obj)方法,没有找到,于是到 A 的 super(超类) 找,而 A 没有超类,因此转到第三优先级 this.show((super)O),this 仍然是 a2,这里O为B,(super)O 即 (super)B 即 A,因此它到类 A 里面找show(A obj) 的方法,类 A 有这个方法,但是由于 a2 引用的是类B的一个对象,B 覆盖了 A 的 show(A obj) 方法,因此最终锁定到类 B的show(A obj),输出为 「B and A」。

再比如 ⑧,b.show(c),b 是一个引用变量,类型为 B,则 this为 b,c 是 C 的一个实例,于是它到类 B 找 show(C obj) 方法,没有找到,转而到 B 的超类 A 里面找,A 里面也没有,因此也转到第三优先级 this.show((super)O),this 为 b,O 为 C,(super)O 即 (super)C 即 B,因此它到B里面找 show(B obj) 方法,找到了,由于 b 引用的是类B的一个对象,因此直接锁定到类B 的 show(B obj),输出为 「B and B」。

按照上面的方法,可以正确得到其他的结果。

问题还要继续,现在我们再来看上面的分析过程是怎么体现出蓝色字体那句话的内涵的。它说:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。还是拿 a2.show(b) 来说吧。

a2 是一个引用变量,类型为 A,它引用的是 B 的一个对象,因此这句话的意思是由 B 来决定调用的是哪个方法。因此应该调用B的show(B obj) 从而输出「B and B」才对。但是为什么跟前面的分析得到的结果不相符呢?!问题在于我们不要忽略了蓝色字体的后半部分,那里特别指明:这个被调用的方法必须是在超类中定义过的,也就是被子类覆盖的方法。

B 里面的 show(B obj) 在超类 A中有定义吗?没有!那就更谈不上被覆盖了。实际上这句话隐藏了一条信息:它仍然是按照方法调用的优先级来确定的。它在类A中找到了 show(A obj),如果子类B没有覆盖 show(A obj) 方法,那么它就调用 A 的 show(A obj) (由于B继承A,虽然没有覆盖这个方法,但从超类A那里继承了这个方法,从某种意义上说,还是由B确定调用的方法,只是方法是在A中实现而已);现在子类 B 覆盖了 show(A obj),因此它最终锁定到 B 的 show(A obj)。这就是那句话的意义所在。

4  总结

由于向上转换,对子类的父引用只能访问父类拥有的方法和属性。对于存在于子类中但不存在于父类中的方法,尽管方法过载,但不能使用引用。如果子类覆盖父类中的一些方法,那么在调用这些方法时必须使用子类中定义的这些方法(动态连接、动态调用)。

对于面向对象,多态性可以分为编译时多态性和运行时多态性。编辑的多态性是静态的,主要是指方法的过载。它根据不同的参数列表区分不同的函数。编辑之后,它将变成两个不同的函数,在运行时不是多态的。运行时多态性是动态的,并且它是通过动态绑定实现的,这就是我们所说的多态性。

多态机制的原理概括如下:当超类对象引用子类对象时,被引用对象的类型而不是被引用变量的类型决定了要调用哪个成员方法,但是要调用的方法必须在超类中定义,即由子类覆盖的方法。但是,它仍然取决于继承链中方法调用的优先级。确认方法,优先级如下。显示(O),超级。显示(O),这个。show((super)O),super.显示((超级)O)。

深入理解Java的三大特性之多态的更多相关文章

  1. [转]理解java的三大特性之多态

    java提高篇(四)-----理解java的三大特性之多态   面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外 ...

  2. java提高篇(四)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  3. java提高篇(三)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  4. 理解 Java 的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  5. 理解java的三大特性之多态(三)

    摘自:http://cmsblogs.com/?p=52 面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已 ...

  6. (转)java提高篇(四)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  7. 理解java的三大特性之多态

    所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个 ...

  8. java-----理解java的三大特性之多态

    的java提高篇(四)-----理解的java的三大特性之多态 面向对象编程有三大特性:封装,继承,多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外 ...

  9. java提高篇(二)-----理解java的三大特性之继承

    在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...

随机推荐

  1. vue项目中导出PDF的两种方式

    参考大家导出的方式,基本上是如下两种: 1.使用 html2Canvas + jsPDF 导出PDF, 这种方式什么都好,就是下载的pdf太模糊了.对要求好的pdf这种方式真是不行啊! 2.调用浏览器 ...

  2. 6. Java基本数据类型

    Java 基本数据类型 变量就是申请内存来存储值.也就是说,当创建变量的时候,需要在内存中申请空间. 内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据. 因此,通过定义不 ...

  3. LeetCode 腾讯精选50题--2的幂

    在二进制中,2的幂的数字用二进制表示时只会有一位表示为1,其余都为0,基于这个前提,可以有两种方案: 1. 做位移操作 2. 与数值取反并与原数值做与操作,判断是否与原来的数值相同 对于方案1,我的想 ...

  4. Flutter 37: 图解 Flutter 基本动画 (一)

    小菜一直对动画不太熟悉,最近学习了一些关于动画的皮毛知识,网上资料很多,小菜按自己的理解整理一下. Animation Animation 可以生成动画过程中的值,生成的值并非单一的 double 也 ...

  5. 使用SSH命令行远程登录运行在CloudFoundry上的应用

    当我试图用如下命令行采用SSH远程登录到运行在CloudFoundry环境下的应用时, cf ssh -N -T -L 9229:127.0.0.1:9229 jerry-demo-server 遇到 ...

  6. pymysql_mysql密码重置方法,连接局域网数据库的解决办法

    https://blog.csdn.net/qq_37176126/article/details/72824106   pymysql模块的操作 https://blog.csdn.net/skh2 ...

  7. Hadoop_23_MapReduce倒排索引实现

    1.1.倒排索引  根据属性的值来查找记录.这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址.由于不是由记录来确 定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(invert ...

  8. 反编译DLL并修改DLL中的内容

    使用场景:针对当前用户因不愿意进行软件版本升级,但又希望可以解决当前问题,此时可以考虑通过反编辑DLL进行修改内容,然后重新生成新的DLL 操作步骤: 1.首先使用ILSpy.exe反编译DLL,查看 ...

  9. SQLite3学习笔记(2)

      SQLite 创建表 SQLite 的CREATE TABLE 语句用于在任何指定的数据库创建一个新表. 创建新表,涉及到命名表.定义列及每一行的数据类型. CREATE TABLE 的基本语法如 ...

  10. Java入门第二季——Java中的this关键字

    如果想访问一个private属性,通常会使用setter和getter方法来操作属性,属性名经常会与参数名相同,我们为了区分属性和参数的概念,我们会在属性前面加上this关键字,此时代表我将一个参数的 ...