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

不知道有没有小伙伴在面试时被问到过方法重写(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345