概述

java作为一门面向对象的语言,其中方法重载与方法重写应该耳熟能详了。

接下来从以下几点来理重载与重写在虚拟机层面如何确定正确的目标方法:

重载与重写的基础定义

重载与静态分派

重写与动态分派

重载与重写的基础定义

关于重载与重写的定义这里再简单复述一下:

重载指方法名称一样,但参数个数或参数类型不一样的多个方法。

重写指子类重写了父类参数签名与返回类型一致的同名方法。

重载与静态分派

先看一道面试题,分析下面代码的执行结果。

public class TestOverload {
static class Cat{}
static class Tom extends Cat{}
static class Jack extends Cat{}
public void overloadMethod(Cat cat) {
System.out.println("cat method");
}
public void overloadMethod(Tom tom) {
System.out.println("tom method");
}
public void overloadMethod(Jack jack) {
System.out.println("jack method");
}
public static void main(String[] args) {
TestOverload testOverload = new TestOverload();
Cat tom = new Tom();
Cat jack = new Jack();
testOverload.overloadMethod(tom);
testOverload.overloadMethod(jack);
System.out.println("----------");
Tom tom2 = new Tom();
Jack jack2 = new Jack();
testOverload.overloadMethod(tom2);
testOverload.overloadMethod(jack2);
}
}

输出:

cat method

cat method

----------

tom method

jack method

复制代码

上面这段代码如果对重载不是很理解可能会输出结果有点模糊。下面我们来具体分析一下重载方法的调用。

要理解上面的输出结果首先我们要知道创建对象的"类型"

Cat tom = new Tom();

复制代码

这里声明的Cat tom是代表静态类型为Cat,而后面的new Tom()是实际类型为Tom。

当我们调用overloadMethod()重载方法时:

首先确定接收者对象(是testOverload对象)

选择重载方法。虚拟机根据参数数量以及参数类型来确定要调用的重载方法。而这里选择重载方法就是以静态类型为准,由于静态类型在编译阶段就可以确定,所以在编译阶段根据静态类型可以确定重载方法。

关于静态分派

静态分派是指以静态类型为准决定执行的目标方法。由于在编译阶段可以确定静态类型, 所以静态分派是发生在编译阶段的。

下面再通过上面测试代码的main方法字节码来观察一下:

... 省略对象创建等字节码

24 aload_1
25 aload_2
26 invokevirtual #13 
29 aload_1
30 aload_3
31 invokevirtual #13 
34 getstatic #2 
37 ldc #14 
39 invokevirtual #4 
42 new #9 
45 dup
46 invokespecial #10 >
49 astore 4
51 new #11 
54 dup
55 invokespecial #12 >
58 astore 5
60 aload_1
61 aload 4
63 invokevirtual #15 
66 aload_1
67 aload 5
69 invokevirtual #16 
72 return

复制代码

字节码索引26跟31,对应我们源码中的:

testOverload.overloadMethod(tom);

testOverload.overloadMethod(jack);

复制代码

都是调用同一个常量池的符号引用索引#13,对应的方法符号引用描述符就是(Lcn/tomcoding/overload/TestOverload$Cat;)V。

字节码索引34到39输出"----------"

字节码索引42到61创建了tom2,jack2对象以及存储到局部遍历表等。

字节码索引63,对应我们源码中的:

testOverload.overloadMethod(tom2);

复制代码

调用对应的常量池的符号引用索引#15,对应的方法符号引用描述符就是(Lcn/tomcoding/overload/TestOverload$Tom;)V。

字节码索引69,对应我们源码中的:

testOverload.overloadMethod(jack2);

复制代码

调用对应的常量池的符号引用索引#16,对应的方法符号引用描述符就是(Lcn/tomcoding/overload/TestOverload$Jack;)V。

重写与动态分派

先来段示例代码:

public class TestOverride {
static abstract class Cat{
abstract void overrideMethod();
}
static class Tom extends Cat{
@Override
void overrideMethod() {
System.out.println("tom method");
}
}
static class Jack extends Cat{
@Override
void overrideMethod() {
System.out.println("jack method");
}
}
public static void main(String[] args) {
Cat tom = new Tom();
Cat jack = new Jack();
tom.overrideMethod();
jack.overrideMethod();
}
}

输出:

tom method

jack method

复制代码

上面代码的输出应该都能正确的分析出来(错了的去撸java编程思想)

通过上述对重载与静态类型的理解,重写方法的调用就是以实际类型为准。

当我们以上述tom.overrideMethod();调用为例,tom对象代表overrideMethod()方法的接收者(或所有者)。

下面分析下示例的字节码部分:

0 new #2 
3 dup
4 invokespecial #3 >
7 astore_1
8 new #4 
11 dup
12 invokespecial #5 >
15 astore_2
16 aload_1
17 invokevirtual #6 
20 aload_2
21 invokevirtual #6 
24 return

复制代码

字节码索引0到15,创建了tom,jack对象并初始化,将引用存储到局部变量表中等。

字节码索引17跟21调用overrideMethod()方法。

这里咋一看符号引用都是cn/tomcoding/override/TestOverride$Cat.overrideMethod,但invokevirtual的处理过程如下:

找到操作数栈顶第一个元素的实际类型,这里记为C。

如果类型C找到名称与描述符相符的方法,则进行权限校验,通过后并返回直接引用。

如果类型C中没有找到,则按照继承关系从下往上依次查找,找到后按第2步验证处理。

最终没有找到则抛出异常。

这里主要在第一步,找到操作数栈顶第一个元素的实际类型为接收者。这种在运行期根据实际类型来确定要执行的方法就称为动态分派。

总结

重载与重写的定义理解并不难,但如果了解虚拟机是如何确认调用方法的过程就可以更深层次去理解两者。也不会被各种面试题折腾。