JAVA 编程五年多,我自以为已经熟谙 Overload 和 Override 背后的工作机制 。当开始思考和记录下面这些案例时,才意识到我对它们的了解并不像自己想象的那样 。为了让内容更有趣,下面会把它们列为一系列谜题,同时也提供了答案 。如果你能不偷看做出所有答案,我会对你刮目相看 。
1. 单一分派
给定下面两个类:
class Parent { void print(String a) { log.info("Parent - String"); } void print(Object a) { log.info("Parent - Object"); }}class Child extends Parent { void print(String a) { log.info("Child - String"); } void print(Object a) { log.info("Child - Object"); }}
下面代码打印结果是什么?
String string = "";Object stringObject = string;// 打印结果是什么?Child child = new Child();child.print(string);child.print(stringObject);Parent parent = new Child();parent.print(string);parent.print(stringObject);
答案:
child.print(string); // Prints: "Child - String"child.print(stringObject); // Prints: "Child - Object"parent.print(string); // Prints: "Child - String"parent.print(stringObject); // Prints: "Child - Object"child.print(string) 和 parent.print(string) 是 Java 面向对象程序编程的教科书式示例 。调用的方法取决于实例的“实际”类型,而非“声明”类型 。也就是说,不论把变量定义为 Child 还是Parent,因为实际的实例类型是Child 都会调用 Child::print 。
第二组输出结果更为棘手 。stringObject 和 string 是完全相同的字符串,唯一的区别在于 string 声明为 String,而 stringObject 声明为 Object 。Java 不支持双重分派(double-dispatch),因此在处理方法参数时,参数的“声明”类型优先于“实际”类型 。即使参数“实际”类型是 String 还是会调用 print(Object) 。
2. 隐式 Override
给定下面两个类:
class Parent { void print(Object a) { log.info("Parent - Object"); }}class Child extends Parent { void print(String a) { log.info("Child - String"); }}
下面代码打印结果是什么?
String string = "";Parent parent = new Child();parent.print(string);
答案:
parent.print(string); // Prints: "Parent - Object"
实例的“实际”类型是 Child,声明的参数类型是 String 。代码中确实有一个 Child::print(String)方法 。前面的例子中 parent.print(string) 调用的正是这个方法,但这里不是 。
检查子类 Override 前,Java 似乎要先选择调用哪个方法 。这种情况下,实例声明的类型是Parent,而 Parent 中唯一匹配的方法是 Parent::print(Object) 。接着,Java 会检查Parent::print(Object) 有没有潜在的 Override 方法,结果没有找到 。因此,最后执行的就是这个方法 。
3. 显式 Override
给定下面两个类:
class Parent { void print(Object a) { log.info("Parent - Object!"); } void print(String a) { throw new RuntimeException(); }}class Child extends Parent { void print(String a) { log.info("Child - String!"); }}
下面代码打印结果是什么?
String string = "";Parent parent = new Child();parent.print(string);
答案:
parent.print(string); // Prints: "Child - String!"
这个例子与前面的唯一区别在于添加了一个新的 Parent::print(String) 方法 。实际上这个方法从来没有执行 。如果运行,会抛出一个异常 。然而,正是因为它的存在让 Java 执行了一个不同的方法 。
运行时在分析 parent.print(String) 的时候,找到了一个可以匹配的 Parent::print(String) 方法,然后看到这个方法被 Child::print(String) 覆写 。
通常认为如果不调用,只是添加一个新方法绝不会改变系统行为 。上面的例子显然是另一种情况 。
4. 带有歧义的参数
给定下面的类:
class Foo { void print(Cloneable a) { log.info("I am cloneable!"); } void print(Map a) { log.info("I am Map!"); }}
下面代码打印结果是什么?
HashMap cloneableMap = new HashMap();Cloneable cloneable = cloneableMap;Map map = cloneableMap;// 打印结果是什么?Foo foo = new Foo();foo.print(map);foo.print(cloneable);foo.print(cloneableMap);
答案:
foo.print(map); // Prints: "I am Map!"foo.print(cloneable); // Prints: "I am cloneable!"foo.print(cloneableMap); // 编译失败
与单一分派类似,参数声明的类型优先于实际类型 。如果传入参数有多个方法可以匹配,Java 会抛出编译错误,要求明确指定调用哪个方法 。
推荐阅读
- 详解Java多线程锁之Lock和ReadWriteLock
- Java面向对象——成员变量和局部变量
- 卫生间墙体渗水 卫生间与房间共同的墙渗水怎么办
- java第一次调用 Hadoop Java API
- 2019年最流行的五大JavaScript 自动化测试框架
- Java虚拟机最多支持多少个线程?
- Java服务端推送消息有那么难吗?
- 西藏的茶文化与茶俗
- 怎么判断车头与墙壁的距离,只要看准这个点,每次停车都很准确
- 徽州茶文化与徽州茶俗