

Java中方法与字段的重写
source link: http://bboyjing.github.io/2020/10/27/Java%E4%B8%AD%E6%96%B9%E6%B3%95%E4%B8%8E%E5%AD%97%E6%AE%B5%E7%9A%84%E9%87%8D%E5%86%99/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本章我们来看一下,Java中字段和方法是如何参与重写的。
字段的重写
首先,需要明确一点,Java的字段永远不参与多态,当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会屏蔽父类的同名字段。我们来看一个简单地例子,该例子来自于《深入理解Java虚拟机》:
输出“This guy has $2”,可见调用的是Father的money字段,因为它是通过静态类型访问到的,我们把代码Father guy = new Son();
的“Father”称为变量的“静态类型(Static Type)”,或者叫“外观类型(Apparent Type)”,后面的“Son”则称为变量的“实际类型(Actual Type)”。后面通过代码(Son) guy)
把guy强转成Son类型,此时的静态类型就是Son,所以自然调用的就是Son的money字段了,输出4。所以,可以确认,Java的字段确实是不参与多态的。
再来看下最初的两行输出是因为何,首先两个类的构造函数中都有showMeTheMoney()函数,Son类在创建的时候,首先隐式调用Father的构造函数(跟主调调用super()
行为一样),而Father构造函数中对showMeTheMoney()的调用因为是虚方法调用,实际执行的版本是Son::showMeTheMoney()方法,所以输出都是“I am Son”。第一次输出是0,是因为当时子类Son的构造函数还没执行,它的money字段还是int类型的初始值0。
下面就来看下之前提到的虚方法调用,以及实际执行的版本是怎么回事。
方法的重写
先看一个小例子,同样来自于《深入理解Java虚拟机》:
这段代码很简单,就是Java语言的多态特性。那在JVM层到底是如何实现的呢,我们看一下截取的部分字节码:
16和20行的aload指令分别把之前创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21行是方法调用指令,这两条指令单从字节码角度看,无论是指令还是参数都完全一样,但是这两句指令最终执行的目标方法并不相同。那就得来看下invokevirtual指令的执行流程了:
- 找到操作数栈顶的第一个元素所指的对象的实际类型,记作C。
- 如果在类型C中找到与常量池中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。
至此我们知道了方法重写的本质,再来看一个复杂点的例子:
在这个例子中,Son重写了Father的say2()方法,然后经过一系列方法的调用,其中还有对this、super关键字的调用,有些行为看起来会让人疑惑。下面对照输出的顺序,结合字节码,我们来详细了解一下整个调用过程。
首先基于之前重写的分析,理应调用Son::say1()方法,但是Son并没有重写say1()方法,按照继承关系往上找到Father::say1(),所以此时调用的正是Fathe的say1()方法,输出“this is father say1”。
在Father::say1()中执行了代码
this.getClass().getName()
,意在输出this指向的对象。在这里有一个关于this的知识点:如果执行的是实例方法(没有被static修饰的方法),那么局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。也就是说当调用father.say1();
的时候,默认传递了方法所属对象的实际类型Son的对象。所以说此时运行环境中Father::say1()方法中的this,指向的是main函数中声明的Son对象。所以输出“this -> cn.didadu.sample.jvm.methodInvoke.thisinFather.Son”。看下Father::say1()方法中的部分字节码:11: aload_012: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;15: invokevirtual #6 // Method java/lang/Class.getName:()Ljava/lang/String;第11行,表示把第0位局部变量表的内容推入操作数栈顶,也就是是把this引用推入操作数栈顶,接着invokevirtual指令调用操作数栈顶指向对象的getClass()方法,第15行再调用其getName()方法,就是对应代码
this.getClass().getName()
接着调用say2()方法,其实这里省略了this引用,完整的调用写法应该是
this.say2()
,上面已经清楚地解释了当前this指向的是Son类型的对象,同时Son对象重写了say2()方法,所以调用栈进入了Son::say2(),从如下部分字节码也可以看出来:26: aload_027: invokevirtual #8 // Method say2:()V同样是把第0位局部变量表的内容推入操作数栈顶,也就是是把this引用推入操作数栈顶,接着调用栈顶对象的say2()方法,自然输出“”this is son say2””。
接着输出“this -> cn.didadu.sample.jvm.methodInvoke.thisinFather.Son”,很好理解,因为此时的this一直是当初那个Son对象。
下面两行代码
super.getClass().getName()
和super.say3();
,从输出来看,super指向的是Son对象,但调用的确实Father::say3(),这两个输出为什么是矛盾的。我们来看下字节码:29: aload_030: invokespecial #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;33: invokevirtual #6 // Method java/lang/Class.getName:()Ljava/lang/String;44: aload_045: invokespecial #9 // Method cn/didadu/sample/jvm/methodInvoke/thisinFather/Father.say3:()V这里使用了invokespecial指令,是在编译时就确定了方法调用的版本。
super.getClass()
在编译期确定了调用Object.getClass()方法,看一下Object.getClass()方法的注释:Returns the runtime class of this {@code Object}。也就是说,返回的是运行时对象的类型。这里很明显,运行时对象还是那个Son实例。所以输出“super -> cn.didadu.sample.jvm.methodInvoke.thisinFather.Son”。super.say3()
在编译期确定了调用Father::say3()方法,所以输出“this is father say3”。
至此,算是理清了字段和方法的重写。尤其是方法重写的过程,这也是模板方法模式得以运行的根本所在吧。
Recommend
-
128
Java中方法的重写与成员变量的隐藏-51CTO.COM Java中方法的重写与成员变量的隐藏 作者:Hollis 2017-09-30 09:10:21 这篇文章讨论了Java面向对象概念中一个基本的概念–Field Hiding(隐...
-
22
【51CTO.com原创稿件】 在 C# 中 Object 是所有类的基类,所有的结构和类都直接或间接的派生自它。前面这段话可以说所有的 C# 开发人员...
-
6
如何判断一个原生方法是否被重写 蚊子前端博客 发布于 2020-11-12 11:44 有的脚本会重写该方法,那么如何判断这个方法是否被重写了呢? 浏览器根据 ECMScript 标...
-
8
V2EX › JavaScript [IT 邦帮忙] 优化或重写方法,使计算长字符串时尽可能缩短运算时间 ciddechan
-
7
Codable 作为 Swift 的特性之一也是很注重安全,也很严谨,但它对于“严谨”和“安全”的定义不一定跟别的语言一样,这就导致了它在实际使用时总会有这样那样的磕磕绊绊,我们不得不重写 init 方法去让它跟外部环境融洽地共存。最近在工作中这样的事情发生多了,我...
-
5
下图是我自我学习模拟数组时总结的一些重新数组的方法:本文我们暂不讨论不改变原数组的方法,只谈改变原数组用到的 6 种方法。改变原数组的方法push()按参数顺序向数组尾部添加元素,返回新数组的长度
-
5
Django model重写save方法及update踩坑记录 一个非常实用的小方法 试想一下,Django中如果我们想对保存进数据库的数据做校验,有哪些实现的方法? 我们可以在view中去处理,每当view接收请求,就对...
-
3
Java 中的静态字段和静态方法 作者:踏雪痕还记得我们写的第一个 Java 代码吗? public class Main { public...
-
8
经常会想要重写 localStorage 来实现某个功能,都有哪些方法重写 localStorage 里的方法呢? 经常会有同学想要重写 localStorage 中的方法,来实现 key 的过期时间,或者监听 key 的读写等。那么都有哪些方法重写 localStorage 里的...
-
5
重写并自定义依赖的原生的Bean方法 转载请注明出处:
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK