从字节码看重载和重写

简介: 从字节码看重载和重写

从字节码看重载和重写

重载和重写

Java作为面向对象(OOP)的语言,其中之一的特性就是多态(polymorphic)。而对于多态在Java上主要体现就是“重载”和“重写”。

稍有Java常识的人便会知道“重载”和“重写”的区别。

重载,方法名相同,参数类型或者参数个数不同,返回可以修改也可以不变。
重写,方法名相同,参数和返回都必须相同,但方法中实现可以不同,也可以说方法签名不变,方法核心可以改变。

举个栗子

以下是重载和重写简单的栗子,可以生食。

代码1 重载

public class Forest {
    static class Animal {}
    static class Cat extends Animal{}
    static class Bird  extends Animal{}

    public void animalSound(Cat cat) {
        System.out.println("cat is sounding ");
    }

    public void animalSound(Bird bird) {
        System.out.println("bird is sounding ");
    }

    public static void main(String[] args) {
        Forest forest = new Forest();
        Cat cat = new Cat();
        Bird bird = new Bird();
        forest.animalSound(cat);
        forest.animalSound(bird);
    }
}
//结果
// cat is sounding 
// bird is sounding 

定义三个类分别是 Animal , Cat , Bird , 根据不同的入参重写 animalSound() 这个方法, 使之表现出不同版本。

代码2 重写

public class Forest {
    static abstract  class Animal {
        public abstract void animalSound();
    }
    static class Cat extends Animal {
        @Override
        public void animalSound() { System.out.println("cat is sounding "); }
    }

    static class Bird extends Animal {
        @Override
        public void animalSound() {System.out.println("bird is sounding ");}

    }
    public static void main(String[] args) {
        Animal cat = new Cat();
        cat.animalSound();

        Animal bird = new Bird();
        bird.animalSound();
    }
}
//结果
//cat is sounding 
//bird is sounding 

定义Animal父类,Cat和Bird都继承Animal类,分别在Dog类和Bird类中重写继承自Animal重animalSound方法,使之具备不同特性。

最简单的问题

从代码1和代码2中我们很容易就可以看出执行的结果。此时,我们不免往深入思考一点点,得出本文最重要也是最本质的问题。

1. 重载示例代码Main方法中,在forest对象调用animalSound方法时,它如何从多个重载方法中识别匹配上我们想要调用的目标方法的呢?

2. 重写示例代码Main方法中,在forest对象调用animalSound方法时,它又如何从对个重写方法中识别匹配上我们想要的调用的目标方法的呢?

3. 重载和重写两者对象调用方法的方式一样么?

从字节码上分析

对代码1使用javap -verbose -c Forest 生成以下字节码,

代码2-1 重载字节码

Warning: Binary file Forest contains com.helix.about.OverloadDemo.Forest
Classfile /Users/helix/IdeaProjects/java10/target/classes/com/helix/about/OverloadDemo/Forest.class
  Last modified Apr 15, 2018; size 1513 bytes
  MD5 checksum fdb78190178b0b189a9e20acad2fe128
  Compiled from "Forest.java"
public class com.helix.about.OverloadDemo.Forest
  minor version: 0
  major version: 54
  flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
   #1 = Methodref          #15.#44        // java/lang/Object."<init>":()V
   #2 = Fieldref           #45.#46        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #47            // animal is sounding
   #4 = Methodref          #48.#49        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #50            // cat is sounding
   #6 = String             #51            // bird is sounding
   #7 = Class              #52            // com/helix/about/OverloadDemo/Forest
   #8 = Methodref          #7.#44         // com/helix/about/OverloadDemo/Forest."<init>":()V
   #9 = Class              #53            // com/helix/about/OverloadDemo/Forest$Cat
  #10 = Methodref          #9.#44         // com/helix/about/OverloadDemo/Forest$Cat."<init>":()V
  #11 = Class              #54            // com/helix/about/OverloadDemo/Forest$Bird
  #12 = Methodref          #11.#44        // com/helix/about/OverloadDemo/Forest$Bird."<init>":()V
  #13 = Methodref          #7.#55         // com/helix/about/OverloadDemo/Forest.animalSound:(Lcom/helix/about/OverloadDemo/Forest$Cat;)V
  #14 = Methodref          #7.#56         // com/helix/about/OverloadDemo/Forest.animalSound:(Lcom/helix/about/OverloadDemo/Forest$Bird;)V
  #15 = Class              #57            // java/lang/Object
  #16 = Utf8               Bird
  #17 = Utf8               InnerClasses
  #18 = Utf8               Cat
  #19 = Class              #58            // com/helix/about/OverloadDemo/Forest$Animal
  #20 = Utf8               Animal
.....

{
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
        // 实例化
         0: new           #7                  // class com/helix/about/OverloadDemo/Forest
         3: dup
        // 调用Forest构造,(调用对象的构造函数,因为方法调用会弹出参数,因此需要上面的dup指令,保证在调用构造函数之后栈顶上还是对象的引用,很多种情况下dup指令都是为这个目的而存在的)
         4: invokespecial #8                  // Method "<init>":()V
        // 从操作数栈顶弹出对象引用,然后保存到索引为1的本地变量中
         7: astore_1

        // 实例化
         8: new           #9                  // class com/helix/about/OverloadDemo/Forest$Cat
        11: dup
        // 调用Cat构造   从常量池中获取方法符号引用(#10)
        12: invokespecial #10                 // Method com/helix/about/OverloadDemo/Forest$Cat."<init>":()V
        // 从操作数栈弹出cat对象引用,然后保存到索引为2的本地变量中
        15: astore_2

        // 实例化
        16: new           #11                 // class com/helix/about/OverloadDemo/Forest$Bird
        19: dup
        // 调用Bird构造    从常量池中获取方法符号引用(#12)
        20: invokespecial #12                 // Method com/helix/about/OverloadDemo/Forest$Bird."<init>":()V
        // 从操作数栈弹出bird对象引用,然后保存到索引为3的本地变量中
        23: astore_3

        // 将本地变量表中索引1位置的对象引用压如操作数栈
        24: aload_1
        // 将本地变量表中索引2位置的对象引用压如操作数栈
        25: aload_2
        // 从下标为13的常量池中获得类方法符号解析到cat对象的直接引用
        26: invokevirtual #13                 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Cat;)V

        // 将本地变量表中索引1位置的对象引用压如操作数栈
        29: aload_1
        // 将本地变量表中索引3位置的对象引用压如操作数栈
        30: aload_3
        // 获取操作数栈顶元素所指向的对象的实际类型,即从下标为14的常量池中获得类方法符号解析到bird对象的直接引用
        31: invokevirtual #14                 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Bird;)V
        34: return

      LineNumberTable:
        line 26: 0
        line 27: 8
        line 28: 16
        line 29: 24
        line 30: 29
        line 31: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      35     0  args   [Ljava/lang/String;
            8      27     1 forest   Lcom/helix/about/OverloadDemo/Forest;
           16      19     2   cat   Lcom/helix/about/OverloadDemo/Forest$Cat;
           24      11     3  bird   Lcom/helix/about/OverloadDemo/Forest$Bird;
}


而对应对象如何识别调用的重载方法问题,在《深入理解Java虚拟机》第8章,分派中这样写道:

代码中刻意地定义了两个静态类型相同、实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据。并且静态类型是编译期可知的,所以在编译阶段,Javac编译器就根据参数类型决定使用哪个重载版本。
......

所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

我们通过Cat cat = new Cat(); 实例化cat对象 , cat变量的静态类型和动态类型都是Cat(使用new Cat(),默认静态类型为Cat类型)。

我们通过Bird bird = new Bird(); 实例化bird对象 , bird变量的静态类型和动态类型都是Bird(使用new Bird(),默认静态类型为Bird类型)。

从字节码注释中可以看出。

第26行, 26: invokevirtual #13 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Cat;)V ,下标为13的常量池中的类方法的符号引用Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Cat;)V 包含方法的签名(方法名,方法参数等),将其解析成栈顶元素所指向的对象的实际类型。

第31行,31: invokevirtual #14 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Bird;)V , 下标为31的常量池中的类方法的符号引用(Lcom/helix/about/OverloadDemo/Forest$Bird;)V,将其解析成栈顶元素所指向的对象的实际类型。

当然,此时并不能说明重载时是通过参数的静态类型而不是实际类型作为判定依据。我们将调用重载的方法换成以下代码:

Forest forest = new Forest();
Animal cat = new Cat();
Animal bird = new Bird();
forest.animalSound(cat);
forest.animalSound(bird);

// 结果
//animal is sounding 
//animal is sounding 

此时,
我们通过Animal cat = new Cat(); 实例化cat对象 , 此时Animal为cat对象的静态类型,Cat为cat对象的实际类型(使用new Cat(),默认静态类型为Cat类型)。

我们通过Animal bird = new Bird(); 实例化bird对象 , 此时Animal为变量bird的静态类型,Bird为bird对象的实际类型(使用new Bird(),默认静态类型为Bird类型)。

为什么只调用了Animal类型的重载了呢?

对应字节码如下:
代码2-2 修改静态类型后调用重载产生的字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #7                  // class com/helix/about/OverloadDemo/Forest
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: new           #9                  // class com/helix/about/OverloadDemo/Forest$Cat
        11: dup
        12: invokespecial #10                 // Method com/helix/about/OverloadDemo/Forest$Cat."<init>":()V
        15: astore_2
        16: new           #11                 // class com/helix/about/OverloadDemo/Forest$Bird
        19: dup
        20: invokespecial #12                 // Method com/helix/about/OverloadDemo/Forest$Bird."<init>":()V
        23: astore_3
        24: aload_1
        25: aload_2
        26: invokevirtual #13                 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Animal;)V
        29: aload_1
        30: aload_3
        31: invokevirtual #13                 // Method animalSound:(Lcom/helix/about/OverloadDemo/Forest$Animal;)V
        34: return

此时我们发现,cat和bird在调用重载方法对应在字节码第26行和第31行,看出从常量池中取出的方法的符号引用都是(Lcom/helix/about/OverloadDemo/Forest$Animal;)V。

如上可以得出:
在编译期间,编译器在重载时通过参数的静态类型而不是实际类型作为判定依据。

我们继续通过javap -verbose -c Forest 命令生成的重写字节码。 如下所示,

代码2-3 重写字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1


         0: new           #2                  // class com/helix/about/OverrideDemo/Forest$Cat
         3: dup
         4: invokespecial #3                  // Method com/helix/about/OverrideDemo/Forest$Cat."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method com/helix/about/OverrideDemo/Forest$Animal.animalSound:()V


        12: new           #5                  // class com/helix/about/OverrideDemo/Forest$Bird
        15: dup
        16: invokespecial #6                  // Method com/helix/about/OverrideDemo/Forest$Bird."<init>":()V
        19: astore_2
        20: aload_2
        21: invokevirtual #4                  // Method com/helix/about/OverrideDemo/Forest$Animal.animalSound:()V
        24: return

第8行aload_1 和第20行 aload_2 正好分别对应着cat和bird在实例化后对象引用压到操作数栈顶,下一步就是执行invokevirtual指令,也就是执行Animal.animalSound方法,但最终执行的目标并不相同,那么这个时候cat和bird对象究竟是如何知道所执行的重写方法的呢?
我们不知道,编译器也不知道,所以编译期间无法判断重写方法的版本。

其实问题的关键点在于invokevirtual指令,对于invokevirtual指令的运行时解析会做以下事情:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型;
  2. 如果在该类型(子类)中找到与常量中的描述符和简单名称都相同的方法,进行权限访问,通过则返回这个方法的引用;如果权限访问不通过,则返回java.lang.IllegalAccessError错误。
  3. 如果没有找到签名相同的方法,对该类型的父类进行第二步的搜索和验证;
  4. 如果没有找到,则返回java.lang.AbstractMethodError.


由于invokevirtual指令在第一步就确定了在运行期间接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上这个就是Java语言中重写的本质。而这种在运行期间根据实际类型确定方法执行版本的分派被称为动态分派。

总结

  1. 重载,在编译期间,编译器通过参数的静态类型来确定重载目标方法的版本,通过静态类型来确定重载目标方法的分派被称作静态分派。
  2. 重写,在运行期间通过 invokevirtual 指令,将常量池中的方法的符号引用解析成栈顶元素不同直接引用,通过运行期实际类型的分派来确定执行目标方法的版本被称作动态分派。

参考

  1. [深入理解Java虚拟机-第8章]
  2. [JVM系列2—字节码指令]
目录
相关文章
|
25天前
|
弹性计算 人工智能 安全
2026年阿里云服务器优惠活动整理汇总及参数说明
2026年阿里云服务器最新优惠汇总:新用户轻量服务器低至38元/年,学生享300元无门槛券,企业u1实例199元起,含ECS、建站套餐及GPU服务选型指南。(239字)
171 0
|
7月前
|
人工智能 弹性计算 数据可视化
OpenClaw怎么部署?阿里云一键部署,只需两步搞定!
阿里云推出OpenClaw龙虾AI助理一键部署方案!无需代码、不配环境,两步搞定:①购买预装镜像的轻量服务器;②控制台粘贴百炼API Key并放通端口。新用户首月9.9元,享7000万Token免费额度,只需两步极速上线专属AI助理!
516 7
|
5月前
|
域名解析 弹性计算 运维
阿里云ECS云服务器从零搭建网站:新手零基础建站全流程教程
对于完全没有技术基础的新手而言,使用阿里云ECS云服务器搭建独立网站,是当前性价比最高、自由度最大、扩展性最强的建站方式。区别于模板建站的功能受限与虚拟主机的性能瓶颈,ECS云服务器支持自主配置运行环境、自由部署网站源码、独立管理所有数据,可完美适配个人博客、作品集展示、中小企业官网、小型资讯站点等各类基础Web项目。整套建站流程逻辑清晰、步骤标准化,无需深厚代码功底,按流程操作即可完成从服务器部署到网站公网访问的全部落地工作。
1098 0
|
8月前
|
人工智能 安全 API
阿里云百炼是什么?2026阿里云百炼AI大模型平台介绍及官网登录入口、功能使用全解析
阿里云百炼是一站式大模型服务平台(MaaS),聚合Qwen全系列等百余款大模型,提供模型调用、低/高代码Agent开发、工作流编排、安全合规部署等能力,支持“零门槛”落地应用。新用户可免费领取超7000万Tokens。(239字)
6686 3
|
数据可视化 安全 JavaScript
股票假图一键生成器, 股票持仓图生成器免费, 股票虚拟仿真交易软件
本系统包含三大核心模块:假图生成器、持仓可视化工具和虚拟交易引擎。采用Python+Flask+Vue技术栈实现前后端分离架构。
|
搜索推荐 应用服务中间件 PHP
301重定向完整指南: 原理、应用与实现方法
301重定向是一种永久性URL转发技术,用于将旧链接的权重传递给新URL,有助于SEO优化、提升用户体验和维护网站流量。本文介绍了301重定向的应用场景(如更换域名、HTTP转HTTPS等)、实现方法(Apache、Nginx、PHP等)及最佳实践,并解答了常见问题,帮助用户正确配置以确保网站无缝过渡。建议在操作前备份配置并使用工具验证效果。
1471 10
|
存储 缓存 Java
c++的哈希表、哈希桶的介绍与实现
这一篇文章大致实现详细介绍什么是哈希,然后再介绍什么是哈希表,怎么代码实现哈希表,然后再介绍哈希桶,怎么代码实现哈希桶,最后再介绍他俩有什么细节上的差别,与代码的一些细节优化。
496 0
技术笔记:MOS管常用效应
技术笔记:MOS管常用效应
1899 0