美文网首页
从字节码分析java的方法重写和重载

从字节码分析java的方法重写和重载

作者: qlmmys | 来源:发表于2019-01-09 19:47 被阅读0次

不知道有没有小伙伴在面试时被问到过方法重写(Override)和重载(Overload)的区别?反正我是被问起过数次,大概情况是这样的:

面试官:说下Override和Overload的区别?
:(内心:额,送人头的,然后就)方法重写发生在子、父继承的关系下,子类可以修改父类的方法,以达到增强、扩展等~~#¥%@ bala bala
面试官:嗯,还有吗?
:(xx 一紧,还有啥?努力回想是不是忘说了啥)额,就这些吧
面试官:恩,今天面试就到这吧,你回去等消息吧
:#¥%
@~

方法重写和重载,相信只要是刚接触过java语言,对这两个概念就不会陌生,遇到相关的面试题估计也不少,今天我们就从面试题下手,然后再分析到其字节码层面,对这两个概念做一个介绍。
首先贴上面试题

// 父类
public class Parent {
    int age = 40;
    public void walk() {
        System.out.println("parent walk");
    }
}

// 子类
public class Child extends Parent {
    int age = 15;
    public void walk() {
        System.out.println("child walk");
    }
}

// 测试重载
public class TestOverload {

    public void method(Parent parent) {
        System.out.println("parent");
    }

    public void method(Child child) {
        System.out.println("child");
    }

    public static void main(String[] args) {
        TestOverload testOverload = new TestOverload();

        Parent parent = new Child();
        testOverload.method(parent);
        Child child = new Child();
        testOverload.method(child);
    }
}

相信机智如你,早就知道了答案,结果:

parent
child

Process finished with exit code 0

不多解释概念,先javap看下字节码(为节省篇幅,只贴出部分常量池和main字节码)

Constant pool:
   #1 = Methodref          #12.#34        // java/lang/Object."<init>":()V
   #2 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #22            // parent
   #4 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #25            // child
   #6 = Class              #39            // com/jvm/learnjvm/test/TestOverload
   #7 = Methodref          #6.#34         // com/jvm/learnjvm/test/TestOverload."<init>":()V
   #8 = Class              #40            // com/jvm/learnjvm/test/Child
   #9 = Methodref          #8.#34         // com/jvm/learnjvm/test/Child."<init>":()V
  #10 = Methodref          #6.#41         // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V
  #11 = Methodref          #6.#42         // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V
  #12 = Class              #43            // java/lang/Object
  #13 = Utf8               <init>


 public static void main(java.lang.String[]);  // main 方法
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #6                  // class com/jvm/learnjvm/test/TestOverload
         3: dup
         4: invokespecial #7                  // Method "<init>":()V
         7: astore_1
         8: new           #8                  // class com/jvm/learnjvm/test/Child
        11: dup
        12: invokespecial #9                  // Method com/jvm/learnjvm/test/Child."<init>":()V
        15: astore_2
        16: aload_1
        17: aload_2
        18: invokevirtual #10                 // Method method:(Lcom/jvm/learnjvm/test/Parent;)V
        21: new           #8                  // class com/jvm/learnjvm/test/Child
        24: dup
        25: invokespecial #9                  // Method com/jvm/learnjvm/test/Child."<init>":()V
        28: astore_3
        29: aload_1
        30: aload_3
        31: invokevirtual #11                 // Method method:(Lcom/jvm/learnjvm/test/Child;)V


我们重点看一下main方法的 18: invokevirtual #1031: invokevirtual #11,执行的方法,对照常量池的#10 = Methodref #6.#41 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V,
#11 = Methodref #6.#42 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V,通过方法入参,可以看到很清楚的看到调用的方法情况,因为此时并没有运行,不知道将来传入的参数真实实例是什么,编译器只是根据声明的参数的类型和数量等匹配到合适的重载方法,这种方式,被称为“静态分派”。
同样在编译期确定的还有调用成员变量的经典面试题,有兴趣的可以自己看下字节码分析下

public class TestOverload {

    public void method(Parent parent) {
        System.out.println("parent");
    }

    public void method(Child child) {
        System.out.println("child");
    }

    public static void main(String[] args) {
        TestOverload testOverload = new TestOverload();

        Parent parent = new Child();
        System.out.println(parent.age);
        
        Child child = new Child();
        System.out.println(child.age);
    }
}

结果

40
15

Process finished with exit code 0

分析完方法重载,现在分析下方法重写,先来一段大家都熟到不能再熟的代码(Parent和Child类依旧使用之前的)

public class TestOverride {

    public static void main(String[] args) {
        Parent parent = new Child();
        parent.walk();
    }
}

大家用脚指头想都知道的结果

child walk

Process finished with exit code 0

面对感觉理所应当的结果,我们还是先看看字节码吧

Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/jvm/learnjvm/test/Child
   #3 = Methodref          #2.#22         // com/jvm/learnjvm/test/Child."<init>":()V
   #4 = Methodref          #24.#25        // com/jvm/learnjvm/test/Parent.walk:()V
   #5 = Class              #26            // com/jvm/learnjvm/test/TestOverride
   #6 = Class              #27            // java/lang/Object

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/jvm/learnjvm/test/Child
         3: dup
         4: invokespecial #3                  // Method com/jvm/learnjvm/test/Child."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method com/jvm/learnjvm/test/Parent.walk:()V
        12: return

执行方法调用的是9: invokevirtual #4,指向的是常量池#4,然后我们丝毫不慌的去常量看看,
#4 = Methodref #24.#25 // com/jvm/learnjvm/test/Parent.walk:()V
what???调用的是Parent的walk() ?气氛突然有些尴尬......
其实这个时候,就要说下面向对象的三大特性:封装、继承、多态中的多态的实现原理了。多态是什么不用我说大家也都知道,就不解释概念了,从字节码角度说一下,还是回到main方法的字节码再看一下,前面主要是创建对象,执行构造方法,并把当前创建的对象实例压到操作数栈,重点看下9,执行invokevirtual指令,(jdk提供了5条方法调用的指令,在最下面有列出),invokevirtual指令是找到当前操作数栈栈顶元素指向的对象的实际类型,也就是new出来的Child,然后执行该指令对应的常量池中的方法#4,而这时候是运行期,jvm会根据方法的名称和描述来定位方法,调用的是Child实例的walk,这种动态调用方法的方式,也被称为动态分派。

方法调用指令
invokestatic          调用静态方法
invokevirtual        调用实例方法
invokespecial        调用私有方法、实例构造方法、super()
invokeinterface     调用引用类型为interface的实例方法
invokedynamic        JDK 7引入的,主要是为了支持动态语言的方法调用,如Lambda

相关文章

网友评论

      本文标题:从字节码分析java的方法重写和重载

      本文链接:https://www.haomeiwen.com/subject/qcrgrqtx.html