从底层理解Java中方法的调用与多态

在《Java核心技术卷一》一书中,对方法调用有着比较明确的阐述。书中写道,假设要调用x.f(args),隐式参数x声明为类C的一个对象,调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名,编译器一一列举所有C类中名为f的方法以及其超类中访问属性为public且名为f的方法
  2. 编译器查看调用方法提供的参数类型,然后找到最恰当的一个f方法,例如调用x.f(“Hello”),编译器将调用f(String)
  3. private, static, final方法编译器都可以准确地知道应该调用哪个方法,称之为静态绑定;其他成员方法依赖于隐式参数的实际变量,我们称之为动态绑定。
  4. 程序运行时,虚拟机一定调用与x所引用对象实际类型最合适的那个类方法。、

总结起来,前三步都是编译器行为,其中前两步是重载解析。编译器并不知道一个变量属于哪个真实类,它只能读取“字面值”(因为编译过程没有内存参与,更不会创建对象),因此我们发现,它是在x的声明类C中寻找适合的函数。对于private, static, final方法,编译器可以准确地知道应该调用哪个方法,我们称之为静态绑定,然而对于普通成员方法,编译器并不知道具体调用哪个类的哪个方法,因此生成一条动态绑定指令。换句话说,当我们使用javac命令编译完字节码文件之后,这三步已经完成了。
第四步是虚拟机行为,虚拟机知道x的真实类,例如这里x的真实类是D,如果D有f(String)函数,那就直接调用它。

以上是从Java语言层面解释方法调用以及多态的过程,那么其在编译或者运行过程中,具体是怎么样的呢?看下面

public class Demo {
    private void test1() { }
    private final void test2() { }
    public void test3() { }
    public static void test4() { }

    public static void main(String[] args) {
        Demo d = new Demo();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
    }
}

上面代码字节码反编译后的结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Demo
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4                  // Method test1:()V
        12: aload_1
        13: invokespecial #5                  // Method test2:()V
        16: aload_1
        17: invokevirtual #6                  // Method test3:()V
        20: aload_1
        21: pop
        22: invokestatic  #7                  // Method test4:()V
        25: return

根据《Java核心技术》中的概念,在编译成字节码之后,已经完成了前三步,那么静态绑定和动态绑定是如何体现出来的呢?
new是创建对象,然后将对象的引用放入操作栈,同时复制栈顶元素并且入栈,然后调用了Demo类的初始化方法,并把对象引用弹栈,此时操作数栈中还剩下一个对象引用,然后将其复制到局部变量表1的位置,至此,Demo对象创建完毕。(对该部分陌生的同学可以看我这篇文章
在java字节码中,用invokevirtual调用普通成员方法,用invokespecial调用构造方法、私有方法和最终(final)方法,用invokestatic调用类方法。因此,所谓的动态绑定就是用invokevirtual标明的。
比较有趣的点是,在Java语言中,我们可以使用一个对象调用其类方法,也就是d.test4(), 我们观察字节码中发生了什么。字节码的第20行-22行对应着该过程,我们发现,虚拟机会先把局部变量表1位置的元素加载到栈中,然后又退栈,接着调用了test4(),说明调用test4()是不需要类对象的。
我们还会好奇,invokevirtual到底是如何实现多态的呢?当执行invokevirtual指令时,

  1. 先通过操作数栈中的对象引用找到对象
  2. 分析对象头(详情见该链接中的“对象的创建”部分),找到对象的实际Class
  3. Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值