java虚拟机学习04 | JVM是如何执行方法调用的?(上)

https://time.geekbang.org/column/article/11539

避免重载可变长参数方法

错误示例:

void invoke(Object obj, Object... args) { ... }
void invoke(String s, Object obj, Object... args) { ... }

invoke(null, 1);    // 调用第二个 invoke 方法
invoke(null, 1, 2); // 调用第二个 invoke 方法
invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,
                               // 才能调用第一个 invoke 方法

重载与重写

        在Java代码中出现一个类中有多个名字相同,并且参数相同的方法,将无法通过编译. 在正常情况下,如果想在同一个类中定义多个相同名字的方法,那么这些方法的参数必须不同,这些方法的关系称为 重载.

       ps:小知识:这个限制可以通过字节码工具绕开。这是因为Java虚拟机与Java语言不同,并不限制名字与参数类型相同,返回类型不同的方法出现在同一个类中. 也就是说,在编译完成之后,我们可以再向 class 文件中添加方法名和参数类型相同,而返回类型不同的方法。当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在 Java 编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?当前版本的 Java 编译器会直接选取第一个方法名以及参数类型匹配的方法。并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。

重载方法在编译过程中就会完成识别,具体调用则是根据传入的参数的声明类型来选取,具体有以下三个阶段:

  1. 在不考虑基本类型的自动装箱与拆箱以及可变长参数的情况下选取重载方法
  2. 如果在1中没找到匹配的方法,那么允许自动装箱,不允许可变长参数的情况下选取
  3. 在2中没找到的情况下,允许自动装箱,允许可变长参数 ,在这种情况下选取

如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在根据形参类型的继承关系其中选择一个.

在开头的例子中,当传入 null 时,它既可以匹配第一个方法中声明为 Object 的形式参数,也可以匹配第二个方法中声明为 String 的形式参数。由于 String 是 Object 的子类,因此 Java 编译器会认为第二个方法更为贴切。

如果在子类中有一个方法与父类的的非私有方法同名,但参数不同,则构成了重载

如果在子类中有一个方法与父类的非私有方法同名,参数也相同则构成重写

众所周知,Java 是一门面向对象的编程语言,它的一个重要特性便是多态。而方法重写,正是多态最重要的一种体现方式:它允许子类在继承父类部分功能的同时,拥有自己独特的行为。

JVM的静态绑定与动态绑定

Java虚拟机识别方法的关键在于类名,方法名,方法描述符(method descriptor),其中方法描述符有方法的参数类型和返回类型所构成; 如果在同一个类中出现多个名字相同且方法描述符相同,Java虚拟机会再类的验证阶段报错.

Java虚拟机中的重写方法同样基于方法描述符,当子类定义了与父类非私有非静态方法同名的方法,那么只有两个方法的参数类型与返回类型相同时才会判定重写.

对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法 [2] 来实现 Java 中的重写语义. 对于这种情况,我的理解是,当两个方法参数相同,但返回类型不同时. 在这种情景下,当父类方法与子类方法的返回类型是父子或者兄弟关系时不会报错,其他情况编译器会报错,例子如下:


public class Persons {
    public String  eat(String s){
        return "吃饭";
    }
    public ArrayList haveLunch(String s){
        return new ArrayList();
    }
}


public class Student extends Persons {
    /**
     * 编译器报错
     */
    public int eat (String s){
        return 1;
    }

    /**
     * 编译器不报错
     */
    public LinkedList havelunch(String s){
        return new LinkedList();
    }
}

由于对重载方法的区分在编译阶段已经完成,我们可以认为 Java 虚拟机不存在重载这一概念,因此,在某些文章中,重载也被称为静态绑定(static binding),或者编译时多态(compile-time polymorphism);而重写则被称为动态绑定(dynamic binding).

这个说法在 Java 虚拟机语境下并非完全正确.这是因为某个类中的重载方法可能被它的子类所重写,因此 Java 编译器会将所有对非私有实例方法的调用编译为需要动态绑定的类型.

确切地说,Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况,

Java字节码中有以下5种调用相关的指令:

  1. invokestatic:用于调用静态方法.
  2. invokespecial:用于调用私有实例方法 构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法.
  3. invokevirtual:用于调用非私有实例方法.
  4. invokeinterface:用于调用接口方法.
  5. invokedynamic:用于调用动态方法.

调用指令的符号引用

在编译过程中,我们并不知到目标方法的内存地址. 因此,Java编译器会暂时用符号引用来表示该目标方法. 目标方法的内容包括目标方法所在的类或接口的名字以及目标方法的方法名和方法描述符。

符号引用存储在class文件的常量池中,根据目标方法是否是接口方法,这些引用分为接口符号引用和非接口符号引用.

在上一篇提过,在执行使用了符号引用的字节码前,Java虚拟机需要需要解析这些符号应用,并替换为实际引用

对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找.

  1. 在 C 中查找符合名字及描述符的方法.

  2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类.

  3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的.并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法.如果有多个符合条件的目标方法,则任意返回其中一个.

从这个解析算法可以看出,静态方法也可以通过子类来调用.此外,子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法.

对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找.

  1. 在 I 中查找符合名字及描述符的方法.
  2. 如果没有找到,在 Object 类中的公有实例方法中搜索.
  3. 如果没有找到,则在 I 的超接口中搜索.这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致.

经过上述的解析步骤之后,符号引用会被解析成实际引用.对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针.对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引

猜你喜欢

转载自blog.csdn.net/qq_34332035/article/details/87074741