Java - 动态分派无效的例子

在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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值