从字节码看重载和重写
从字节码看重载和重写
重载和重写
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指令的运行时解析会做以下事情:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型;
- 如果在该类型(子类)中找到与常量中的描述符和简单名称都相同的方法,进行权限访问,通过则返回这个方法的引用;如果权限访问不通过,则返回java.lang.IllegalAccessError错误。
- 如果没有找到签名相同的方法,对该类型的父类进行第二步的搜索和验证;
- 如果没有找到,则返回java.lang.AbstractMethodError.
由于invokevirtual指令在第一步就确定了在运行期间接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上这个就是Java语言中重写的本质。而这种在运行期间根据实际类型确定方法执行版本的分派被称为动态分派。
总结
- 重载,在编译期间,编译器通过参数的静态类型来确定重载目标方法的版本,通过静态类型来确定重载目标方法的分派被称作静态分派。
- 重写,在运行期间通过
invokevirtual
指令,将常量池中的方法的符号引用解析成栈顶元素不同直接引用,通过运行期实际类型的分派来确定执行目标方法的版本被称作动态分派。
参考
- [深入理解Java虚拟机-第8章]
- [JVM系列2—字节码指令]
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Netty之前篇——NIO基础
以下内容由动脑five老师的笔记整理而来。 一、几个概念 1、阻塞与非阻塞 阻塞与非阻塞是描述进程在访问某个资源时,数据是否准备就绪的的一种处理方式。当数据没有准备就绪时: 阻塞:线程持续等待资源中数据准备完成,直到返回响应结果。非阻塞:线程直接返回结果,不会持续等待资源准备数据结束后才响应结果。 2、同步与异步 同步与异步是指访问数据的机制,同步一般指主动请求并等待IO操作完成的方式。 异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知。 举一个很形象的例子就很容易明白了:老王烧开水:1、普通水壶煮水,站在旁边,主动的看水开了没有?同步的阻塞2、普通水壶煮水,去干点别的事,每过一段时间去看看水开了没有,水没开就走人。 同步非阻塞3、响水壶煮水,站在旁边,不会每过一段时间主动看水开了没有。如果水开了,水壶自动通知他。 异步阻塞4、响水壶煮水,去干点别的事,如果水开了,水壶自动通知他。异步非阻塞 二、NIO基础 1、传统BIO模型 传统BIO是一种同步的阻塞IO,IO在进行读写时,该线程将被阻塞,线程无法进行其它操作。 IO流在读取时,会阻塞。直到发生以下情况:1、...
- 下一篇
【Spring Boot 开发实战】第1讲 Kotlin 的极简特性之:隐式类型与函数式编程
《Spring Boot 开发实战》—— 基于 Gradle + Kotlin 的企业级应用开发最佳实践 幻灯片1.png Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。在 Java 开发领域的诸多著名框架:Spring 框架及其衍生框架、做缓存Redis、消息队列框架RabbitMQ、Greenplum数据库等等,这些都是 Pivotal 团队的产品。还有Tomcat、Apache Http Server、Groovy里的一些顶级开发者,DevOps理论的提出者都在Pivotal。Spring 团队在现有 Spring 框架的基础上,开发了一个新框架:Spring Boot,用来简化配置和部署 Spring 应用程序的过程,干掉了那些繁琐的开发步骤和样板代码及其配置,使得基于 Spring 框架的 Java 企业级应用开发“极简化”。相比于传统的 Spring/Spring MVC 框架的企业级应用开发(Spring 的各种配置太复杂了,我们之前是在用“生命”在搞这些配置),而Spring Boot...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS关闭SELinux安全模块
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Hadoop3单机部署,实现最简伪集群
- Docker使用Oracle官方镜像安装(12C,18C,19C)