java 动态重写方法_java方法调用之动态调用多态(重写override)的实现原理——方法表(三)...

上两篇篇博文讨论了java的重载(overload)与重写(override)、静态分派与动态分派。这篇博文讨论下动态分派的实现方法,即多态override的实现原理。

java方法调用之重载、重写的调用原理(一)

java方法调用之单分派与多分派(二)

本文大部分内容来自于IBM的博文多态在 Java 和 C++ 编程语言中的实现比較 。这里写一遍主要是加深自己的理解。方便以后查看,增加了一些自己的见解及行文组织,不是出于商业目的,如若须要下线。请告知。

结论

基于基类的调用和基于接口的调用,从性能上来讲,基于基类的调用性能更高 。

由于invokevirtual是基于偏移量的方式来查找方法的。而invokeinterface是基于搜索的。

概述

多态是面向对象程序设计的重要特性。多态同意基类的引用指向派生类的对象,而在详细訪问时实现方法的动态绑定。

java对方法动态绑定的实现方法主要基于方法表,可是这里分两种调用方式invokevirtual和invokeinterface,即类引用调用和接口引用调用。类引用调用仅仅须要改动方法表的指针就能够实现动态绑定(具有同样签名的方法,在父类、子类的方法表中具有同样的索引號)。而接口引用调用须要扫描整个方法表才干实现动态绑定(由于。一个类能够实现多个接口,另外一个类可能仅仅实现一个接口。无法具有同样的索引號。这句假设没有看懂,继续往下看。会有样例。

写到这里。感觉自己看书时,有的时候也会不理解,看不懂,思考一段时间,还是不明确,做个标记,继续阅读吧。然后回头再看。可能就豁然开朗。)。

类引用调用的大致过程为:java编译器将java源码编译成class文件,在编译过程中。会依据静态类型将调用的符号引用写到class文件里。在运行时,JVM依据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量。然后依据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,假设在实际类型的方法表中找到该方法,则直接调用,否则。依照继承关系从下往上搜索。

以下对上面的描写叙述做详细的分析讨论。

JVM的运行时结构

8347e9311b6471974c8c0a93ffc8ac60.png

从上图能够看出,当程序运行时。须要某个类时,类加载子系统会将相应的class文件加载到JVM中,并在内部建立该类的类型信息。这个类型信息事实上就是class文件在JVM中存储的一种数据结构,他包含着java类定义的全部信息。包含方法代码,类变量、成员变量、以及本博文要重点讨论的方法表。这个类型信息就存储在方法区。

注意,这种方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息仅仅有唯一的实例(所以是各个线程共享的内存区域)。而在堆中能够有多个该class对象。能够通过堆中的class对象訪问到方法区中类型信息。

就像在java反射机制那样,通过class对象能够訪问到该类的全部信息一样。

方法表是实现动态调用的核心。

方法表存放在方法区中的类型信息中。方法表中存放有该类定义的全部方法及指向方法代码的指针。这些方法中包含从父类继承的全部方法以及自身重写(override)的方法。

类引用调用invokevirtual

代码例如以下:

package org.fan.learn.methodTable;

/**

* Created by fan on 2016/3/30.

*/

public class ClassReference {

static class Person {

@Override

public String toString(){

return "I'm a person.";

}

public void eat(){

System.out.println("Person eat");

}

public void speak(){

System.out.println("Person speak");

}

}

static class Boy extends Person{

@Override

public String toString(){

return "I'm a boy";

}

@Override

public void speak(){

System.out.println("Boy speak");

}

public void fight(){

System.out.println("Boy fight");

}

}

static class Girl extends Person{

@Override

public String toString(){

return "I'm a girl";

}

@Override

public void speak(){

System.out.println("Girl speak");

}

public void sing(){

System.out.println("Girl sing");

}

}

public static void main(String[] args) {

Person boy = new Boy();

Person girl = new Girl();

System.out.println(boy);

boy.eat();

boy.speak();

//boy.fight();

System.out.println(girl);

girl.eat();

girl.speak();

//girl.sing();

}

}

注意,boy.fight(); 和 girl.sing(); 这两个是有问题的,在IDEA中会提示“Cannot resolve method ‘fight()’”。由于,方法的调用是有静态类型检查的,而boy和girl的静态类型都是Person类型的,在Person中没有fight方法和sing方法。因此。会报错。

运行结果例如以下:

4633165db15073e6ef71ad359f778970.png

从上图能够看到,boy.eat() 和 girl.eat() 调用产生的输出都是”Person eat”。由于Boy和Girl中没有override 父类的eat方法。

字节码指令:

public static void main(java.lang.String[]);

Code:

Stack=2, Locals=3, Args_size=1

0: new #2; //class ClassReference$Boy

3: dup

4: invokespecial #3; //Method ClassReference$Boy."":()V

7: astore_1

8: new #4; //class ClassReference$Girl

11: dup

12: invokespecial #5; //Method ClassReference$Girl."":()V

15: astore_2

16: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;

19: aload_1

20: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V

23: aload_1

24: invokevirtual #8; //Method ClassReference$Person.eat:()V

27: aload_1

28: invokevirtual #9; //Method ClassReference$Person.speak:()V

31: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;

34: aload_2

35: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V

38: aload_2

39: invokevirtual #8; //Method ClassReference$Person.eat:()V

42: aload_2

43: invokevirtual #9; //Method ClassReference$Person.speak:()V

46: return

当中全部的invokevirtual调用的都是Person类中的方法。

以下看看java对象的内存模型:

f6e0545b5483c8f93ee413c286c33055.png

从上图能够清楚地看到调用方法的指针指向。

并且能够看出同样签名的方法在方法表中的偏移量是一样的。这个偏移量仅仅是说Boy方法表中的继承自Object类的方法、继承自Person类的方法的偏移量与Person类中的同样方法的偏移量是一样的。与Girl是没有不论什么关系的。

以下再看看调用过程,以girl.speak() 方法的调用为例。在我的字节码中,这条指令相应43: invokevirtual #9; //Method ClassReference$Person.speak:()V ,为了便于使用IBM的图,这里採用跟IBM一致的符号引用:invokevirtual #12; 。调用过程图例如以下所看到的:

bd04ecb065d04dcc588f706119ab07ad.png

(1)在常量池中找到方法调用的符号引用

(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15)。这样就得到该方法的直接引用。

(3)依据this指针确定方法接收者(girl)的实际类型

(4)依据对象的实际类型得到该实际类型相应的方法表,依据偏移量15查看有无重写(override)该方法。假设重写。则能够直接调用;假设没有重写。则须要拿到依照继承关系从下往上的基类(这里是Person类)的方法表。同样依照这个偏移量15查看有无该方法。

接口引用调用invokeinterface

代码例如以下:

package org.fan.learn.methodTable;

/**

* Created by fan on 2016/3/29.

*/

public class InterfaceReference {

interface IDance {

void dance();

}

static class Person {

@Override

public String toString() {

return "I'm a person";

}

public void speak() {

System.out.println("Person speak");

}

public void eat() {

System.out.println("Person eat");

}

}

static class Dancer extends Person implements IDance {

@Override

public String toString() {

return "I'm a Dancer";

}

@Override

public void speak() {

System.out.println("Dancer speak");

}

public void dance() {

System.out.println("Dancer dance");

}

}

static class Snake implements IDance {

@Override

public String toString() {

return "I'm a Snake";

}

public void dance() {

System.out.println("Snake dance");

}

}

public static void main(String[] args) {

IDance dancer = new Dancer();

System.out.println(dancer);

dancer.dance();

//dancer.speak();

//dancer.eat();

IDance snake = new Snake();

System.out.println(snake);

snake.dance();

}

}

上面的代码中dancer.speak(); dancer.eat(); 这两句同样不能调用。

运行结果例如以下所看到的:

ebaab197df1c28194e178d10b1468615.png

其字节码指令例如以下所看到的:

public static void main(java.lang.String[]);

Code:

Stack=2, Locals=3, Args_size=1

0: new #2; //class InterfaceReference$Dancer

3: dup

4: invokespecial #3; //Method InterfaceReference$Dancer."":()V

7: astore_1

8: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;

11: aload_1

12: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V

15: aload_1

16: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V

21: new #7; //class InterfaceReference$Snake

24: dup

25: invokespecial #8; //Method InterfaceReference$Snake."":()V

28: astore_2

29: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;

32: aload_2

33: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V

36: aload_2

37: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V

42: return

从上面的字节码指令能够看到,dancer.dance(); 和snake.dance(); 的字节码指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V 。

为什么invokeinterface指令会有两个參数呢?

对象的内存模型例如以下所看到的:

be5836dd7cb140e1357757396ab0ae87.png

从上图能够看到IDance接口中的方法dance()在Dancer类的方法表中的偏移量跟在Snake类的方法表中的偏移量是不一样的,因此无法仅依据偏移量来进行方法的调用。(这句话在理解时。要注意,仅仅是为了强调invokeinterface在查找方法时不再是基于偏移量来实现的,而是基于搜索的方式。

)应该这么说,dance方法在IDance方法表(假设有的话)中的偏移量与在Dancer方法表中的偏移量是不一样的。

因此,要在Dancer的方法表中找到dance方法,必须搜索Dancer的整个方法表。

以下写一个,假设Dancer中没有重写(override)toString方法,会发生什么?

代码例如以下:

package org.fan.learn.methodTable;

/**

* Created by fan on 2016/3/29.

*/

public class InterfaceReference {

interface IDance {

void dance();

}

static class Person {

@Override

public String toString() {

return "I'm a person";

}

public void speak() {

System.out.println("Person speak");

}

public void eat() {

System.out.println("Person eat");

}

}

static class Dancer extends Person implements IDance {

// @Override

// public String toString() {

// return "I'm a Dancer";

// }

@Override

public void speak() {

System.out.println("Dancer speak");

}

public void dance() {

System.out.println("Dancer dance");

}

}

static class Snake implements IDance {

@Override

public String toString() {

return "I'm a Snake";

}

public void dance() {

System.out.println("Snake dance");

}

}

public static void main(String[] args) {

IDance dancer = new Dancer();

System.out.println(dancer);

dancer.dance();

//dancer.speak();

//dancer.eat();

IDance snake = new Snake();

System.out.println(snake);

snake.dance();

}

}

运行结果例如以下:

b6cc004e91d8cb037300358da4145b6d.png

能够看到System.out.println(dancer); 调用的是Person的toString方法。

内存模型例如以下所看到的:

323c48b1d0fb9461263485174bbdfbbd.png

结束语

这篇博文讨论了invokevirtual和invokeinterface的内部实现的差别,以及override的实现原理。

下一步,打算讨论下invokevirtual的详细实现细节。如:怎样实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。

參考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值