深刻理解Java的三大特性之多态

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

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

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

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

1  定义

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

由于只有当程序运行时才肯定特定的类,因此能够在不修改源代码的状况下将引用变量绑定到不一样的类实现,这致使引用调用的特定方法的改变。也就是说,在不修改程序代码的状况下,能够改变程序运行时绑定的特定代码,从而使程序能够选择多种运行状态,即多态性。。spa

2  多态的实现

2.1 实现的条件

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

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

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

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

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

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

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

2.2 实现的方式

2.2.一、基于继承实现的多态

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

 1 public class Wine {
 2         private String name;
 3     
 4         public String getName() {
 5             return name;
 6         }
 7     
 8         public void setName(String name) {
 9             this.name = name;
10         }
11     
12         public Wine(){
13         }
14     
15         public String drink(){
16             return "喝的是 " + getName();
17         }
18     
19         /**
20          * 重写toString()
21          */
22         public String toString(){
23             return null;
24         }
25     }
26     
27     public class JNC extends Wine{
28         public JNC(){
29             setName("JNC");
30         }
31     
32         /**
33          * 重写父类方法,实现多态
34          */
35         public String drink(){
36             return "喝的是 " + getName();
37         }
38     
39         /**
40          * 重写toString()
41          */
42         public String toString(){
43             return "Wine : " + getName();
44         }
45     }
46     
47     public class JGJ extends Wine{
48         public JGJ(){
49             setName("JGJ");
50         }
51     
52         /**
53          * 重写父类方法,实现多态
54          */
55         public String drink(){
56             return "喝的是 " + getName();
57         }
58     
59         /**
60          * 重写toString()
61          */
62         public String toString(){
63             return "Wine : " + getName();
64         }
65     }
66     
67     public class Test {
68         public static void main(String[] args) {
69             //定义父类数组
70             Wine[] wines = new Wine[2];
71             //定义两个子类
72             JNC jnc = new JNC();
73             JGJ jgj = new JGJ();
74     
75             //父类引用子类对象
76             wines[0] = jnc;
77             wines[1] = jgj;
78     
79             for(int i = 0 ; i < 2 ; i++){
80                 System.out.println(wines[i].toString() + "--" + wines[i].drink());
81             }
82             System.out.println("-------------------------------");
83     
84         }
85     }

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.二、基于接口实现的多态

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

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

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

3  经典实战

 1 public class A {   
 2     public String show(D obj) {       
 3         return ("A and D");   
 4     }   
 5     public String show(A obj) {       
 6         return ("A and A");   
 7     } 
 8 }
 9 
10 public class B extends A {   
11     public String show(B obj){       
12         return ("B and B");   
13     }   
14     public String show(A obj){       
15         return ("B and A");   
16     } 
17 }
18 
19 public class C extends B{}
20 
21 public class D extends B{}
22 
23 public class Test {   
24     public static void main(String[] args) {       
25         A a1 = new A();       
26         A a2 = new B();       
27         B b = new B();       
28         C c = new C();       
29         D d = new D();       
30         System.out.println(a1.show(b));   ①
31         System.out.println(a1.show(c));   ②
32         System.out.println(a1.show(d));   ③
33         System.out.println(a2.show(b));   ④
34         System.out.println(a2.show(c));   ⑤
35         System.out.println(a2.show(d));   ⑥
36         System.out.println(b.show(b));    ⑦
37         System.out.println(b.show(c));    ⑧
38         System.out.println(b.show(d));    ⑨       
39    }
40 }

输出结果

分析

①②③ 比较好理解,通常不会出错。④⑤ 就有点糊涂了,为何输出的不是「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)。

相关文章
相关标签/搜索