资讯专栏INFORMATION COLUMN

JVM执行方法调用(一)- 重载与重写

韩冰 / 512人阅读

摘要:重写语言中的定义子类方法有一个方法与父类方法的名字相同且参数类型相同。父类方法的返回值可以替换掉子类方法的返回值。思维导图参考文档极客时间深入拆解虚拟机是如何执行方法调用的上广告

原文

回顾Java语言中的重载与重写,并且看看JVM是怎么处理它们的。

重载Overload

定义:

在同一个类中有多个方法,它们的名字相同,但是参数类型不同。

或者,父子类中,子类有一个方法与父类非私有方法名字相同,但是参数类型不同。那么子类的这个方法对父类方法构成重载。

JVM是怎么处理重载的?其实是编译阶段编译器就已经决定好调用哪一个重载方法。看下面代码:

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

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

上面的注释告诉了我们结果,那么怎么才能证明上面的注释呢?我们利用javap观察字节码可以知道。

$ javac Overload.java
$ javap -c Overload.java

Compiled from "Overload.java"
class Overload {
  ...
  void invoke(java.lang.Object, java.lang.Object...);
    Code:
       0: return
  void invoke(java.lang.String, java.lang.Object, java.lang.Object...);
    Code:
       0: return
  void test1();
    Code:
      ...
      10: invokevirtual #4  // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V
      13: return
  void test2();
    Code:
      ...
      17: invokevirtual #4  // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V
      20: return
  void test3();
    Code:
      ...
      13: invokevirtual #5  // Method invoke:(Ljava/lang/Object;[Ljava/lang/Object;)V
      16: return
}

这里面有很多JVM指令,你暂且不用关心,我们看test1test2test3方法调用的是哪个方法:

  void test1();
    Code:
      ...
      10: invokevirtual #4  // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V
      13: return

invoke是方法名,(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V则是方法描述符。这里翻译过来就是void invoke(String, Object, Object[]),Java的可变长参数实际上就是数组,所以等同于void invoke(String, Object, Object...)。同理,test2调用的是void invoke(String, Object, Object...)test3调用的是void invoke(Object, Object...)。关于方法描述符的详参JVM Spec - 4.3.2. Field Descriptors和JVM Spec - 4.3.3. Method Descriptors。

所以重载方法的选择是在编译过程中就已经决定的,下面是编译器的匹配步骤:

不允许自动拆装箱,不允许可变长参数,尝试匹配

如果没有匹配到,则允许自动拆装箱,不允许可变长参数,尝试匹配

如果没有匹配到,则允许自动拆装箱,允许可变长参数,尝试匹配

注意:编译器是根据实参类型来匹配,实参类型和实际类型不是一个概念

如果在一个步骤里匹配到了多个方法,则根据形参类型来找最贴切的。在上面的例子中第一个invoke的参数是Object, Object...,第二个invoke的参数是String, Object, Object...,两个方法的第一个参数StringObject的子类,因此更为贴切,所以invoke(null, 1, 2)会匹配到第二个invoke方法上。

重写Override

Java语言中的定义:

子类方法有一个方法与父类方法的名字相同且参数类型相同。

父类方法的返回值可以替换掉子类方法的返回值。也就是说父类方法的返回值类型:

要么和子类方法返回值类型一样。

要么是子类方法返回值类型的父类。

两者都是非私有、非静态方法。

(更多详细信息可参考Java Language Spec - 8.4.8. Inheritance, Overriding, and Hiding,这里除了有更精确详细的重写的定义,同时包含了范型方法的重写定义。)

但是JVM中对于重写的定义则有点不同:

子类方法的名字与方法描述符与父类方法相同。

两者都是非私有、非静态方法。

(更多详细信息可参考JVM Spec - 5.4.5. Overriding)

注意上面提到的方法描述符,前面讲过方法描述符包含了参数类型及返回值,JVM要求这两个必须完全相同才可以,但是Java语言说的是参数类型相同但是返回值类型可以不同。Java编译器通过创建Bridge Method来解决这个问题,看下面代码:

class A {
  Object f() {
    return null;
  }
}
class C extends A {
  Integer f() {
    return null;
  }
}

然后用javap查看编译结果:

$ javac Override.java
$ javap -v C.class
class C extends A
...
{
  java.lang.Integer f();
    descriptor: ()Ljava/lang/Integer;
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
  ...
  java.lang.Object f();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method f:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 7: 0
}

可以看到编译器替我们创建了一个Object f()的Bridge Method,它调用的是Integer f(),这样就构成了JVM所定义的重写。

思维导图

参考文档

极客时间 - 深入拆解 Java 虚拟机 - 04 | JVM是如何执行方法调用的?(上)

JVM Spec - 4.3.2. Field Descriptors

JVM Spec - 4.3.3. Method Descriptors

Java Language Spec - 8.4.8. Inheritance, Overriding, and Hiding

Java Language Spec - 8.4.9. Overloading

JVM Spec - 5.4.5. Overriding

Effects of Type Erasure and Bridge Methods

广告

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73367.html

相关文章

  • 【金三银四】面试题之java基础

    摘要:中,任何未处理的受检查异常强制在子句中声明。运行时多态是面向对象最精髓的东西,要实现运行时多态需要方法重写子类继承父类并重写父类中已 1、简述Java程序编译和运行的过程:答:① Java编译程序将Java源程序翻译为JVM可执行代码--字节码,创建完源文件之后,程序会先被编译成 .class 文件。② 在编译好的java程序得到.class文件后,使用命令java 运行这个 .c...

    Yangyang 评论0 收藏0
  • 【金三银四】面试题之java基础

    摘要:中,任何未处理的受检查异常强制在子句中声明。运行时多态是面向对象最精髓的东西,要实现运行时多态需要方法重写子类继承父类并重写父类中已 1、简述Java程序编译和运行的过程:答:① Java编译程序将Java源程序翻译为JVM可执行代码--字节码,创建完源文件之后,程序会先被编译成 .class 文件。② 在编译好的java程序得到.class文件后,使用命令java 运行这个 .c...

    Barrior 评论0 收藏0
  • jvm角度看懂类初始化、方法重载重写

    摘要:对应的代码接下来的句是关键部分,两句分分别把刚刚创建的两个对象的引用压到栈顶。所以虽然指令的调用是相同的,但行调用方法时,此时栈顶存放的对象引用是,行则是。这,就是语言中方法重写的本质。 类初始化 在讲类的初始化之前,我们先来大概了解一下类的声明周期。如下图 类的声明周期可以分为7个阶段,但今天我们只讲初始化阶段。我们我觉得出来使用和卸载阶段外,初始化阶段是最贴近我们平时学的,也是笔试...

    tinyq 评论0 收藏0
  • 谈谈Java的面向对象

    摘要:也就是说,一个实例变量,在的对象初始化过程中,最多可以被初始化次。当所有必要的类都已经装载结束,开始执行方法体,并用创建对象。对子类成员数据按照它们声明的顺序初始化,执行子类构造函数的其余部分。 类的拷贝和构造 C++是默认具有拷贝语义的,对于没有拷贝运算符和拷贝构造函数的类,可以直接进行二进制拷贝,但是Java并不天生支持深拷贝,它的拷贝只是拷贝在堆上的地址,不同的变量引用的是堆上的...

    ormsf 评论0 收藏0
  • Java 面试准备

    摘要:网站的面试专题学习笔记非可变性和对象引用输出为,前后皆有空格。假定栈空间足够的话,尽管递归调用比较难以调试,在语言中实现递归调用也是完全可行的。栈遵守规则,因此递归调用方法能够记住调用者并且知道此轮执行结束之返回至当初的被调用位置。 ImportNew 网站的Java面试专题学习笔记 1. 非可变性和对象引用 String s = Hello ; s += World ; s.tr...

    chanjarster 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<