多态性之编译期多态和运行期多态(JAVA版)

多态性之编译期多态和运行期多态(JAVA版)
上一篇讲述了C++中的多态性,由于多态性是面向对象编程中常用的特性,所以JAVA作为面向对象的主流语言自然也有多态性,它的多态性其实和C++的概念差不多,只是实现形式和表现形式不一样。在C++中可能还会提到多态的划分,但是在JAVA中可能很多人都不会听到编译期多态和运行期多态这种划分,一般我们说到多态都是指运行期多态,因为这才是面向对象思想的真正体现之处,即OOP(面向对象)的多态性的体现,所以JAVA中我们不再讨论编译期多态这个问题,只重点讨论运行期多态,下面简称运行期多态为JAVA中的多态性。

1. 编译期多态(静态多态)
如上所述,此处不再多说,大家如果学习JAVA的话就不要深究这个问题了,直接进入运行期多态。
2. 运行期多态(动态多态)
运行期多态主要是指在程序运行的时候,动态绑定所调用的函数,动态地找到了调用函数的入口地址,从而确定到底调用哪个函数。
(1)前提
A. 要有继承关系。
B. 要有方法重写。其实没有也是可以的,但是如果没有这个就没有意义。
C. 要有父类引用指向子类对象。

(2)多态中的成员访问特点
A. 成员变量:编译看左边,运行看左边。
B. 构造方法:创建子类对象的时候,访问父类的构造方法,对父类的数据进行初始化。
C. 成员方法:编译看左边,运行看右边。由于成员方法存在方法重写,所以它运行看右边。 D. 静态方法:编译看左边,运行看左边。静态和类相关,算不上重写,所以,访问还是左边的。

(3)多态性的例子
下面的例子大部分引用自传智播客风清扬的程序,这里主要是引导大家进行理解,所以选择这些十分经典的例子进行讲解,也有部分是我新增的例子,为了帮助大家加深理解。

  1. // 例1:多态性
  2. class Fu {
  3. public int num = 100;
  4. public void show() {
  5. System.out.println("show Fu");
  6. }
  7. public static void function() {
  8. System.out.println("function Fu");
  9. }
  10. }
  11. class Zi extends Fu {
  12. public int num = 1000;
  13. public int num2 = 200;
  14. public void show() {
  15. System.out.println("show Zi");
  16. }
  17. public void method() {
  18. System.out.println("method zi");
  19. }
  20. public static void function() {
  21. System.out.println("function Zi");
  22. }
  23. }
  24. class DuoTaiDemo {
  25. public static void main(String[] args) {
  26. //要有父类引用指向子类对象。
  27. //父 f =new 子();
  28. Fu f = new Zi();
  29. System.out.println(f.num);
  30. //找不到符号
  31. //System.out.println(f.num2);
  32. f.show();
  33. //找不到符号
  34. //f.method();
  35. f.function();
  36. }
  37. }

  1. 运行结果:
  2. 100
  3. show Zi
  4. function Fu


例1是一个经典的多态性例子,父类中和子类中有一些同样名称的成员方法和成员变量,那么这里的访问原则就遵从(2)中的原则,从运行结果可以很清楚地看到这一点。我们这里关键来说说多态性到底体现在哪里。从main方法中可以看到,我们定义了一个父类的引用然后指向了子类的对象,我们关键来研究show()方法的调用,因为这才是多态性的关键之处。这里其实存在一个向上转型,即将子类对象向上转型到了一个父类引用,然后我们利用这个父类引用调用show()方法的时候,由于在子类中重写了show()方法,并且该方法是一个普通的成员方法,并不是什么静态方法,所以,遵从“编译看左边,运行看右边”的原则,这个原则正是多态性的体现。这是因为在运行期间才实现的动态绑定,将父类的引用绑定到了子类的对象上,从而用父类的引用去调用父类和子类都有的方法时,实际上调用的是子类的方法,这就是多态性。虽然是父类的引用,但是在运行期间却绑定到了子类对象上。
同时,大家需要注意,main方法中注释掉的部分,父类的引用即使绑定到了子类对象上,但是依然是不能访问子类的特有成员和特有方法的,比如main方法中的f.num2和f.method()都会出错,这也是多态性的一个缺点,无法调用子类特有的功能。
上图是针对例1给出的一个内存解释,该图中的程序和例1并不完全一致,但是结构是类似的,大家根据此图可以更加清晰地看出多态的实现过程。
下面给出一些其它例子。供大家学习参考。
  1. // 例2:向上向下转型
  2. class 孔子爹 {
  3. public int age = 40;
  4. public void teach() {
  5. System.out.println("讲解JavaSE");
  6. }
  7. }
  8. class 孔子 extends 孔子爹 {
  9. public int age = 20;
  10. public void teach() {
  11. System.out.println("讲解论语");
  12. }
  13. public void playGame() {
  14. System.out.println("英雄联盟");
  15. }
  16. }
  17. //Java培训特别火,很多人来请孔子爹去讲课,这一天孔子爹被请走了
  18. //但是还有人来请,就剩孔子在家,价格还挺高。孔子一想,我是不是可以考虑去呢?
  19. //然后就穿上爹的衣服,带上爹的眼睛,粘上爹的胡子。就开始装爹
  20. //向上转型
  21. 孔子爹 k爹 = new 孔子();
  22. //到人家那里去了
  23. System.out.println(k爹.age); //40
  24. k爹.teach(); //讲解论语
  25. //k爹.playGame(); //这是儿子才能做的
  26. //讲完了,下班回家了
  27. //脱下爹的装备,换上自己的装备
  28. //向下转型
  29. 孔子 k = (孔子) k爹;
  30. System.out.println(k.age); //20
  31. k.teach(); //讲解论语
  32. k.playGame(); //英雄联盟


例2很形象地说明了向上转型和向下转型的问题,这里的向上转型就是多态性的一个体现。注意,这个例子只是用来让大家理解,并不能直接运行,大家可以修改成合理的字母表示,然后运行。这个例子是摘自风清扬的一个经典的例子,十分形象合理。
  1. /*
  2. 例3 多态的好处:
  3. A:提高了代码的维护性(继承保证)
  4. B:提高了代码的扩展性(由多态保证)
  5. 猫狗案例代码
  6. */
  7. class Animal {
  8. public void eat(){
  9. System.out.println("eat");
  10. }
  11. public void sleep(){
  12. System.out.println("sleep");
  13. }
  14. }
  15. class Dog extends Animal {
  16. public void eat(){
  17. System.out.println("狗吃肉");
  18. }
  19. public void sleep(){
  20. System.out.println("狗站着睡觉");
  21. }
  22. }
  23. class Cat extends Animal {
  24. public void eat() {
  25. System.out.println("猫吃鱼");
  26. }
  27. public void sleep() {
  28. System.out.println("猫趴着睡觉");
  29. }
  30. }
  31. class Pig extends Animal {
  32. public void eat() {
  33. System.out.println("猪吃白菜");
  34. }
  35. public void sleep() {
  36. System.out.println("猪侧着睡");
  37. }
  38. }
  39. //针对动物操作的工具类
  40. class AnimalTool {
  41. private AnimalTool(){}
  42. /*
  43. //调用猫的功能
  44. public static void useCat(Cat c) {
  45. c.eat();
  46. c.sleep();
  47. }
  48. //调用狗的功能
  49. public static void useDog(Dog d) {
  50. d.eat();
  51. d.sleep();
  52. }
  53. //调用猪的功能
  54. public static void usePig(Pig p) {
  55. p.eat();
  56. p.sleep();
  57. }
  58. */
  59. public static void useAnimal(Animal a) {
  60. a.eat();
  61. a.sleep();
  62. }
  63. }
  64. class DuoTaiDemo2 {
  65. public static void main(String[] args) {
  66. //我喜欢猫,就养了一只
  67. Cat c = new Cat();
  68. c.eat();
  69. c.sleep();
  70. //我很喜欢猫,所以,又养了一只
  71. Cat c2 = new Cat();
  72. c2.eat();
  73. c2.sleep();
  74. //我特别喜欢猫,又养了一只
  75. Cat c3 = new Cat();
  76. c3.eat();
  77. c3.sleep();
  78. //...
  79. System.out.println("--------------");
  80. //问题来了,我养了很多只猫,每次创建对象是可以接受的
  81. //但是呢?调用方法,你不觉得很相似吗?仅仅是对象名不一样。
  82. //我们准备用方法改进
  83. //调用方式改进版本
  84. //useCat(c);
  85. //useCat(c2);
  86. //useCat(c3);
  87. //AnimalTool.useCat(c);
  88. //AnimalTool.useCat(c2);
  89. //AnimalTool.useCat(c3);
  90. AnimalTool.useAnimal(c);
  91. AnimalTool.useAnimal(c2);
  92. AnimalTool.useAnimal(c3);
  93. System.out.println("--------------");
  94. //我喜欢狗
  95. Dog d = new Dog();
  96. Dog d2 = new Dog();
  97. Dog d3 = new Dog();
  98. //AnimalTool.useDog(d);
  99. //AnimalTool.useDog(d2);
  100. //AnimalTool.useDog(d3);
  101. AnimalTool.useAnimal(d);
  102. AnimalTool.useAnimal(d2);
  103. AnimalTool.useAnimal(d3);
  104. System.out.println("--------------");
  105. //我喜欢宠物猪
  106. //定义一个猪类,它要继承自动物,提供两个方法,并且还得在工具类中添加该类方法调用
  107. Pig p = new Pig();
  108. Pig p2 = new Pig();
  109. Pig p3 = new Pig();
  110. //AnimalTool.usePig(p);
  111. //AnimalTool.usePig(p2);
  112. //AnimalTool.usePig(p3);
  113. AnimalTool.useAnimal(p);
  114. AnimalTool.useAnimal(p2);
  115. AnimalTool.useAnimal(p3);
  116. System.out.println("--------------");
  117. //我喜欢宠物狼,老虎,豹子...
  118. //定义对应的类,继承自动物,提供对应的方法重写,并在工具类添加方法调用
  119. //前面几个必须写,我是没有意见的
  120. //但是,工具类每次都改,麻烦不
  121. //我就想,你能不能不改了
  122. //太简单:把所有的动物都写上。问题是名字是什么呢?到底哪些需要被加入呢?
  123. //改用另一种解决方案。
  124. }
  125. /*
  126. //调用猫的功能
  127. public static void useCat(Cat c) {
  128. c.eat();
  129. c.sleep();
  130. }
  131. //调用狗的功能
  132. public static void useDog(Dog d) {
  133. d.eat();
  134. d.sleep();
  135. }
  136. */
  137. }


例3是一个更加全面的例子,也是摘自风清扬的例子,供大家学习参考。这个例子是说明一个简化的设计思想,应用到了多态,是一个稍微复杂一点的例子,大家可以用心去按照注释的思路自己思考下。
最后,给出一个百度百科的例子,这个例子主要说明了接口和多态性的应用。
  1. public interface Parent//父类接口
  2. {
  3. public void simpleCall();
  4. }
  5. public class Child_A implements Parent
  6. {
  7. public void simpleCall();
  8. {
  9. //具体的实现细节;
  10. }
  11. }
  12. public class Child_B implements Parent
  13. {
  14. public void simpleCall();
  15. {
  16. //具体的实现细节;
  17. }
  18. }



然后,我们可以看到
Parent pa = new Child_A();
pa.simpleCall()则显然是调用Child_A的方法;
Parent pa = new Child_B();
pa.simpleCall()则是在调用Child_B的方法。所以,我们对于抽象的父类或者接口给出了我们的具体实现后,pa 可以完全不用管实现的细节,只访问我们定义的方法,就可以了。事实上,这就是多态所起的作用,可以实现控制反转这在大量的J2EE轻量级框架中被用到,比如Spring的依赖注入机制。

3. 总结 总之,在JAVA中大家就不要去过多纠结编译期多态和运行期多态,只要掌握好常用的多态性即运行期多态即可。这篇文章可能存在很多纰漏,希望大家看到后给予指正,谢谢。

    推荐阅读