我已经用Java编程超过五年了,并且认为我知道重载和覆盖是如何工作的。只有一次我开始思考并写下以下的角落案例,我才意识到我几乎不知道它。为了游戏化这些细微差别,我在下面将它们列为一系列谜题。
单一分发
假设有以下类:
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; // What gets printed? 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不支持 双分发调度 ,因此,在处理方法参数时,重要的是参数的“声明”类型,而不是其“实际”类型。print(Object)将被调用,即使“实际”参数类型是String。
隐藏覆盖
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"
Java在检查子类覆盖之前,首先会选择要调用的方法。在这种情况下,声明的实例类型是Parent,唯一匹配的方法是Parent::print(Object)。当Java检查任何潜在的覆盖Parent::print(Object)的方法时,它没有找到任何覆盖方法,因此这只能是它的执行的方法。
暴露覆盖
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)方法,然后看到此方法被覆盖Child::print(String)。
模糊参数
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; // What gets printed? 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); // Does not compile
与第一个单分发single_dispatch示例类似,此处重要的是参数的声明类型,而不是实际类型。此外,如果有多个方法对给定参数同样有效,则Java会抛出编译错误并强制您指定应调用哪个方法。
多重继承 - 接口
interface Father { default void print() { log.info("I am Father!"); } } interface Mother { default void print() { log.info("I am Mother!"); } } class Child implements Father, Mother {}
new Child().print();
与前面的示例类似,这也不编译。具体来说,类定义本身Child将无法编译,因为在Father和中存在冲突的默认方法Mother。您需要更新Child类以指定其行为Child::print。 请参阅此处 以获取更详细的说明。
多重继承 - 类和接口
class ParentClass { void print() { log.info("I am a class!"); } } interface ParentInterface { default void print() { log.info("I am an interface!"); } } class Child extends ParentClass implements ParentInterface {}
new Child().print(); // Prints: "I am a class!"
说明: 上一节中 的链接文章 实际上也涵盖了这一点。如果类和接口之间存在继承冲突,则类获胜。
传递覆盖
class Parent { void print() { foo(); } void foo() { log.info("I am Parent!"); } } class Child extends Parent { void foo() { log.info("I am Child!"); } }
new Child().print(); // Prints: "I am Child!"
覆盖方法即使对传递调用也会生效。有人可能会认为Parent::print总会调用Parent::foo。但是如果方法被覆盖,那么Parent::print将调用被覆盖的版本foo()。
私有覆盖
class Parent { void print() { foo(); } private void foo() { log.info("I am Parent!"); } } class Child extends Parent { void foo() { log.info("I am Child!"); } } new Child().print(); // Prints: "I am Parent!"
除了与前一个一个区别外,其余相同。Parent.foo()现在被宣布为私有。
通常假设将方法从公共更改为私有,只要编译仍然成功,就是纯粹的重构更改。上面的例子表明这是错误的 - 即使编译成功,系统行为也会以戏剧性的方式发生变化。
通过@Override所有覆盖方法使用注释将有助于防止此类回归,一旦任何基本方法的可见性发生更改,就会产生编译错误。
静态覆盖
class Parent { static void print() { log.info("I am Parent!"); } } class Child extends Parent { static void print() { log.info("I am Child!"); } } Child child = new Child(); Parent parent = child; parent.print(); child.print(); parent.print(); // Prints: "I am Parent!" child.print(); // Prints: "I am Child!"
Java不允许重写静态方法。如果在父类和子类中都定义了相同的静态方法,则实例的实际类型根本不重要。只有声明的类型用于确定调用两个方法中的哪一个。
这与非静态方法的情况完全相反,其中忽略声明的类型以支持实际类型。因此,为什么在将方法从非静态更改为静态或反之亦然时需要小心。即使没有编译错误,系统行为也可能发生巨大变化。
这是使用@Override注释标记所有覆盖方法的另一个原因。在上面的例子中,添加注释时会出现编译错误Child::print,告诉您由于它是静态的,因此无法覆盖该方法。
这也是为什么永远不要使用类的实例调用静态方法的好习惯 - 它可能导致像上面这样令人惊讶的行为,并且在进行有问题的重构更改时无法提醒您。许多像Intellij这样的IDE会在从非静态上下文中调用静态方法时发出警告,最好跟进这些警告。
静态链接
class Parent { void print() { staticMethod(); instanceMethod(); } static void staticMethod() { log.info("Parent::staticMethod"); } void instanceMethod() { log.info("Parent::instanceMethod"); } } class Child extends Parent { static void staticMethod() { log.info("Child::staticMethod"); } void instanceMethod() { log.info("Child::instanceMethod"); } }
Child child = new Child(); child.print();
结果:
Parent::staticMethod Child::instanceMethod
这是我们之前介绍过的一些不同概念的组合。对于实例方法,即使调用者在父级中,覆盖也会生效。但是,对于静态方法,即使变量的声明类型是Child,Parent::staticMethod也会因为中间foo()方法而被调用。
总结
如果碰到其中一个,那就是非常棘手,继承容易出错。如果你想要聪明,有一天它会咬你的屁股。使用非常愚蠢的护栏和最佳实践来保护自己: