Java 中的方法重写、静态连接、动态连接
public class DynamicLinking {
public static class Base {
public void fun1() {
System.out.println("Base fun1");
}
}
public static class Derived extends Base {
@Override
public void fun1() {
System.out.println("Derived fun1");
}
}
public static void main(String[] args) {
Base base = new Derived();
base.fun1(); // 打印 "Derived fun1"
}
}
我们先看上面代码的例子。我们实例化了 Derived
,但在运行时将其类型转换为了 Base
,为什么 base.fun1()
确是调用的子类重写的方法呢?
静态连接
所有的方法调用在编译好的 Class 文件里面都是一个常量池中的符号引用。运行时,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。这种在类加载阶段 JVM 就能直接找到真正调用地址,并替换它所对应的符号引用,这个过程我们叫做静态连接。符合这样条件的方法,我们称为“编译期可知,运行期不可变”的方法,主要包括了静态方法和私有方法两大类。这两种方法都不可能以继承或者其它方式被重写,因此它们适合在类加载阶段进行解析。 JVM中提供了 5 条方法调用指令:
-
invokestatic
:调用静态方法 -
invokespecial
:调用实例构造器<init>
方法、私有方法和父类方法 -
invokevirtual
:调用所有的虚方法 -
invokeinterface
:调用接口方法,会在运行时再确定一个实现此接口的对象 -
invokedynamic
:这个是新加的,用于支持 Java 的动态语言特性。
只要能被 invokestatic
和 invokespecial
指令调用的方法,都可以在解析阶段使用静态连接,对应上面的 JVM 指令,符合静态连接的有静态方法、私有方法、实例构造器、父类方法,它们会在类加载的时候,把符号引用解析为该方法的直接引用。
能够在类加载阶段,方法的符号地址就能转换为直接地址的方法我们称为非虚方法(这里的虚函数、非虚函数跟 C++ 中是差不多的概念)。其实非虚方法除了以上两个之外还有 final
修饰的方法。虽然 final
方法是使用 invokevirtual
指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接受者进行多态选择,因此它符合 能够在类加载阶段,方法的符号地址就能转换为直接地址的方法。
重载
public class StaticDispatch {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human guy) {
System.out.println("hello, guy");
}
public void sayHello(Man guy) {
System.out.println("hello, gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello, lady");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
// 运行结果
// hello, guy
// hello, guy
上面的 sayHello
有三个重载方法,为什么运行时的 sayHello(man)
和 sayHello(woman)
都会选择 sayHello(Human)
这个重载方法呢?
这涉及到JVM虚拟机中的一个概念,叫静态分派。我们将代码 Human man = new Man()
中的声明类型 Human
称作静态类型或外观类型,将 Man
称为变量的实际类型。静态类型是在编译期确定,其变化只会在使用时发生,且变量本身的静态类型不会发生改变。而实际类型是运行时才会确定的。
Human man = new Man();
// 变量的实际类型改变
man = new Woman();
// 静态类型在使用处改变
staticDispatch.sayHello((Man) man);
staticDispatch.sayHello((Woman) man)
对于重载,JVM虚拟机会在编译期就根据方法参数的静态类型,找到对应的重载方法,将方法的符号引用替换为直接引用。这种根据静态类型寻找方法的机制叫做静态分派。也就是说重载方法在调用处使用静态分派的机制,被静态连接成了直接引用。
- 因此,重载是
静态分派
的应用,而重写则是动态分派
的应用。
重写
回到文章最开头的例子,显然 Derived
类的 fun1
方法重写了 Base
类的 fun1
方法。被重写的方法是一个虚函数,因此它的调用应该采用动态分派机制,所以虚函数只有在运行时根据调用者和参数的实际类型通过动态连接的方式找到实际的方法。