深入理解“重载”与“重写”——分派

java语言虽不是动态类型语言,但它具有动态特性,方法重写是java语言动态特性的一个重要因素。本文将从虚拟机层次去理解方法重载和方法重写的实现原理。

一、方法调用

在java语言中,方法调用是程序运行时最普遍、最频繁的操作。方法调用不等同于方法执行。方法调用阶段的唯一任务是确认本调用方法的版本,也就是确定被调用方法的直接引用。
方法调用的过程以编译的Class文件为起点,发生在类加载过程的解析阶段,部分方法调用在程序运行阶段才能完成。
根据方法调用过程的时间不同,可分为解析分派两类。
关于方法调用的字节码指令有5个:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,再执行该方法。

1、解析

所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转化为直接引用。这类方法在程序运行前就已确定了一个调用版本,并且这个版本在程序运行期间不可变,这类方法调用过程成为“解析”。
只能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这一条件的有静态方法、私有方法、实例构造器、父类方法四种。这些方法成为“非虚方法”,其他方法都成为“虚方法”(final方法除外)。
解析调用是一个完全静态的过程,在编译期间就完全确定,在类加载阶段会把所涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期间去完成。而分派则可能是静态的也可能是动态的。

2、分派

分派也是方法调用的一种重要方法,它分为静态分派和动态分派,分别对应多态性的两种基本体现:重载和重写。

1)静态分派

要理解静态分派,先来看一道关于重载的题目:

public class TestStaticDispatch {
    static abstract class Pet{}
    static class Dog extends Pet{}
    static class Cat extends Pet{}
    
    public void feed(Pet pet){
        System.out.println(" feed pet");
    }
    public void feed(Dog dog){
        System.out.println(" feed dog");
    }
    public void feed(Cat cat){
        System.out.println(" feed cat");
    }

    public static void main(String[] args) {
        TestStaticDispatch tsd = new TestStaticDispatch();
        Pet dog = new Dog();
        Pet cat = new Cat();
        tsd.feed(dog);
        tsd.feed(cat);
    }
}

运行结果:
feed pet
feed pet
在以下这行代码中:Pet dog = new Dog(); “Pet”称为变量的“静态类型”,或者叫“外观类型”,“Dog”称为变量的实际类型,当发生类型变化时,静态类型的变化在编译期可知,而实际类型需要到运行期间才可确定。编译期在编译期间并不知道变量的实际类型是什么,所以java提供强制类型转化的语法。
在代码中,main()方法两次调用feed()方法,方法接受者已确定为“tsd”,在编译阶段,编译期选择参数的静态类型而不是实际类型作为判断依据,来决定使用方法的哪个重载版本。这种依赖静态类型定位方法执行版本的分派动作称为“静态分派”。静态分派的典型应用是方法重载。
重载方法优先级
在java中,有些字面量不需要进行显示的类型定义。如:

import java.io.Serializable;

public class TestOverloadPriority {
    public static void sayHello(Object arg){
        System.out.println("hello Object");
    }
    public static void sayHello(int arg){
        System.out.println("hello int");
    }
    public static void sayHello(long arg){
        System.out.println("hello long");
    }
    public static void sayHello(char arg){
        System.out.println("hello char");
    }
    public static void sayHello(Character arg){
        System.out.println("hello Character");
    }
    public static void sayHello(Serializable arg){
        System.out.println("hello Serializable");
    }
    public static void sayHello(char ... arg){
        System.out.println("hello char ...");
    }

    public static void main(String[] args) {
        TestOverloadPriority.sayHello('a');
    }
}

运行结果:
hello char
参数’a’默认为char类型,所以调用重载方法的sayHello(char arg)方法。如果去掉这个重载方法,
运行结果:
hello int
这时发生了一次自动类型转换,'a’转为十进制数值97,参数类型为int,再去掉这个重载方法,
运行结果:
hello long
int类型可自动类型转换为long类型,再去掉这个重载方法,
运行结果:
hello Character
自动装箱,char类型转化为char的包装类型Character,再去掉这个重载方法,
运行结果:
hello Serializable
java.lang.Character实现了java.lang.Serializable接口,自动转化为Serializable接口类型,再去掉这个重载方法,
运行结果:
hello Object
子类转型为父类,再去掉这个重载方法,
运行结果:
hello char …
可变长参数,优先级最低。
由上面的测试,重载的优先级不言而喻。
注意:静态分派和解析并不是二选一的排他关系。

2、动态分派

动态分派和多态的另一个重要体现:重写,有密切的关联。先看题目:

public class TestDynamicDispatch {
    static abstract class Pet{
        protected abstract void feed();
    }
    static class Dog extends Pet{
        @Override
        protected void feed() {
            System.out.println("feed dog");
        }
    }
    static class Cat extends Pet{
        @Override
        protected void feed() {
            System.out.println("feed cat");
        }
    }
    
    public static void main(String[] args) {
        Pet dog = new Dog();
        Pet cat = new Cat();
        dog.feed();
        cat.feed();
        dog = new Cat();
        dog.feed();
    }
}

运行结果:
feed dog
feed cat
feed cat
从结果可以看出,方法重写已经不会再根据变量的静态类型来决定调用方法的版本了,而应该是根据变量的实际类型。这种情况下会用到字节码指令invokevirtual,invokevirtual指令的运行解析过程大致分为以下几个步骤:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索及验证。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

invokevirtual指令执行的第一步就是在运行期间确定方法接收者的实际类型,也就是把常量池中的符号引用解析道不同的直接引用上,这个过程也就是方法中邪的本质。也就是动态分派的过程。
动态分派的实现与优化
由于动态分派是非常频繁的动作,虚拟机的实际实现会基于性能的考虑,做一些优化手段。最常用的优化手段就是在类的方法区简历一个“虚方法表”,使用虚方法表索引来替代元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果子类没有重写父类方法,那么虚方法表里的方法入口都指向父类的方法入口地址。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化。
不同虚拟机的优化手段可能不同,还有两种比较激进的优化手段:"内联缓存"和“守护内联”。

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/88380980
今日推荐