Java多态性的“飘渺之旅”
原文出处:斯武丶风晴
摘要: 如何从Java多态性进行飘渺之旅呢? 我们用例子来旅行。
1 朵星人A:人类,是一个很奇妙的物种。 2 朵星人B:他们好像分为两种,嗯 先生,以及美女? 3 朵星人C:对,更年轻的有叫 美少女的。 4 朵星人D:他们之间怎么打招呼的?我们问问AI(编译器大佬)吧。。 5 朵星人A:可以有。启动吧~
第一次启动:
1 /** 2 * 编译时多态 3 * 4 * @author Sven Augustus 5 */ 6 public class StaticTest { 7 8 static abstract class Human { 9 10 } 11 12 static class Man extends StaticTest.Human { 13 14 } 15 16 static class Woman extends StaticTest.Human { 17 18 } 19 20 static class Girl extends StaticTest.Woman { 21 22 } 23 24 public void sayHello(Object guy) { 25 System.out.println("你..."); 26 } 27 28 public void sayHello(Human guy) { 29 System.out.println("你好"); 30 } 31 32 public void sayHello(Man guy) { 33 System.out.println("您好,先生"); 34 } 35 36 public void sayHello(Woman guy) { 37 System.out.println("您好,美女"); 38 } 39 40 public void sayHello(Girl guy) { 41 System.out.println("您好,美少女"); 42 } 43 44 public static void main(String[] args) { 45 StaticTest test = new StaticTest(); 46 StaticTest.Human manAsGuy = new StaticTest.Man(); 47 StaticTest.Human womanAsGuy = new StaticTest.Woman(); 48 StaticTest.Woman girlAsWoman = new StaticTest.Girl(); 49 test.sayHello(manAsGuy); 50 test.sayHello(womanAsGuy); 51 test.sayHello(girlAsWoman); 52 } 53 54 }
输出:
编译器大佬告诉了他们答案。
1 朵星人众人:纳尼,他们叫的好奇怪啊?好没礼貌啊,没“您”。为毛呢??还有最后一个明明是美少女,你怎么叫美女啊?! 2 编译器大佬:你们好意思吗,你们都标为人类或美女。 3 我怎么知道他们具体是先生还是美女,亦或是美少女啊!!!所以我就只知道这么多。
StaticTest.Human manAsGuy = new StaticTest.Man();
test.sayHello(manAsGuy);
从这里,Human 称为 声明类型(也有叫静态类型) ,Man 称为 实际类型。
很清楚,现在关键在于 manAsGuy 作为 参数。
1 在编译阶段,编译器就可以根据 参数 的 声明类型(或叫静态类型) 决定使用哪个重载版本的方法。
朵星人A:说的好有道理,我们让他们自己称呼吧。 朵星人B:可以有。 朵星人C:赞同。 朵星人D:不会有问题吧? 朵星人A:不会的。就这样吧~~~
第二次启动:
1 /** 2 * 运行时多态 3 * 4 * @author Sven Augustus 5 */ 6 public class DynamicTest { 7 8 static abstract class Human { 9 10 public void sayHello() { 11 System.out.println("你好"); 12 } 13 } 14 15 static class Man extends DynamicTest.Human { 16 17 public void sayHello() { 18 System.out.println("您好,我是Y先生"); 19 } 20 } 21 22 static class Woman extends DynamicTest.Human { 23 24 public void sayHello() { 25 System.out.println("您好,我是X美女"); 26 } 27 } 28 29 public static void main(String[] args) { 30 DynamicTest.Human manAsGuy = new DynamicTest.Man();// 注释1 31 DynamicTest.Human womanAsGuy = new DynamicTest.Woman(); 32 manAsGuy.sayHello(); 33 womanAsGuy.sayHello(); 34 } 35 36 }
输出:
编译器大佬好像去休息了,交给社会JVM回答问题。
DynamicTest.Human manAsGuy = new DynamicTest.Man();// 注释1
manAsGuy.sayHello();
这里与上个例子不同的是,manAsGuy不作为 参数,是作为引用变量 去 调用方法。
这时候,编译器只知道 引用变量manAsGuy的 静态类型,对于实际类型 就无能为力。因此在运行时由JVM方法表动态绑定。
我们发现, 引用变量调用方法 的时候,决定去调用哪个方法,是由 实际类型 在运行时确认调用哪个方法,而不是 声明类型(或叫静态类型)。 呵呵,当然这个解释还是比较勉强。我们继续。
朵星人A:咦,他们太客气了,都“您好”,还首先介绍自己,好不矜持啊。 朵星人B:地球人是这样的吗?? 朵星人C:是这样的。他们不知道对方是谁,只知道自己是谁的时候是这样的。 朵星人D:好像不是啊。 朵星人A:那你说是怎样的? 朵星人D:他们需要知道对方是谁啊! 朵星人B:有道理、 朵星人C:赞同。 朵星人A:就这样吧~~~
第三次启动:
1 /** 2 * 编译时多态 和 运行时多态 混合测试 3 * 4 * @author Sven Augustus 5 */ 6 public class MixTest { 7 8 static class Human { 9 10 public String sayHello(MixTest.Human human) { 11 return "你好"; 12 } 13 14 public String sayHello(MixTest.Man human) { 15 return "您好,先生"; 16 } 17 18 public String sayHello(MixTest.Woman human) { 19 return "您好,美女"; 20 } 21 22 /*public String sayHello(MixTest.Girl human) { 23 return "您好,美少女"; 24 }*/ 25 } 26 27 static class Man extends MixTest.Human { 28 29 public String sayHello(MixTest.Human human) { 30 return "你好,我是Y先生"; 31 } 32 33 public String sayHello(MixTest.Woman human) { 34 return "您好,美女,我是Y先生"; 35 } 36 37 public String sayHello(MixTest.Girl human) { 38 return "您好,美少女,我是Y先生"; 39 } 40 41 // 先生对先生比较谨慎,没那么快介绍自己 =。= 42 } 43 44 static class Woman extends MixTest.Human { 45 46 public String sayHello(MixTest.Human human) { 47 return "你好,我是X美女"; 48 } 49 50 public String sayHello(MixTest.Woman human) { 51 return "您好,美女,我是X美女"; 52 } 53 54 public String sayHello(MixTest.Girl human) { 55 return "您好,美少女,我是X美女"; 56 } 57 58 // 美女对先生比较含蓄,没那么快介绍自己 =。= 59 } 60 61 static class Girl extends MixTest.Woman { 62 63 public String sayHello(MixTest.Human human) { 64 return "你好,我是O美少女"; 65 } 66 67 } 68 69 public static void main(String[] args) { 70 MixTest test = new MixTest(); 71 MixTest.Human guy = new MixTest.Human(); 72 MixTest.Human manAsGuy = new MixTest.Man(); 73 MixTest.Man man = new MixTest.Man(); 74 MixTest.Human womanAsGuy = new MixTest.Woman(); 75 MixTest.Woman woman = new MixTest.Woman(); 76 MixTest.Girl girl = new MixTest.Girl(); 77 78 System.out.print("假设大家在QQ等聊天软件上认识,这时候一般来招呼如下"); 79 System.out.println("当然先生对先生比较谨慎,没那么快介绍自己:"); 80 printMessage("一个人 欢迎 一个人", guy.sayHello(guy), 81 "[我不想你知道我的性别,我也不知道你的性别,囧]"); 82 printMessage("一个人 欢迎 一名先生", guy.sayHello(man), 83 "[我不想你知道我的性别,我知道你是一名先生,嘿嘿]"); 84 printMessage("一个人 欢迎 一名美女", guy.sayHello(woman), 85 "[我不想你知道我的性别,我知道你是一名美女,哈哈]"); 86 printMessage("一个人[其实是先生] 欢迎 一个人", manAsGuy.sayHello(guy), 87 "[我不想你知道我的性别,但是你知道我是先生,可是我不知道你的性别,汗]"); 88 printMessage("一个人[其实是先生] 欢迎 一个人[其实是先生]", manAsGuy.sayHello(manAsGuy), 89 "[我不想你知道我的性别,但是你知道我是先生,可我不知道你的性别(或许你是一名先生),呵]"); 90 printMessage("一个人[其实是先生] 欢迎 一个人[其实是美女]", manAsGuy.sayHello(womanAsGuy), 91 "[我不想你知道我的性别,但是你知道我是先生,可我不知道你的性别(或许你是一名美女),嘿]"); 92 printMessage("一个人[其实是先生] 欢迎 一名先生", manAsGuy.sayHello(man), 93 "[我不想你知道我的性别,但是你知道我是先生,我知道你也是一名先生,呵呵]"); 94 printMessage("一个人[其实是先生] 欢迎 一名美女", manAsGuy.sayHello(woman), 95 "[我不想你知道我的性别,但是你知道我是先生,我知道你是一名美女,噢噢]"); 96 printMessage("一个人[其实是先生] 欢迎 一名美少女", manAsGuy.sayHello(girl), 97 "[我不想你知道我的性别,但是你知道我是先生,我知道你是一名美少女,噢]"); 98 printMessage("一名先生 欢迎 一个人 ", man.sayHello(guy), 99 "[我是一名光明磊落的先生,可我不知道你的性别,额]"); 100 printMessage("一名先生 欢迎 一个人[其实是先生]", man.sayHello(manAsGuy), 101 "[我是一名光明磊落的先生,可我不知道你的性别(或许你是一名先生),咦]"); 102 printMessage("一名先生 欢迎 一个人[其实是美女]", man.sayHello(womanAsGuy), 103 "[我是一名光明磊落的先生,可我不知道你的性别(或许你是一名美女),嗯]"); 104 printMessage("一名先生 欢迎 一名先生", man.sayHello(man), 105 "[我是一名光明磊落的先生,我知道你也是一名先生,非常好,我先观察]"); 106 printMessage("一名先生 欢迎 一名美女", man.sayHello(woman), 107 "[我是一名光明磊落的先生,我知道你是一名美女,我先介绍自己]"); 108 printMessage("一名先生 欢迎 一名美少女", man.sayHello(girl), 109 "[我是一名光明磊落的先生,我知道你是一名美少女,我先礼貌介绍自己]"); 110 } 111 112 private static volatile int index = 1; 113 114 private static void printMessage(String title, String message, String narrator) { 115 System.out.println((index++) + "、" + String.format("%-35s%-20s%s", 116 new String[]{title, message, narrator})); 117 } 118 119 }
输出:
社会JVM一片混沌,不知所云,乱出答案。
朵星人A:看不懂人类的世界,太复杂了吧。 朵星人B:地球人是这样的吗?? 朵星人C:是这样的。他们百变。 朵星人D:额。让人类自己解读吧。
现在 这个例子 混杂了 编译时多态 和 运行时多态。
因此,我们首先观察一下,发现:
a、结果 1-3中,是 单纯的编译时多态。
b、结果 4-8 对比 10-14中,“一个人[其实是先生]” 和 “ 一名先生 ”( 引用变量) 在欢迎(方法调用) 同一个类型的人(同一静态类型参数)的时候,欢迎语是一致(调用的具体方法可能一致的?)。
c、结果9 对比 15 中,我们发现结论 b 不生效了。为什么呢?我们发现 一个人[其实是先生]” 和 “ 一名先生 ”还是有区别的。
我们仔细观察一下代码实现。
Human类有 对 Human、Man、Woman的欢迎方法
Man类有 对 Human、Woman、Girl的欢迎方法
结果9:
MixTest.Human manAsGuy = new MixTest.Man();
manAsGuy.sayHello(girl),
因为manAsGuy 声明是Human 类,方法从Human类开始搜索,Human类没有欢迎Girl的方法,
因此按照最适合方法版本,兼容找到了Human 类的欢迎Woman的方法,
又因为实际类型是Man类,该方法有重写,因此实际执行了Man类的欢迎Woman的方法。
首先定义声明类型 与 实际类型 存在向上转型的情况,称之为“动态绑定”。
如 Parent p = new Children();
我们得出了一个
方法调用步骤:
1、编译器检查引用对象的声明类型、方法名;
假设我们调用x.func(args) 方法,如果x声明为X类,那么编译器会列举X类所有名称为func的方法,以及从X类的超类继承的所有名称为func的方法。
2、接下来,编译器检查方法提供中的参数类型
如果在第1步中列举的所有func方法中找到一个 参数类型 与 args的声明类型 最为匹配的,
如果方法调用,不是动态绑定,编译器就确定调用 该func(args)方法。
如果方法调用,是动态绑定。那么继续下一步。
--------------------------------以下动态绑定-------------------------------------------
3、当程序运行并且使用动态绑定调用方法时,JVM会调用x对象实际类型相匹配的方法版本。
意思就是,如果 X x= new T();实际类型是T类,那么如果T类有定义了与第2步方法签名一致的func(args)方法,也就是重写,那么T类的该func(args)方法会被JVM实际调用,否则就在T类的超类X类中继续寻找。