Java 中的方法重写、静态连接、动态连接

2019-12-08  本文已影响0人  你可记得叫安可
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 条方法调用指令:


只要能被 invokestaticinvokespecial 指令调用的方法,都可以在解析阶段使用静态连接,对应上面的 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 方法。被重写的方法是一个虚函数,因此它的调用应该采用动态分派机制,所以虚函数只有在运行时根据调用者和参数的实际类型通过动态连接的方式找到实际的方法。

上一篇下一篇

猜你喜欢

热点阅读