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
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK