从JVM字节码执行看重载和重写

Java 重写(Override)与重载(Overload)

重写(Override)

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重载(Overload)

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

——————————————————————————————————————————————————————————————————————

分派

JVM字节码的执行涉及到方法的调用,其中分派调用过程将会解释Java多态特性,

1. 静态分派

首先是一段重载代码:

public class StaticDispatch {
    
    static abstract class Human{
        
    }
    
    static class Man extends Human{
        
    }
    
    static class Woman extends Human{
        
    }
    
    public void sayHello(Human guy){
        System.out.println("Hello, guy!");
    }
    
    public void sayHello(Man guy){
        System.out.println("Hello, gentleman!");
    }
    
    public void sayHello(Woman guy){
        System.out.println("Hello, lady!");
    }
    
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

与重载有关,代码的执行结果为:

但为什么会选择执行参数类型为Human的重载呢?

Human man = new Man();

在上面那段代码中,Human称为变量的静态类型(static type),Man称为变量的实际类型(actual type), 静态类型和实际类型在程序中都会发生变化,区别是静态类型的变化仅仅发生在使用期,变量本身的静态类型不会发生改变,并且静态类型是编译期可

知的。而实际类型变化的结果只有运行期才可以确定,编译期并不知道变量的实际类型是什么。

例如

//实际类型变化举例
Human man = new Man();
man = new Woman();

//静态类型变化举例
sr.sayHello((Man)man);
sr.sayHello((Woman)woman);

在之前的代码中,方法的接收者已经确定为sr的情况下,使用哪一个重载的版本取决于传入参数的数据类型。但编译器在重载的时候是以参数的静态类型而不是实际类型作为判断依据的。

因此结果就是执行sayHello(Human)作为调用的目标。

2. 动态分派

首先是一段重写代码:

public class Dynamicdispatch {

    
    static abstract class Human{
        protected abstract void sayHello();
    }
    static class Man extends Human{

        @Override
        protected void sayHello() {
            // TODO Auto-generated method stub
            System.out.println("man say hello!");
        }
        
    }
    static class Woman extends Human{

        @Override
        protected void sayHello() {
            // TODO Auto-generated method stub
            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();
    }
}

代码执行的结果:

现在的问题是虚拟机如何知道要调用什么方法?

可以javap -verbose来反编译输出的信息。

16和20句把创建好的两个对象的引用压到栈顶,这两个对象是将要执行sayHello()方法的所有者,为接收者; 17和21是方法调用指令,这两个指令都指向了#22号,就是Human.sayHello(),但是最终执行的目标方法却是不相同的。

这与invokevirtual指令的运行时解析有关:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型;
  2. 如果在该类型中找到与常量中的描述符和简单名称都相同的方法,进行权限访问,通过则返回这个方法的引用;
  3. 否则对该类型的父类进行第二步的搜索和验证;
  4. 如果始终找不到就跑出异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次指令都将符号引用解析到了不同的直接引用上去了。这就是重写的本质。

3. 单分派与多分派

静态多分派,动态单分派(到目前为止)

方法的接收者和方法的参数统称为方法的宗量。

静态分派过程中,选择目标方法的依据:静态类型的选择和方法参数的选择

动态分派过程中,选择目标方法的依据:静态类型,因为在动态执行时,编译期已经决定目标方法的签名(参数),所以不会关心传过来的参数。

原文地址:https://www.cnblogs.com/winterfells/p/7923241.html