在Java语言中,动态分派是在运行时决定哪个多态方法将被执行的过程。规则非常简单:要执行的方法属于实际运行时类型,而不是声明的类型。例如:
A a = new B();
a.doSomething(..)
如果类A和类B都有一个名为doSomething()的方法,那么应该运行B类的doSomething()方法,因为这是实际的运行时对象类型。
但是,在某些情况下,开发人员可能会犯错误。例如,以下代码尝试从一个List中的索引0处删除元素两次(第二次使用其超类型Collection):
public class CollectionScenario {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
System.out.println(list);
list.remove(0);//should remove element at index 0, i.e. 10
System.out.println(list);
Collection<Integer> c = list;
c.remove(0);//should remove element at index 0, i.e. 20
System.out.println(list);
}
}
Output
[10, 20]
[20]
[20]
为了理解为什么在上面的示例中第二次调用 c.remove(0)
不起作用,我们来看一下这两个接口中remove()
方法的定义:
public interface Collection<E> extends Iterable<E> {
.....
boolean remove(Object o);
.....
}
public interface List<E> extends Collection<E> {
.....
boolean remove(Object o);
E remove(int index);
.....
}
Collection#remove(0)
实际上调用的是Collection#remove(Object o)
方法,而不是List#remove(int index)
方法。原因非常简单:Collection
类没有定义E remove(int index)
方法,因此它实际上没有被List
类重写。这意味着remove(index)
不是一个多态方法,所以动态分派在这里不适用。
为了更清楚地理解这个场景,让我们看另一个例子。
public class GeneralScenario {
public static class A {
public void doSomething(Object obj) {
System.out.println("A#doSomething(Object) " + obj);
}
}
public static class B extends A {
public void doSomething(int i) {
System.out.println("B#doSomething(int) " + i);
}
}
public static void main(String[] args) {
B b = new B();
b.doSomething(1);
A a = b;
a.doSomething(1);
}
}
Output
B#doSomething(int) 1
A#doSomething(Object) 1
就像集合的例子一样,在这里调用 a.doSomething(1)
似乎也没有分派到实际的运行时类型。请记住,动态分派是一个两步过程:
1、编译时方法绑定:当编译器看到像这样的语句时,
A a = new B();
a.doSomething(0)
它将 a.doSomething(0)
方法调用绑定到A#doSomething(Object)
。在这里,编译器并不关心右侧的赋值是什么(对于编译器来说,右侧只需要能够赋值给左侧即可,它不会深入分析被重写的方法是什么)。
2、运行时方法调用:JVM运行时只会在方法被实际重写时,才会将方法调用动态分派到其子类型。在这个例子中,没有方法被重写,因此没有动态分派,而是使用了编译器决定调用A#doSomething(Object)的方法。
让我们在类A中定义doSomething(int)
方法,这样它将被类B重写:
public class GeneralScenario2 {
public static class A {
public void doSomething(Object obj) {
System.out.println("A#doSomething(Object) " + obj);
}
public void doSomething(int i) {
System.out.println("A#doSomething(int) " + i);
}
}
public static class B extends A {
public void doSomething(int i) {
System.out.println("B#doSomething(int) " + i);
}
}
public static void main(String[] args) {
B b = new B();
b.doSomething(1);
A a = b;
a.doSomething(1);
}
}
Output
B#doSomething(int) 1
B#doSomething(int) 1
现在编译器首先将a.doSomething(1)
调用绑定到A#doSomething(int i)
,因为它具有最匹配的方法参数。在运行时,JVM会动态地将调用分派到重写的版本:
B#doSomething(int i)
。