【Java】子类到底能继承父类中的哪些内容

一、引入

在将这个知识点的时候,首先要跟大家讲两个误区,很多课程中都会认为:

误区1:父类私有的东西,子类就无法继承了

误区2:父类中非私有的成员,子类可以访问的,就一定是被子类继承下来了。

接下来我们会从两个角度介绍一下,真正情况到底是怎么样的,因为难度比较大,所以首先会通过内存图的方式告诉你结论,等你知道结论之后,再利用Java提供的 内存分析工具,带着你让你看一下事实情况究竟是怎么样的。

第二个 内存分析工具 是不需要大家掌握的,在后面学习到虚拟机的时候还会来说明。

那为什么现在要用 内存分析工具 呢,其实这是传智教育一直的教学观念。传智教育总裁方立勋老师曾经说过:知识点要有出处,特别是像这种比较有争议的知识点,别人怎么知道你讲的就是对的呢?你需要有证据,证据就在今天要学习的 内存分析工具 当中。

废话不多扯,开始来撸。


二、子类到底能继承父类中的哪些内容

子类能继承父类中的哪些内容呢,首先要来看父类中到底有什么。

在父类中总共其实也只有三部分内容:构造方法、成员变量、成员方法,这三个统称为类的成员,不同的成员修饰符它的结果是不一样的。

第一类:非私有,第二类:私有,就是用 private 来修饰的。

其中非私有我们就学了一个:public ,在后面我们还会学习其他的非私有修饰符,它们的规则是一样的。


首先我们来看第一个:构造方法。

构造方法不管是什么样的修饰符,都是是不能够被子类继承下来的。

不管是什么样的修饰符,子类都可以把父类的成员变量继承下来。只不过私有的子类是不能直接使用的,如果一定要使用,就需要通过对应的 get、set 方法来使用。

私有的成员变量子类不能调,但是继承下来和调用不是一个概念。

继承下来指的是,父类里面有的东西,子类没有写,但是它可以把父类中的拷贝一份拿过来,这样相当于子类也有了,这才叫做继承下来。

但是有的时候,子类虽然把东西拿过来了,却调用不了。

但结论就是:不管是什么样的修饰符,成员变量都能被子类继承下来。我们先不管子类能不能用,只要知道关于成员变量子类都可以继承下来就可以了。

3、成员方法

这个跟上面又不一样了,非私有的,它能被继承下来;但是如果成员方法是私有的,就不能继承下来。

image-20240411215803380


三、构造方法是否可以被继承

构造方法不管是什么样的修饰符,都是是不能够被子类继承下来的。

那为什么不能被继承下来呢?关于这个知识点,我们可以反过来理解。

假设构造方法是可以被子类继承的。

下面我在父类中可以写一个空参构造,再写一个有参构造,假设它能被子类继承,那么子类里面就不用再写一遍了,相当于就是把父类里面的构造方法拿过来就可以用了。

但是拿过来之后你有没有发现,它违背了构造方法的定义规则。

image-20240411220043274

构造方法的名字跟子类的类名不一样了!,因此父类的构造方法它是不能被子类继承的。

image-20240412141544449

当子类想要有多个构造方法,必须要自己手动写一遍。如果没有手写任何构造方法,那么虚拟机就会默认给它一个 空构造方法


四、构造方法是否能被继承 —— 代码演示

注意,下面代码只建了一个文件,文件里面包含了三个类,这样写是为了大家看代码的时候更加的方便。但是在以后写综合项目的时候还是要按照以前的习惯:一个Java文件中只写一个类。

package com.itheima.a02oopextendsdemo2;

public class Test {
    public static void main(String[] args) {
        //利用空参构造创建子类对象
        Zi z1 = new Zi();

        //利用带参构造创建子类对象
        Zi z2 = new Zi("zhangsan",23); // 会报错,就是因为子类中没有这个有参构造
    }
}

class Fu{
    String name;
    int age;

    public Fu(){}
    public Fu(String name,int age){
        this.name = name;
        this.age = age;
    }
}

class Zi extends Fu{
    //如果一个类中没有构造方法,虚拟机会自动的给你添加一个默认的空参构造
}

五、成员变量是否可以被继承

1、解释

不管是什么样的修饰符,子类都可以把父类的成员变量继承下来。只不过私有的子类是不能直接使用的,如果一定要使用,就需要通过对应的 get、set 方法来使用。

例如下图,就是直接将 gongFu 成员变量继承了下来。

image-20240412193820194

刚刚看到的是非私有的情况,那如果这个变量用 private 来修饰了呢。其实也很好理解,儿子也将上图的武功招式继承下来了,只不过它拿到的是锁起来的武功招式,不能直接用,如果实在想用,得用钥匙去打开,这里的钥匙指的其实就是 getset方法。

image-20240412194133281


2、没有用 private 修饰 的继承的内存图

看内存图前先来看代码,代码有两段,第一段是关于子父类的,父类中有两个成员变量,一个是 name、一个是 age

在子类继承了父,里面有一个独有的成员变量 game,相当于是儿子玩的游戏。

另一个就是一个测试类。此时我们就可以来看一下最右边的内存。

image-20240412194541688

其中,栈是跟方法有关的,方法被调用,进栈执行;方法里面的代码执行完毕,需要从栈里面出去。

堆是跟 new 关键字有关系的,创建的对象都是在堆里面的。

而右下角的方法区,它是跟字节码文件有关的。一个类想要被使用了,这个类的字节码文件就要加载到方法区当中临时存储。

首先是测试类先来执行,因此 TestStudent 这个类的字节码文件就要加载到方法区,字节码文件中存储的就是 main 方法。

image-20240412205202583

main方法 被虚拟机自动调用,因此 main方法 就需要进栈里去运行 。

然后执行 main方法 里面的第一行代码,在 main方法 的第一行代码中,它用到了子这个类了,因此首先它要把子这个字节码文件先加载到内存当中,在这个里面它存储的就是成员变量 game,在加载的时候它发现了,子类有一个爸爸,因此此时它还会把父类的字节码文件加载到内存当中,在这个里面存储的父里面的 nameage。此时还没结束,父类还有自己的爸爸,

如果一个类没有写继承关系,虚拟机会给它添加一个默认的爹:Object,因此在方法区中,它还会加载 Object 的字节码文件,但是在我们现在的这个内存当中,那个 Object 关系不大,因此那个 Object 暂时可以不用考虑。

image-20240412211636600

继续往下,现在字节码文件都已经加载完毕了,所以说它再来执行等号的左边,等号的左边相当于在栈里面声明了一个变量,变量的名字叫做 z,即栈中的这块小空间名字就叫做 z,前面加了一个 Zi 的类型限定,表示这个小空间以后,能存储 Zi 这个对象的地址值。

等号的右边有一个 new 关键字,一旦看到了 new ,那就一定是在堆里面开辟了一个空间,这个空间就是我们说的对象。因此现在它在堆里面开辟了一个空间,跟以前不一样的是,在以前,我们没有将继承,这个对象里就只有一块,但是现在有了继承了,就会把对象里面一分为二,一份它会去记录父里面的成员变量,还有一部分就是去记录 里面的 成员变量。

因此在左边这块当中,它会记录 nameage,这个是从父类里面继承过来的。还有一块是记录自己的 game(游戏),然后给它们三个做一个默认初始化值,String 引用数据类型,所以默认初始化值是 nullint 默认初始化值就是 0

最后再把地址 001 赋值给左边的 zz 就可以通过 001 找到右边堆内存中创建的对象。

image-20240412212822774

再往下 sout(z),相当于就是把变量 z 里记录的东西做一个打印。由于现在 z 中记录的是一个地址值,所以在控制台中打印的就是地址值 001

再往下,z.name = "钢门吹雪",相当于就是把 "钢门吹雪" 赋值给 znamez 又是 001001 就是右边的这块空间,所以它就会把 "钢门吹雪" 赋值给右边的这块空间里面的 name,它在找的时候,会先找存储子类的成员变量,即右边的这块空间;如果没有找到,再到左边存储父类成员变量这块空间里面找,此时就找到了,因此 name 此时记的值就是 "钢门吹雪"

image-20240412213414903

再来往下,z.age = 23,相当于把 23 赋值给 001age,找到了就是把 23 赋值给 age,原来的 0 就被覆盖了。

接下来,z.game = "王者荣耀",相当于就是把 王者荣耀 赋值给 001game,一下子在右边就能找到了,所以说 game 中记的就是 "王者荣耀"

最后再用 z 获取 nameagegame,分别找到里面对应的值,然后打印出来。

这个就是带有继承结构完整的内存

image-20240412213714768

最后 main方法里的代码全都执行完了,main方法 出栈,一旦方法出去了,方法里的变量也就消失了,变量一旦消失了,针对右边的对象而言,就没有人去用它了,一旦没有人去用它,它就会变成垃圾。

在Java虚拟机里面其实还有个叫做:垃圾回收器。垃圾回收器你可以把它理解成 保洁阿姨保洁阿姨 会在合适的时候会把内存里面这些用不到的垃圾去给清理掉。但是它什么时候去清理我们不知道,它是自动的,我们不管,我们只要知道对象一旦变成垃圾,就不能再用了就可以了。


3、没有用 private修饰 内存图总结

与之前内存图不一样的地方:

1)再加载字节码文件的时候,它会把父类也加载进来

2)在创建对象的时候,它里面会有一部分空间是存储父类继承下来的成员变量;还有一部分空间,才是存储自己子类里面的成员变量。


4、使用private修饰 的内存图

在刚刚,我们父类里面的成员变量没有用 private 修饰,但如果我们用 private 修饰了,这个内存会变成什么样呢,跟刚刚是一样的吗?

如下图,代码已经将成员变量的修饰符都变成了 private。一旦父类中用 private 修饰了,在测试类中,如果用 z 直接调用,就报错了。因此在这里就用红色加以区分,在IDEA中,红色就是错误的意思。

还是来看右边的内存。

image-20240412215126270

在一开始,还是会把 TestStudent 的字节码加载到方法区中,然后 main方法 被虚拟机调用,会进到栈中运行,再来执行main方法中的第一行代码:Zi z = new Zi()

第一行代码中用到了 Zi 这个类,所以会把 Zi 的字节码文件加载到内存。但又因为 Zi 是有一个爹的,它的爹叫做 Fu,因此会把 Fu 的字节码文件也加载到内存。

在加载的时候它又发现了,Fu 也有一个默认的爹 Object,所以在方法区中,其实还会把 Object 字节码加载进来,而由于在这个案例中,Object 暂时没有什么太大的关系,因此我们可以暂时忽略 Object

继续再来往下,此时就轮到等号的左边去执行。等号的左边相当于就是一个变量的定义,因此在栈里面,它会定一个变量,变量的名字叫做 z,类型限定 Zi

再来看等号的右边,有 new 关键字,因此会在堆里面开辟一个空间,再把这块空间一分为二,一部分存储父类成员变量,一部分存储子类的成员变量。然后默认初始化值,最后再把 001 赋值给左边的变量 zz 通过变量,就可以找到右边的对象。

这个时候你就发现了,虽然他是 private 私有的,但是在子类里面,也真的能继承下来。现在,第一行代码就正式执行完了。

image-20240412220014197

继续往下,走第二行 System.out.println(z),相当于就是把变量 z 里面记录的内容打印出来,这里变量里记的就是 001,因此打印的就是 001

再往下,z.name = "钢门吹雪";,由于 z001,所以就把 "钢门吹雪" 赋值给 001name。在找 name 的时候先找右边(子类)发现没有 name,然后再到左边(父类)中找,但是这个时候问题就出现了,name 是被 private 修饰的,一旦被 private 修饰了,像这种直接调用,它是找不到的。所以此时赋值失败,代码报错。

再往下:z.age = 23;,同样的道理,把 23 赋值给 001age,先在右边找,没找到,再到左边找,发现有 private 修饰,因此这个时候也无法赋值,那么 nameage 还是原来的默认初始化 null0

image-20240414214747275

再往下,z.game = "王者农药",这个时候它是可以成功的赋值的,在右边这里找到了 game ,把它赋值成了 "王者农药",原来的 null 就被覆盖了。

最后 sout(z.game),但是由于 nameage 都是由 private 去修饰的,所以它是找不到的,就算打印,它也找不到值,它会报错。

因此在这个地方只能打印 z.game,控制台显示 "王者农药"

image-20240414215820899

最后,代码执行完了,main方法需要出去,栈中的变量消失,堆中的对象没有人用了,对象就会变成垃圾,垃圾回收器会在合适的时候把这个垃圾从内存当中清理掉。

这个就是关于 成员变量 的继承情况。我们要知道,只要是成员变量,不管你是私有的,还是非私有的,在子类里面其实都是可以继承下来的,只不过私有的虽然继承下来了,但是不能直接去使用。

image-20240415201059655


六、成员方法是否可以被继承

1)虚方法表

成员方法就不一样的,私有非私有 是两个不一样的情况。

首先我们先来看一个创建对象调用方法的规则,知道了这样的规则之后,我们再来画内存图。

首先有下面这样的继承结构,A 继承 BB 继承 C,现在在代码当中,我们创建了 A 的对象,用 A 的对象去调用 方法C,你觉得是怎么调用的呢?

很多课程里面是这么解释的:既然我们现在创建的是 A 的对象,首先就应该先到 A类 里面去找 方法C,没有找到,再到它的父类 B类 中找,还是没有,继续到父类 C类 中去找 方法C,一级一级的往上。但这种解释其实是不对的,如果说Java虚拟机真的是这么设计的话,它会被别人骂死。

image-20240415202139605

如果你想不明白,那为什么先将这个继承结构复杂化

image-20240415202226362

再复杂化

image-20240415202246095

此时再来看,如果我们创建的还是 A类 对象,但是我现在想要调用 方法P,按照其他课程的理解方式,那我是不是要从 A类 开始找,然后一级一级的往上,一直找到 P类 才行(其中一共有15个类),你想一下,你只是想要调用 P类的 一个方法,却要查 15个类,这样会让代码的效率变得非常的慢!!!

因此Java在底层它做了一些优化,它会从最顶级的父类开始,设立了一个 虚方发表,它会把这个类当中,经常可能要用到的方法单独抽取出来,放到一个 虚方法表 中。

image-20240415202636974

那什么叫做经常要用的方法呢?它有三个条件:1、方法不能被 private 修饰;2、方法不能被 static 修饰;3、方法不能被 final 关键字修饰。

image-20240415203019050

只要方法满足要求,这样的方法叫做 虚方法,Java就会把这些虚方法抽取出来,放到 虚方法表中,在继承的时候,父类C 它会把自己的 虚方发表 交给儿子 B类,那么在 B类 中它也会有自己的 虚方法表 :它会在 C类 给它的 虚方法表 中再添加自己类中的 虚方法

image-20240415203108903

那么当 A类 再继承 B类 的时候,B类 就会把自己的 虚方法表 再交给儿子 A类,在 A类 中它也会有自己的 虚方法表 :它会在 B类 给它的 虚方法表 中再添加自己类中的 虚方法

image-20240415203310291

有了 虚方发表 后,程序的性能就会大大的提高。

当然除了提高性能之外,虚方法表 还有其他的作用,比如说我们后面马上学习到的 方法重写,关键点就是在 虚方法表 中。

此时,当我们在创建一个 A类 对象的时候,并调用 方法C,此时它就不会一层一层的去往父类找了,因为在 A类 的虚方法表中就能直接找到 方法C,但是它有一个前提条件:方法C 必须是一个 虚方法 才行。

如果 方法C 不是一个虚方法,那么程序还是会跟以前一样,一层一层往上,先找 A类,再找 B类,再找 C类

所以我们要知道一点,子类是可以继承父类里面的方法的,但是不是所有的方法都能被继承下来,只有父类中的虚方法才能被子类继承

image-20240415204119109

有了 虚方法表 的概念之后,我们就可以来画内存图了。


2)内存图

看下下面这段代码,上面是两个 JavaBean类,父类中有一个用 public 修饰的 fuShow1(),还有一个用 private 修饰的 fuShow2

第二个类是 Zi,继承了 Fu,在 Zi类 中,它有一个 public 修饰的 ziShow1()

下面是测试类,在测试类中执行一系列代码。其中,由于 fuShow2() 是由 private 修饰的,因此我直接调用,它是会报错的,在这里我们用红色来标记,表示报错的意思。

在右边我们就要开始来画一画它的内存图。

image-20240415204648950

首先还是测试类 main方法 要运行,所以会把测试类 main方法 的字节码文件加载到方法区,然后虚拟机会自动去调用 main方法main方法 加载进栈。

image-20240415204911858

开始执行 main 方法里面的第一行代码。在第一行代码当中,它用到 Zi,此时,它就会去加载 Zi 的字节码文件,在 Zi 的字节码文件中,它会有 Zi所有成员变量所有的成员方法 。但是在这个代码中我们没有写成员变量,所以成员变量就可以忽略,在这里我们只需要关注成员方法就可以了。

在加载 Zi 的时候,它发现它继承了 Fu,所以它还会把 Fu.class 加载到内存当中。而 Fu 还有个父类 Object,因此 Object 字节码文件也会被加载到方法区。

image-20240415205646370

在它们的下面还会有自己的虚方法表,在 Object类 中它有很多很多的方法,但是不是所有的方法都是 虚方法 ,只有满足 非 private 修饰、非 static 修饰的、非 final 修饰的 ,在 Object类 中,它一共会有 5虚方法,所以 ObjectFu.class 继承下来的是 虚方法表中的5个虚方法

image-20240415210154166

与此同时,在这个 Fu类 中,它也有两个方法,由于 fuShow2() 它是被 private 修饰的,所以不能被叫做 虚方法

但是它会把上面的 fuShow1() 添加到自己的 虚方法表 中。此时在 Fu.class 的虚方法表中,它一共就有 6个 虚方法。

然后它再把自己的 虚方法表 交给 Zi.class

image-20240415210555782

Zi.class 也会把这个类的 虚方法 添加到 虚方法表 中。又由于这里的 ziShow() 是满足 虚方法 的条件的,因此,它会把 ziShow() 也添加进去。

这个时候就要注意了,在 Zi.class 的虚方法表中,它一共有 7个 方法。其中有 5个方法,是从它爷爷 Object.class 中继承下来的,还有一个它是从父类 Fu.class 中继承下来的,还有最后一个 ziShow() 是自己类中的虚方法,一共会有这样的关系。

image-20240415210902160

到目前为止,整个Zi.class字节码文件就算是加载完毕了。

然后再来看 =(等号) 的左边:Zi z ,相当于就是在栈中定义了一个变量,变量的名字叫做 z,类型叫做 Zi,其实也就是在栈中的 main方法 中开辟了一个这样的小空间,就表示这个变量里面以后只能存储 Zi 这个对象的地址值。

再来看 =(等号) 的右边,等号的右边它是有 new 关键字的,看到了 new,就一定是在堆中开辟了一个小空间。

在这个小空间里面,左边这部分用来存储从父类中继承下来的 成员变量 的信息。右边部分使用来记录自己类中 成员变量 的信息,但是现在在代码中没有写成员变量,因此这个空间它就会空着。

image-20240415213918912

整个对象的地址值假设是 001。最后再把 001 赋值给左边的 变量zz 通过 001 就可以找到堆中的对象。

现在 main方法 中的第一行代码才算执行完毕。

image-20240415214539706

接下来执行第二行代码 sout(z),相当于打印 变量z 中记录的内容,你记录什么,就打印什么,由于它现在记录的是堆中的地址值 001,所以在控制台中打印的就是 001

再往下,第三行 z.ziShow(),相当于就是来调用 ziShow() 这个方法。它在调用的时候首先会判断一下你当前的这个方法它是不是 虚方法,如果是 虚方法,它就会从 虚方法表 中直接调用。

现在发现了,ziShow() 是虚方法,所以就会从 Zi.classs的虚方法表中 直接调用。

一旦调用之后,它就会加载到栈中,打印 public --- ZiShow

image-20240415215516208

再往下,执行 z.fushow1,这个方法也是虚方法,因此它会从 Zi.class虚方法表 中直接把 z.ziShow() 方法加载到栈中。

然后执行 ziShow() 中的方法,在控制台中打印 public --- fuShow

image-20240415215837016

最后一个 z.fuShow2(),在调用的时候,虚拟机还是回来判断 fuShow2()虚方法吗,明显不是。

既然不是虚方法,明显就不会在 虚方法表 中找,它会在自己的类中找有没有 fuShow2(),如果没有,到父类中找。

当它找到 fu.class 的时候,它发现这个方法是一个私有的,是不能直接调用的,所以在这里它就会报错。

image-20240416082621294


七、通过 内存分析工具 验证结论

1)代码分析

首先,根据下面代码注解中的序号逐一阅读代码

package com.itheima.a03oopextendsdemo3;

import java.io.IOException;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) throws IOException {
        // 7.在测试类中创建了一个 `Zi` 的对象
        
        // 8.这句话大家自己在打的时候会报错,因为它用到了Java中的第三方工具,这个代码大家就不需要自己练了,你能看懂我们做的是什么就行
        // 如果想要执行这段代码,如要在项目中引入 `jol-core-0.16` 的jar包
        // 这句话的意思是:把对象的地址值z以16进制的形式打在控制台上
        System.out.println(Long.toHexString(VM.current().addressOf(z)));

        // 9. 在这里加 Scanner 的原因是:因为内存分析工具需要程序不停止
        // 如果在这里没有加 Scanner,那么程序刷的一下,直接就会将代码运行完,运行完,程序就会停止了。
        Scanner sc = new Scanner(System.in);
        // 加了键盘录入后,程序会一直停在 18 行
        sc.next();
    }
}


class Fu {
    // 1.`a` 用 `private`(私有) 修饰了,它的值是 `0x111`
    // 0x 开头是十六进制,写成十六进制是在内存结构中,我们看上去会更加方便
    private int a = 0x111;
    // 2.`b` 是非私有的,值是 `0x222`
    int b = 0x222;

    // 3.里面有两个方法
    // 3.1 一个是 `public` 修饰的 `fuShow1()`
    public void fuShow1() {
        System.out.println("public --- FuShow");
    }

    // 3.2 还有一个 `private` 私有修饰的 `fuShow2()`
    private void fuShow2() {
        System.out.println("private --- FuShow");
    }
}

// 4.在 `Zi`类 中它继承了 `Fu`
class Zi extends Fu {
    // 5.里面有个成员变量 `c`,值为 `0x333`
    int c = 0x333;

    // 6.`Zi`类中有 `public` 修饰的 `ziShow()`方法
    public void ziShow() {
        System.out.println("public --- ZiShow");
    }
}

右键运行来看下效果,在控制台中我们看到的就是 Zi 这个对象的地址值。这个是 对象 在内存中的 真实地址。

image-20240416084745314


2)内存分析工具的使用

接下来我们需要点击控制台下面的 Terminal,这个工具相当于就是IDEA集成的CMD工具

image-20240416084949233

在控制台中输入 jps,这个就可以把在内存当中运行的这些 类的 ID 打印出来。

例如我现在运行的是 Test类Test类ID 就是 18036

image-20240416085139060

重新再来打开一个 Terminal image-20240416085228317

输入内存分析工具: jhsdb hsdb,此时这个分析工具就已经弹出来了。

image-20240416085410091

这个工具是 JDK 自带的。

我们可以点击左上角的 File,第一个选项 Attach to HotSpot process...Attach:连接;HotSpot:虚拟机的名字)

image-20240416085454367

点击完成后,我们可以将 进程的ID 粘贴过去。

进程的ID 通过 jps 就能看到了,我这里的 Test 的进程ID是 18036,因此在这我们就输入 18036 即可。

输入完毕后回车即可。

image-20240416085954225

然后我们可以打开一个 内存分析工具:Tools ——> Memory Viewer(这个就是一个内存分析工具)

image-20240416090249455

Address 这里,可以把刚刚看到的地址值输入过来,我这里是 713d4fbe8,这个数字前面一定要记得加上 0x,因为它是十六进制的

image-20240416090440764

然后回车即可。

现在看到右边的这一堆,这一堆其实就是 z 这个对象里面的所有信息

image-20240416090627533


3)验证成员变量是否可以被继承

那么这些信息到底是什么呢?我们先来简单的说一下,后面我们在学习虚拟机的时候,会跟大家补充说明。

先来看第一行,是16个字节,它表示对象的对象头,它里面会有我们后面所学习到的 信息、哈希值 的信息…

在下面的第二行,它又是 16个字节,在读的时候,右边是低位,左边是高位,所以我们在读的时候,需要从右往左读。

image-20240416091005506

那么在第二行的前八个字节,它就表示 当前对象的类型,这里我们看到了 01000c08,这个就表示 Zi 的类型。

image-20240416091535033

再往前看 111222333,这个就是对象里面记录的成员变量的信息

image-20240416091518146

image-20240416091931027

我们打开之前画的内存图,我们曾经说过,我们创建对象的时候,对象的空间里面会有自己子类成员变量里面的信息(右边),还有从父类中继承下来的父类里面的成员信息(左边)。

image-20240416092752217

这些值在内存结构中我们就能看到了。

image-20240416093323833

在这里,我们通过内存分析工具就知道了,父类中成员变量的信息,它是会被子类继承下来的。

并且父类中的 a 的修饰符是 private 私有的,但是也被继承下来了。


4)通过内存分析工具查看类的字节码文件

我们可以再来打开一个工具 Inspector(检查员),通过这个工具我们就能看到字节码文件中 类的信息

image-20240416093534040

我们需要在下面的输入框中输入 Zi,那么 Zi 是谁呢?

在刚刚的 MemoryViewer 工具中,在第二行的前八个字节,它就表示 当前对象的类型,这里我们看到了 00c01408,这个就表示 Zi 的类型。

在输入的时候你是不需要手动输入的,它是支持粘贴复制的,并且还可以将它手动拖过去。

然后将前面的 00000111 成员变量信息删掉。

只不过IDEA做了一个内存地址的压缩,前序还需要补一个 8,这才是它完整的地址。

image-20240416104035144

你会发现,下面的这一些就是关于 Zi类 的字节码文件相关信息,当然这个字节码文件的信息并不是Java形式的,而是 C++ 形式的,我们挑几个关键的来阅读一下。

在这个里面看一个东西:super.Klasssuper:父类),双击将其展开。

这个里面就是 Zi类 的父亲的字节码文件的信息。

image-20240416104025696

在这个父亲里面它其实还有 super:Klass,这个就是 Object

image-20240416104153758

然后再从 Object 的字节码文件中找 super:Klass,可以发现它为 null。因此 Object 再网上就没有爹了。

image-20240416104340692

然后将刚刚展开的都收起来,回到 Zi类 的字节码文件中。


5)通过 内存分析工具 查看虚方法表的长度

再往下找,我们还需要找一个叫做: _vtable_len_,它表示 虚方发表 的长度,Zi类虚方法表 的长度是 7

image-20240416104532052

那么为什么是 7 呢?虚方法 就是从父类中继承过来的方法。

既然要说虚方法,我们就需要去看Java里面的顶级父类,那就是 Object


6)Object类 中的 虚方法

在IDEA中使用快捷键 ctrl + N 搜索一下 Object

image-20240416105027426

Object 中有很多很多的方法,但是不是所有的方法都是可以添加到虚方法表中的。按 ctrl + F12 查看 Object类 中的所有方法,首先来看 clone()方法。

clone()

这个方法它不是私有的,也不是用 static 修饰的,前面也看不到 final,因此这个方法可以加到虚方发表中。

image-20240416105217480

equals()

image-20240416105911746

finalize()

image-20240416105946902

hashCode()

image-20240416110054270

toString()

image-20240416110126399

因此 Objcet类 中,它有五个方法可以加载到 虚方法表 中,并且可以被 Fu类 继承。


7)验证 成员方法 是否可以被继承

Fu类 拿到 Object类 的五个虚方法后,它又会把自己满足虚方法要求的 fuShow1() 加载到 虚方法表 中,因此在 Fu类 的字节码文件的 虚方法表 中一共有 6个 虚方法。

然后它就将这 6个 虚方法交给了 Zi类Zi类 又将自己满足虚方法要求的 ziShow() 再加到 虚方法表 中。

因此 Zi.class 的虚方法表里面一共有 7个虚方法

再来看一下内存分析工具,此时就可以明白 Zi类 中的 vtable (虚方法表)中的长度为 7 的原因了。


8)通过 内存分析工具 查看 成员变量

再往底下翻,fields_count 表示成员变量的个数,个数为 1,就表示是 Zi类 中成员变量只有一个,即这里的 c

image-20240416110834799

再往下翻:nostatic_field_size,表示的是 非静态的成员变量

这个的总数就是本类的加上父类继承过来的,因此它一共是三个。

image-20240416110954062


9)通过 内存分析工具 查看父类中的信息

例如,Fu类 的字节码文件中,虚方法表长度为6

image-20240416111228986

再找到 Fu类super:Klass,即 Object类,可以看见 Object 的虚方法表一共有 5个虚方法

image-20240416111323214


八、总结

因此到目前为止,关于子类中到底能继承父类中的哪些内容,就已经全部学习完毕了。

其中,构造方法不管是什么样的修饰符,子类都不能继承。

成员变量:不管是什么样的修饰符,子类都可以继承。

成员方法:当前的成员方法能被添加到 虚方法表 中,那么这个方法就能被子类继承下来。但是如果你不能添加到 虚方法表 中,那么它就不能被子类继承。

上面是通过内存图的方式说明白的,接下来我们要通过 Java 提供的 内存分析工具去验证一下我们刚刚的结论。

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值