重新认识重载和重写

196 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

本系列专栏:JVM专栏

前言

前面文章我们解析了Java字节码文件,其中介绍了各种常见的指令,其中说到调用方法时方法调用指令时,我们进行了跳过,所以本章开始,我们来仔细分析一波方法调用指令。

正文

理解方法调用指令很大一个好处就是理解Java程序和JVM中对方法的处理是有区别的,这样可以加深我们对Java程序的理解。

重载和重写

在Java程序中,如果一个类中出现多个名字相同,并且参数类型相同的方法,那么它是无法通过编译的,因为Java会认为这是同一个方法。在正常情况下,如果我们想在同一个类中定义名字相同的方法,那么它们的参数类型必须不同,而这些方法之间的关系,就叫做重载

同样,重载也可以不在同一个类中的方法,比如子类定义了与父类中非私有方法同名的方法,而这2个方法的参数类型不同,那么在子类中,这2个方法同样构成了重载。

那么如果子类定义了与父类中非私有方法同名的方法,而且这2个方法的参数类型相同,那这2个方法之间是什么关系呢

这里要分情况,如果这2个方法都是静态的,那么子类中的方法会隐藏父类中的方法;如果这2个方法都不是静态的,且都不是私有的,那么子类的方法就是重写了父类的方法

这就是Java程序中对于方法重载和重写的定义,也正是由于这些的存在,JVM才会有不同的调用指令,我们接着分析。

JVM的重写和重载

接着,我们看看JVM是怎么识别方法的。

JVM识别方法的关键在于类名、方法以及方法描述符,而方法描述符是由方法的参数类型以及返回类型所构成,所以在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么JVM会在类的验证阶段(链接的第一步骤)报错。

这里就会发现问题所在了,由于JVM和Java语言规则的不同,对于在Java语言中的重载在JVM中是不存在的,因为Java语言中重载的方法它的方法描述符必然不同;而对于重写,JVM中关于方法重写的判断同样是基于方法描述符,也就是说如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这2个方法的参数类型以及返回类型都不同时,JVM才判断为重写。

上面重写看起来没啥问题,但是别忘了方法描述符是包含返回类型的,也就是说下面情况:

//父类
public class People {

    public Number getAge(){
        return 30;
    }
}
//子类
public class Student extends People{

    @Override
    public Integer getAge(){
        return 18;
    }
}

在这里会发现子类虽然重写了父类的getAge方法,但是返回类型却不是Number类型而是其子类型Integer,这种写法在Java语言中是合法的,但是JVM却认为是方法描述符不一样,那如何解决呢

JVM通过桥接方法来实现Java种的重写语义。

JVM桥接方法

桥接方法,顾名思义就是搭建一个桥,来达成一致,我们话不多说,直接看Student类的字节码:

public class com.wayeal.arithmeticapp.testOverride.Student extends com.wayeal.arithmeticapp.testOverride.People
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #4                          // com/wayeal/arithmeticapp/testOverride/Student
  super_class: #5                         // com/wayeal/arithmeticapp/testOverride/People
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #5.#15         // com/wayeal/arithmeticapp/testOverride/People."<init>":()V
   #2 = Methodref          #16.#17        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Methodref          #4.#18         // com/wayeal/arithmeticapp/testOverride/Student.getAge:()Ljava/lang/Integer;
   #4 = Class              #19            // com/wayeal/arithmeticapp/testOverride/Student
   #5 = Class              #20            // com/wayeal/arithmeticapp/testOverride/People
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               getAge
  #11 = Utf8               ()Ljava/lang/Integer;
  #12 = Utf8               ()Ljava/lang/Number;
  #13 = Utf8               SourceFile
  #14 = Utf8               Student.java
  #15 = NameAndType        #6:#7          // "<init>":()V
  #16 = Class              #21            // java/lang/Integer
  #17 = NameAndType        #22:#23        // valueOf:(I)Ljava/lang/Integer;
  #18 = NameAndType        #10:#11        // getAge:()Ljava/lang/Integer;
  #19 = Utf8               com/wayeal/arithmeticapp/testOverride/Student
  #20 = Utf8               com/wayeal/arithmeticapp/testOverride/People
  #21 = Utf8               java/lang/Integer
  #22 = Utf8               valueOf
  #23 = Utf8               (I)Ljava/lang/Integer;
{
  public com.wayeal.arithmeticapp.testOverride.Student();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/wayeal/arithmeticapp/testOverride/People."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public java.lang.Integer getAge();
    descriptor: ()Ljava/lang/Integer;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        18
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: areturn
      LineNumberTable:
        line 7: 0

  public java.lang.Number getAge();
    descriptor: ()Ljava/lang/Number;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #3                  // Method getAge:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 3: 0
}

会发现该字节码文件中,getAge方法居然有2个,我们先看第一个返回类型是Integer的方法,这个方法是我们在Java程序中定义的,会发现它是正常的调用,没有什么问题。

再看第二个返回类型是Number的方法,这个方法的flag中有个是ACC_BRIDGE标志,说明这个方法是Java编译器生成的,而我们会发现它的指令操作: #0 从本地变量表中取出this指针 #1 调用常量池中#3的方法,我们来看看这个常量池中#方法是啥:

 #3 = Methodref          #4.#18         // com/wayeal/arithmeticapp/testOverride/Student.getAge:()Ljava/lang/Integer;

会发现这个方法就是调用我们自己定义的返回Integer的方法,所以对于Java是重写但是JVM不是重写的情况,Java编译器会生成一个JVM符合的重写方法,然后在这个生成的桥接方法中调用Java中定义的重写方法

总结

这篇文章很关键,必须要弄明白,因为它涉及了Java编译器生成关于方法调用指令的不同,我们下篇文章就来具体说一下到底有哪些方法调用指令以及区别。