从JVM角度看Java多态

简介:

Java多态的三个必要条件:

1、 继承

2、 子类重写父类方法

3、 父类引用指向子类对象

 

然后看一个例子

复制代码

package test.xing;class Father{    protected int age;    public Father(){
       age = 40;
   }    
   void eat(){
       System.out.println("父亲在吃饭");
   }
}class Child extends Father{    protected int age;    public Child(){
       age = 18;
   }    
   void eat(){
       System.out.println("孩子在吃饭");
   }    void play(){
       System.out.println("孩子在打CS");
   }
}public class TestPolymorphic {    public static void main(String[] args) {
       Father c = new Child();
       c.eat();        //c.play();
       System.out.println("年龄:"+c.age );
       
   }

}

复制代码

输出结果为:

给出结论:当满Java多态的三个条件时,可以发现c.eat()调用的实际上是子类的eat,但c.age调用的还是父类的age,而c.play()则不会通过编译。

下面从JVM的角度解释上面这种现象

我们就从Father c = new Child()这句话切入

这句话首先会执行new Child(),在堆中分配一个对象。

当然在分配Child类的实例时,先要通过JVM的类加载器将Child类对应的class文件加载到JVM中,然后JVM根据class文件中的字节流产生一个表示class文件中类型信息的结构体

这个结构体的具体实现,不同的JVM会有不同的实现方式,但大致上都差不多,都要遵守JVM的规范。

这个表示class文件中类型信息的结构体大概由以下几部分构成:

1、 常量池

2、 类变量(静态变量)

3、 字段信息

4、 方法信息

5、 类的父类信息

6、 类的访问权限信息等

这些我说的也不够准确,具体的大家可以看JVM相关的书籍如《深入理解Java虚拟机》,在这里就有个大概的概念就好。

之后,JVM会根据上面这个结构体生成一个叫做方法表的东西。这个方法表是实现java多态的一个关键

方法表中包含的是实例方法(就是相对于静态方法而言的,用对象访问的那些方法)的直接引用,也就是说通过这个方法表就能够访问到该类的实例方法

而且,这些实例方法不仅包括本类的方法,还包括其父类的实例方法,以及父类的父类的实例方法(就是一直到Object)。

而且,这些方法中不包含私有方法(因为私有方法不能继承)

方法表中的这些直接应用会指向到JVM中表示类型信息的那个结构体(就是上面那个结构体)的相应的方法信息(就是上面结构体中4的某个位置),当然这只是本类的方法,表中还有父类的方法,相应地指向父类类型信息结构体的具体位置。

可能表达的不够清晰,下面画个图表示。

上面提到过,方法表中不仅包括本类的方法,还包括父类的方法,方法表值这样产生的,以Child类的方法表为例:

首先方法表中,会产生指向继承自Object类的方法的引用,这些包括指向toString的和指向equals的,当然Object中还包括很多方法,这里就不写了

然后方法表中产生指向继承自Parent类的方法的引用,这包括eat,

最后产生指向本类的方法的引用。

这里需要注意的一点是,当Child类的方法表产生指向Parent类中的方法的引用时,会有一个指向eat方法的引用,最后产生指向本类的方法的引用时,也有一个指向eat的引用,这时候,新的数据会覆盖原有的数据,也就是说原来指向Parent.eat的那个引用会被替换成指向Child.eat的引用占据原来表中的位置)。所以我们看到在Child类的方法表中指向的是Child.eat而Parent类的方法表中指向的是Parent.eat。子类的方法表中就没有指向Parent.eat的引用了。

而且还要注意一个特点就是,Parent和Child的方法表中,指向eat的引用在表中的偏移量是一样的,都是第三个位置。(这是因为子类eat方法覆盖掉了父类eat方法,占据了原来父类eat方法的引用在表中的位置

这里再多说一句,表示类型信息的结构体中,的方法信息,只包含本类特有的或者是重写的方法信息,没有父类的方法信息

 

了解了方法区的结构后,我们来看堆中对象的结构

 

接下来是栈区,产生Father类型的引用,这个引用指向堆区中的Child类的实例。

这里需要解释一下Father c的含义,我们知道c表示一个引用,这个引用指向堆中的Child类的实例,说白了就是一个地址,这个地址指向堆中的Child的类的实例,但是我们不要忘记前面还有一个Father修饰这个c

我们都知道在c中有void类型的指针,而给指针前面限定一个类型就限制了指针访问内存的方式,比如char * p就表示p只能一个字节一个字节地访问内存,但是int *p中p就必须四个字节四个字节地访问内存。

但是我们都知道指针是不安全的,其中一个不安全因素就是指针可能访问到没有分配的内存空间,也就是说char *虽然限制了p指针访问内存的方式,但是没有限制能访问内存的大小,这一点要完全靠程序员自己掌握。

但是在java的引用中Father不但指定了c以何种方式访问内存,也规定了能够访问内存空间的大小

我们看Father实例对象的大小是占两行,但Child实例对象占三行(这里就是简单量化一下)。

所以虽然c指向的是Child实例对象,但是前面有Father修饰它,它也只能访问两行的数据,也就是说c根本访问不到Child类中的age!!!只能访问到Father类的age,所以输出40

而且我们注意两个类的方法表:

我们看到Parent的方法表占三行,Child的方法表占4行,c虽然指向了Child类的实例对象,而对象中也有指针指向Child类的方法表,但是由于c受到了Father的修饰,通过c也只能访问到Child方法表中前3行的内容!!!!

然而前面说过,在方法表的形成过程中,子类重写的方法会覆盖掉表中原来的数据,也就是Child类的方法表的第三行是指向Child.eat的引用,而不是指向Parent.eat(因为方法表产生了覆盖),所以c访问到的是Child.eat。也就是子类的方法!!!这种情况下,c是没有办法直接访问到父类的eat方法的。



本文转自 sshpp 51CTO博客,原文链接:http://blog.51cto.com/12902932/1925688,如需转载请自行联系原作者
相关文章
|
12天前
|
Java
Java中的继承和多态是什么?请举例说明。
Java中,继承让子类继承父类属性和方法,如`class Child extends Parent`,子类可重写父类方法。多态允许父类引用指向子类对象,如`Animal a = new Dog()`,调用`a.makeSound()`会根据实际对象类型动态绑定相应实现,增强了代码灵活性和可扩展性。
10 0
|
15天前
|
Oracle Java 关系型数据库
java体系结构和jvm
java体系结构和jvm
|
19天前
|
搜索推荐 Java
Java的面向对象特性主要包括封装、继承和多态
【4月更文挑战第5天】Java的面向对象特性主要包括封装、继承和多态
15 3
|
1月前
|
Java
【Java】一个简单的接口例子(帮助理解接口+多态)
【Java】一个简单的接口例子(帮助理解接口+多态)
17 0
|
26天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
69 0
|
1天前
|
监控 Ubuntu Java
Java VisualVM远程监控JVM
Java VisualVM远程监控JVM
Java VisualVM远程监控JVM
|
6天前
|
缓存 监控 Java
深入理解Java虚拟机(JVM)性能调优
【4月更文挑战第18天】本文探讨了Java虚拟机(JVM)的性能调优,包括使用`jstat`、`jmap`等工具监控CPU、内存和GC活动,选择适合的垃圾回收器(如Serial、Parallel、CMS、G1),调整堆大小和新生代/老年代比例,以及代码优化和JIT编译策略。通过这些方法,开发者能有效提升应用性能并应对复杂性挑战。性能调优是持续过程,需伴随应用演进和环境变化进行监控与优化。
|
1月前
|
Java
java中的多态
java中的多态
9 2
|
1月前
|
Java 编译器
JAVA多态
JAVA多态
9 0
|
1月前
|
存储 缓存 安全
[Java基础]——JVM内存模型
[Java基础]——JVM内存模型