方法重载的底层原理

关于写这篇文章,是来自于一个同学在群里抛出这么一道面试题,问执行结果是什么?

public class OverloadTest {


    static abstract class A{}

    static class B extends A {}

    static class C extends A {}

    public void sayHello(A a){
        System.out.println("a");
    }

    public void sayHello(B a){
        System.out.println("b");
    }

    public void sayHello(C b){
        System.out.println("c");
    }

    // Object 参数
    public void say(Object arg) {
        System.out.println("hello object");
    }


    // int 参数
    public void say(int arg) {
        System.out.println("hello int");
    }

    // long 参数
    public void say(long arg) {
        System.out.println("hello long");
    }

    // char 参数
    public void say(char arg) {
        System.out.println("hello char");
    }

    // Character 参数
    public void say(Character arg) {
        System.out.println("hello character");
    }

    // 变长参数
    public void say(char... arg) {
        System.out.println("hello char...");
    }

    // Serializable 参数
    public void say(Serializable arg) {
        System.out.println("hello serializable");
    }


    public static void main(String[] args) {
        OverloadTest overloadTest = new OverloadTest();
        overloadTest.say('a');
        overloadTest.say("a");

        A b = new B();
        A c = new C();
        overloadTest.sayHello(b);
        overloadTest.sayHello(c);
        overloadTest.sayHello((B)b);
    }
}
复制代码

输出的结果如下。

hello char
hello serializable
a
a
b
复制代码

很明显涉及到方法重载(overload),为什么会是这个结果?要从我们开始学Java的时说起,那时老师就告诉我们两个结论。

javac编译器在编译阶段会根据参数的静态类型来决定选择哪个重载版本。 重载优先级,先匹配参数个数;再匹配参数类型的直接所属类;如果没有找到直接的所属类,会向上转型(包装类 -> 父类 -> 接口);如果向上转型无果,再查找可变参数列表;以上都找不到,则报找不到方法错误。

上面提到了静态类型,我举列说明一下。

A b = new B();
复制代码

这里的A就是静态类型,编译阶段可确定;那么相反B就是实际类型,只能运行阶段才能确定。

我估计知道答案的同学很多,但要搞明白整个底层原理的同学很少,这里涉及到Java方法底层调用的原理。

方法调用

其实说白了,JVM调用Java程序时,其实也是执行的机器指令,利用字节码解释器作为跨越字节码与机器指令的桥梁,也就是说一个字节码对应一段特定逻辑的本地机器指令,而JVM在解释执行字节码指令时,会直接调用字节码所对应的机器指令。关于它是怎么调用的?如果你感兴趣的话,可以去了解一下C的函数指针,它其实就是将函数指针指向这段机器指令的首地址,从而实现C语言直接调用机器指令的目的(以前写exp经常这么干)。

我承认上面这段,有点难。

简而言之,Java调用方法其实用到了字节码指令,最终查找相应的机器指令,来实现方法的调用。

那么关于方法调用,Java提供了5个字节码指令。

  • invokestatic:调用类方法(编译阶段确定方法调用版本)。

  • invokespecial:调用构造器方法、私有方法及父类方法(编译阶段确定方法调用版本)。

  • invokevirtual:调用实例方法(虚方法)。

  • invokeinterface:调用接口方法,在运行再确定一个实现此接口的对象。

  • invokedynamic:由用户引导方法决定。

invokestatic和invokespecial指令在类加载时,就能把符号引用(即逻辑地址,与虚拟机内存无关)解析为直接引用,符合这个条件的有静态方法、实例构造器方法、私有方法、父类方法这4类,叫非虚方法。

非虚方法除了上面静态方法、实例构造器方法、私有方法、父类方法这4种方法之外,还包括final方法。虽然final方法使用invokevirtual指令来调用,但是final方法无法被覆盖,没有其他版本,无需对方法接收者进行多态选择,或者说多态选择的结果是唯一的。

底层实现

要看它底层的实现,我们还是得要看字节码,我通过javap工具把main方法的字节码给各位展示出来,如下所示。

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #14                 // class com/yrzx404/base/code/OverloadTest
         3: dup
         4: invokespecial #15                 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        97
        11: invokevirtual #16                 // Method say:(C)V
        14: aload_1
        15: ldc           #3                  // String a
        17: invokevirtual #17                 // Method say:(Ljava/io/Serializable;)V
        20: new           #18                 // class com/yrzx404/base/code/OverloadTest$B
        23: dup
        24: invokespecial #19                 // Method com/yrzx404/base/code/OverloadTest$B."<init>":()V
        27: astore_2
        28: new           #20                 // class com/yrzx404/base/code/OverloadTest$C
        31: dup
        32: invokespecial #21                 // Method com/yrzx404/base/code/OverloadTest$C."<init>":()V
        35: astore_3
        36: aload_1
        37: aload_2
        38: invokevirtual #22                 // Method sayHello:(Lcom/yrzx404/base/code/OverloadTest$A;)V
        41: aload_1
        42: aload_3
        43: invokevirtual #22                 // Method sayHello:(Lcom/yrzx404/base/code/OverloadTest$A;)V
        46: aload_1
        47: aload_2
        48: checkcast     #18                 // class com/yrzx404/base/code/OverloadTest$B
        51: invokevirtual #23                 // Method sayHello:(Lcom/yrzx404/base/code/OverloadTest$B;)V
        54: return
      LineNumberTable:
        line 67: 0
        line 68: 8
        line 69: 14
        line 71: 20
        line 72: 28
        line 73: 36
        line 74: 41
        line 75: 46
        line 76: 54
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      55     0  args   [Ljava/lang/String;
            8      47     1 overloadTest   Lcom/yrzx404/base/code/OverloadTest;
           28      27     2     b   Lcom/yrzx404/base/code/OverloadTest$A;
           36      19     3     c   Lcom/yrzx404/base/code/OverloadTest$A;
复制代码

我们这段字节码指令可以得出,invokevirtual已经确定了调用方法,并且是根据方法参数的静态类型来决定的。

这里也解决了之前大家的疑问,overloadTest.sayHello((B)b),为什么结果为b?主要在这两句字节码指令起的作用。

48: checkcast     #18                 // class com/yrzx404/base/code/OverloadTest$B
51: invokevirtual #23                 // Method sayHello:(Lcom/yrzx404/base/code/OverloadTest$B;)
复制代码

即在强制类型转换时,会有指令checkcast的调用,而且invokevirtual指令的调用方法也会发生了变化。

关于重写优先级,这是詹爷他们定下的规定,没有什么好说的,记住就好了。

note:简单不先于复杂,而是在复杂之后。

转载于:https://juejin.im/post/5c7e586d6fb9a049fd1096fd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值