查看原文
其他

面试题:方法重载的底层原理?

忆蓉之心 Java面试那些事儿 2019-12-19

前语:微信改版后,大量读者还没养成点赞的习惯,如写得好,望大家阅读后在右下边“好看”处点个赞,以示鼓励!长期坚持原创真的很不容易,多次想放弃,坚持是一种信仰,专注是一种态度。


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

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 charhello serializableaab

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


  1. javac编译器在编译阶段会根据参数的静态类型来决定选择哪个重载版本。

  2. 重载优先级,先匹配参数个数;再匹配参数类型的直接所属类;如果没有找到直接的所属类,会向上转型(包装类 -> 接口 -> 父类 );如果向上转型无果,再查找可变参数列表;以上都找不到,则报找不到方法错误。


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

A b = new B();

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


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


方法调用


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


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


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


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


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

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

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

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

  5. 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$B51: invokevirtual #23 // Method sayHello:(Lcom/yrzx404/base/code/OverloadTest$B;)V

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


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


最后


相信看到这里,大家应该明白了方法重载。说实话,我个人觉得这道面试题除了为难面试者,没有什么鸟用!因为实际工作中,没有谁闲的蛋疼去写这种“炫技”的代码。


那么关于方法重写(override),我这里给一个结论吧,重写方法的调用主要看实际类型,在运行时决定调用版本。实际类型如果实现了该方法则直接调用该方法,如果没有实现,则在继承关系中从低到高搜索有无实现。


那么,希望你也能就这我的思路去分析一下方法重写。


熬夜写文章,各位读者记得在右下角点下【好看】以示鼓励!


---END---


Modified on

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存