【JVM面试系列3】方法重载和方法重写原理分析

结论

1.重载(overload)方法
对重载方法的调用主要看静态类型,静态类型是什么类型,就调用什么类型的参数方法。
2.重写(override)方法
对重写方法的调用主要看实际类型。实际类型如果实现了该方法则直接调用该方法,如果没有实现,则在继承关系中从低到高搜索有无实现。
3.java文件的编译过程中不存在传统编译的连接过程,一切方法调用在class文件中存放的只是符号引用,而不是方法在实际运行时内存布局中的入口地址。

基本概念

1.静态类型与实际类型,方法接收者

Human man = new Man();
man.foo();

上面这条语句中,man的静态类型为Human,实际类型为Man。所谓方法接收者,就是指将要执行foo()方法的所有者(在多态中,有可能是父类Human的对象,也可能是子类Man的对象)。
2.字节码的方法调用指令
(1)invokestatic:调用静态方法
(2)invokespecial:调用实例构造器方法,私有方法和父类方法。
(3)invokevirtual:调用所有的虚方法。
(4)invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
(5)invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
前2条指令(invokestatic, invokespecial),在类加载时就能把符号引用解析为直接引用,符合这个条件的有静态方法、实例构造器方法、私有方法、父类方法这4类,这4类方法叫非虚方法。
非虚方法除了上面静态方法、实例构造器方法、私有方法、父类方法这4种方法之外,还包括final方法。虽然final方法使用invokevirtual指令来调用,但是final方法无法被覆盖,没有其他版本,无需对方法接收者进行多态选择,或者说多态选择的结果是唯一的。

重载overload

上面说的静态类型和动态类型都是可以变化的。静态类型发生变化(强制类型转换)时,对于编译器是可知的,即编译器知道对象的最终静态类型。而实际类型变化(对象指向了其他对象)时,编译器是不可知的,只有在运行时才可知。

//静态类型变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);
//实际类型变化
Human man = new Man();
man = new Woman();

重载只涉及静态类型的选择。
测试代码如下:

public class StaticTest {
    static class Human {}
    static class Man extends Human {}
    static class Woman extends Human {}

    public void sayHello(Human human) {
        System.out.println("Hello guy!");
    }

    public void sayHello(Man man) {
        System.out.println("Hello man!");
    }

    public void sayHello(Woman woman) {
        System.out.println("Hello woman!");
    }

    public static void main(String[] args) {
        StaticTest staticDispatcher = new StaticTest();
        Human man = new Man();
        Human woman = new Woman();
        staticDispatcher.sayHello(man);
        staticDispatcher.sayHello(woman);
        staticDispatcher.sayHello((Man)man);
        staticDispatcher.sayHello((Woman)man);
    }
}

在这里插入图片描述
由此可见,当静态类型发生变化时,会调用相应类型的方法。但是,当将Man强制类型转换成Woman时,没有编译错误,却有运行时异常。“classCastException”类映射异常。
看看字节码指令:
javac StaticTest.Java
javap -verbose -c StaticTest.class

 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           #7                  // class StaticTest
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: new           #9                  // class StaticTest$Man
        11: dup
        12: invokespecial #10                 // Method StaticTest$Man."<init>":()V
        15: astore_2
        16: new           #11                 // class StaticTest$Woman
        19: dup
        20: invokespecial #12                 // Method StaticTest$Woman."<init>":()V
        23: astore_3
        24: aload_1
        25: aload_2
        26: invokevirtual #13                 // Method sayHello:(LStaticTest$Human;)V
        29: aload_1
        30: aload_3
        31: invokevirtual #13                 // Method sayHello:(LStaticTest$Human;)V
        34: aload_1
        35: aload_2
        36: checkcast     #9                  // class StaticTest$Man
        39: invokevirtual #14                 // Method sayHello:(LStaticTest$Man;)V
        42: aload_1
        43: aload_2
        44: checkcast     #11                 // class StaticTest$Woman
        47: invokevirtual #15                 // Method sayHello:(LStaticTest$Woman;)V

看到,在强制类型转换时,会有指令checkCast的调用,而且invokevirtual指令的调用方法也发生了变化39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V 。
虚拟机(准确说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。
对于字面量类型,编译器会自动进行类型转换。转换的顺序为:
char-int-long-float-double-Character-Serializable-Object
转换成Character是因为发生了自动装箱,转换成Serializable是因为Character实现了Serializable接口。
重写override

测试代码如下:
public class DynamicDispatcher {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Man say hello");
        }
    }

    static class Woman extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

执行结果
在这里插入图片描述
看下字节码指令:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class DynamicDispatcher$Man
         3: dup
         4: invokespecial #3                  // Method DynamicDispatcher$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class DynamicDispatcher$Woman
        11: dup
        12: invokespecial #5                  // Method DynamicDispatcher$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method DynamicDispatcher$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method DynamicDispatcher$Human.sayHello:()V
        24: new           #4                  // class DynamicDispatcher$Woman
        27: dup
        28: invokespecial #5                  // Method DynamicDispatcher$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method DynamicDispatcher$Human.sayHello:()V
        36: return
      LineNumberTable:
        line 25: 0
        line 26: 8
        line 27: 16
        line 28: 20
        line 29: 24
        line 30: 32
        line 31: 36
}

从字节码中可以看到,他们调用的都是相同的方法invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V ,但是执行的结果却显示调用了不同的方法。因为,在编译阶段,编译器只知道对象的静态类型,而不知道实际类型,所以在class文件中只能确定要调用父类的方法。但是在执行时却会判断对象的实际类型。如果实际类型实现这个方法,则直接调用,如果没有实现,则按照继承关系从下往上一次检索,只要检索到就调用,如果始终没有检索到,则抛异常(难道能编译通过)。
(1)测试代码如下:

public class Test {

static class Human {
    protected void sayHello() {
        System.out.println("Human say hello");
    }
    protected void sayHehe() {
        System.out.println("Human say hehe");
    }
}

static class Man extends Human {

    @Override
    protected void sayHello() {
        System.out.println("Man say hello");
    }

// protected void sayHehe() { //
System.out.println(“Man say hehe”); // }
}

static class Woman extends Human {

    @Override
    protected void sayHello() {
        System.out.println("Woman say hello");
    }

// protected void sayHehe() { //
System.out.println(“Woman say hehe”); // }
}

public static void main(String[] args) {
    Human man = new Man();
    man.sayHehe();
}

}

测试结果如下:
在这里插入图片描述

这里写图片描述
字节码指令:

public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=2, Args_size=1
   0:   new     #2; //class Test$Man
   3:   dup
   4:   invokespecial   #3; //Method Test$Man."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   invokevirtual   #4; //Method Test$Human.sayHehe:()V
   12:  return

字节码指令与上面代码的字节码指令没有本质区别。

(2)测试代码如下:

public class Test {

    static class Human {
        protected void sayHello() {
        }
    }

    static class Man extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Man say hello");
        }

        protected void sayHehe() {
            System.out.println("Man say hehe");
        }
    }

    static class Woman extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Woman say hello");
        }

        protected void sayHehe() {
            System.out.println("Woman say hehe");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        man.sayHehe();
    }
}

编译时报错:
在这里插入图片描述
这个例子说明了:Java编译器是基于静态类型进行检查的。
修改上面错误代码,如下所示:

public class Test {

    static class Human {
        protected void sayHello() {
            System.out.println("Human say hello");
        }
//        protected void sayHehe() {
//            System.out.println("Human say hehe");
//        }
    }
    static class Man extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Man say hello");
        }

        protected void sayHehe() {
            System.out.println("Man say hehe");
        }
    }

    static class Woman extends Human {

        @Override
        protected void sayHello() {
            System.out.println("Woman say hello");
        }

        protected void sayHehe() {
            System.out.println("Woman say hehe");
        }
    }

    public static void main(String[] args) {
        Man man = new Man();
        man.sayHehe();
    }

}

执行结果:
在这里插入图片描述
注意在Main方法中,改成了Man man = new Man();
执行结果如下所示:
这里写图片描述
字节码指令如下所示:

public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=2, Args_size=1
   0:   new     #2; //class Test$Man
   3:   dup
   4:   invokespecial   #3; //Method Test$Man."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   invokevirtual   #4; //Method Test$Man.sayHehe:()V
   12:  return

注意上面的字节码指令invokevirtual #4; //Method Test$Man.sayHehe:()V 。

参考资料

  • 周志明 《深入理解JAVA虚拟机》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值