Java学习之旅(一):探索extends

2019-09-04 07:17:39来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

Java学习之旅(一):探索extends

鄙人为兴趣爱好,0基础入门学习Java,有些心得想法,记录于此,与君分享。

然毕竟新手,学识尚浅,错误之处,希望多多指正批评,也是对我最大的帮助!

 

前言:本篇文章,主要讨论在子类继承父类之后,一些继承在内存中构建的过程,以及this和super的特点和异同

文章内所有内容均为个人猜测和想法,不代表任何学科结论。

 

一、我是孙子!

  既然有孙子,那肯定是指三代传承,所以,我准备了三个类,A是爷爷,B是爸爸,C是孙子(也就是我,一下均以孙子代替)。代码如下:

类A(爷爷):

 1 public class A {
 2     //属性部分
 3     public int i_A;
 4     public String str_A = "我是类A里str属性的初始值";
 5     //一般方法部分
 6     public void function (){
 7         System.out.println("我是类A里的“function”方法,我不接受参数");
 8     }
 9     public void function_A(){
10         System.out.println("我是类A里的“function_A”方法,我不接收参数");
11     }
12     public void function_A(int n){
13         System.out.println("我是类A里的“function_A”方法,我接收一个值为"+n+"的int类型参数");
14     }
15     //构造方法部分
16     public A(){
17         System.out.println("我是类A的无参数构造方法");
18     }
19     public A(int n){
20         System.out.println("我是类A的带参数构造方法,我接收一个值为"+n+"的int类型参数");
21     }
22     //代码块部分
23     {
24         System.out.println("我是类A的代码块,我受过严格的训练");
25     }
26 }
Class A

类B(爸爸):

 1 public class B extends A {
 2     //属性部分
 3     public int i_B;
 4     public String str_B = "我是类B里str属性的初始值";
 5     //一般方法部分
 6     public void function (){//父类A里一般方法的重写
 7         System.out.println("我是类B里的“function”方法,是对父类A方法的重写,我不接受参数");
 8     }
 9     public void function_B(){
10         System.out.println("我是类B里的“function_B”方法,我不接收参数");
11     }
12     public void function_B(int n){
13         System.out.println("我是类B里的“function_B”方法,我接收一个值为"+n+"的int类型参数");
14     }
15     //构造方法部分
16     public B(){
17         System.out.println("我是类B的无参数构造方法");
18     }
19     public B(int n){
20         System.out.println("我是类B的带参数构造方法,我接收一个值为"+n+"的int类型参数");
21     }
22     //代码块部分
23     {
24         System.out.println("我是类B的代码块,无论多好笑,我都不会笑");
25     }
26 }
Class B

类C(孙子):

public class C extends B{
    //属性部分
    public int i_C;
    public String str_C = "我是类C里str属性的初始值";
    //一般方法部分
    public void function (){//父类A里一般方法的重写
        System.out.println("我是类C里的“function”方法,是对父类B方法的重写,我不接受参数");
    }
    public void function_C(){
        System.out.println("我是类C里的“function_C”方法,我不接收参数");
    }
    public void function_C(int n){
        System.out.println("我是类C里的“function_C”方法,我接收一个值为"+n+"的int类型参数");
    }
    //构造方法部分
    public C(){
        System.out.println("我是类C的无参数构造方法");
    }
    public C(int n){
        System.out.println("我是类C的带参数构造方法,我接收一个值为"+n+"的int类型参数");
    }
    //代码块部分
    {
        System.out.println("我是类C的代码块,除非忍不住");
    }
}
Class C

  准备好了三个类,那我就要开始生成一个孙子。首先,我们不带参数new一个C类对象c_new,在main中的代码如下:

1 public class Relation {
2     public static void main(String[] args){
3         C new_c = new C();
4     }
5 }
main方法

  执行结果如下图:

分析一:

  通过结果来看

  第一、给我的直观感受是,java在new一个孙子对象的过程中,最先是new了一个爷爷类,再在爷爷类的后面new了一个爸爸类,最后在爸爸类的后面,new了一个孙子类。又由于object是所有类的父类,object也是爷爷类的父类,所以爷爷类其实是new在object类之后。

  第二、我认为,所有的“new”操作,一定都是在内存中开辟地址连续的空间(如果new出来的空间不连续,我估计整个java体系就会坍塌),而能100%保证正确申请到所需空间的步骤应该都是:先确定大小,再开辟空间,这个开辟的过程我觉得应该是JVM做的,而且开辟的空间必须等于或者大于所需要的空间。(我要是设计者,我会设计会大于所需的空间,多余的空间可以存储一些代码信息,用于其他的用途,比如底层的一些保护或者回收机制)

  所以,我产生了一个想法:

  这个开辟空间的过程并非动态的(先开辟A大小空间,再接着内存地址开辟B大小空间,再……),而是在编译的过程中,JVM会把A extends Object(隐式) 、B extends A、C extends B这个关系额外记录成一条信息,这条信息告诉电脑,如果我需要生成一个C类对象,你需要给我对应大小空间的连续内存块。于是,在得到指令需要生成一个C类对象后,内存会被开辟出一个能装得下从object到C类所有大小的地址连续的内存空间。

  但是又会存在一个问题,如果我把main中“ C new_c = new C(); ”这条语句注释掉,再编译执行,能通过,既然main里面什么都没做也可以执行通过,那此时ABC和object这四个类到底在不在内存中呢?还是说只有new出现了,才会产生我上述的编译过程?

  为了证明这一点,我在main()里设计了一个while(true)的方法,可以无限选择生成三种类中的任意一种类对象,我发现,在已经编译后的程序执行过程中,我可以随意选择生成A、B或者C类的对象,这说明,类A,B,C甚至object肯定还是在内存中存在的,但是存在的空间是否连续,我无法确定,而且没有一个具体的媒介可以用到他们,而且我猜想,很大可能性,这个内存状态下,各个类所占用的空间也仅仅是类里面代码描述内容所需占用的内存空间,并不是像生成的具体对象那样的内存空间。

  这个确定存在的推论让我又得到一条信息,new的过程肯定都是复制的某块内存的数据,而这块内存正好是所需要生成对象的类存在的内存块,不然怎么可能在不操作内存数据的情况下,把可能不连续的内存块,变成一定连续的内存块呢?(因为我觉得如果通过操作内存数据来使内存连续实在太费力了,不符合java的特性)。

  综上所述,以我的代码为例,我觉得整个 “C new_c = new C();” 的过程应该是下面这个步骤:

  1)内存中已经有几块区域存放了类Object、类A、类B和类C的主体。假如这四个类所占空间分别是50字节,100字节,100字节,100字节。合计:350字节。

  2)在内存的另外一块足够大的地方,开辟出一个350字节的连续地址空间,从object类到C类,依次将其内存的内容赋值到这块新的内存中,这块新内存是连续的,并产生一个代表这块内存的地址值。

  3)将这个地址值,存到另外一块名为c_new的内存里。

  至此,我觉得整个new的过程就完成了,以后对于c_new的操作,都是在c_new的值指向的新内存里面进行的反复读写与计算。

 

  

分析二:

  通过执行顺序来看:

  众所周知,构造一个类的具体对象是通过类里面的构造方法实现的,那么我们按照这个原则,开始从C();分析代码的执行顺序。

  进入C();第一行,直接就是输出字符串 "我是类C的无参数构造方法" ,这和结果完全不一样。从教程中,我了解到,其实构造函数的第一行,再未显式调用的情况下,永远是隐式调用了一个方法,super(),即调用当前类的父类的构造方法。所以我写的构造函数,实际上是这样的:

public C(){
        super();
        System.out.println("我是类C的无参数构造方法");
    }
C();
public C(int n){
        super();
        System.out.println("我是类C的带参数构造方法,我接收一个值为"+n+"的int类型参数");
    }
C(int n);

  依次类推,B和A类的构造方法应该均是如此。所以,整个执行过程应该是C();→B();→A();→object();,这是几个方法的嵌套使用(object有没有构造函数我不确定,目前只讨论ABC类,姑且这样写吧),在object();执行完毕之后,要执行的理应是A();里的下一条语句,System.out.plintln();,但是,结果显示的却是先执行了代码块的部分,而且,B、C类也是如此,我觉得这应该是java设计的一种类的规则,并不是某种机制产生的客观结果(也可能是,只是我还不懂),而且代码块的调用是在super()和输出之间发生,只有这种解释,才能将输出结果与构造过程一一对应。可以new一个带参数的C类对象佐证。

public class Relation {
    public static void main(String[] args){
        C new_c = new C(5);//带参数
    }
}
main()

结果如下:

  完美!

 

 

二、我到底是谁!

  是的,我又变成了this,我是一个指代关键字,在类的代码里面,指代当前类的具体对象,用来调用当前类的各种属性和方法。比如,在类C里面,我是代表类C的对象,在类B里面,我代表B的对象,在A类里,我代表A的对象。。。但是,真的如此吗?我到底是谁?

  为了查明真想,首先,我们在C类的function_C方法里加入一行代码,

public void function_C(){
        this.function();
        System.out.println("我是类C里的“function_C”方法,我不接收参数");
    }
function_C()

  通过main里生成的new_c调用function_C方法,执行后结果如下

 

  function_C方法正确调用到了this所在C类的function方法。

  但是,如果在B类的function_B方法里加入调用方法,那this到底调用的哪一个function方法?让我来测试一下。

1 public void function_B(){
2         this.function();
3         System.out.println("我是类B里的“function_B”方法,我不接收参数");
4     }
function_b()

  结果如下

  没错,function_B里的this.function()调用的却是C类的function方法。如果在A类里面测试,结果一样。

  看上去,this并不是严格指代当前类的对象,起码在父类里通过this调用重写的方法时不符合这个说法。那么如果测试的重写的方法,而是重写的属性(这个说法可能并不专业和准确)呢?在三个类里面设置一个同名同类型的变量,经过同上步骤的测试,this指代的又变成了当前类的对象。

  考虑到function方法实际上是子类对父类的重写,情况可能有些特殊,所以关于this的指代性,可以做一个一般归纳:

  在继承中,this确实在一般情况下,指代的是一个当前类的对象,但是如果在父类或者父父类(父类的父类)中通过this调用重写的方法(同名同参数),那么实际调用的就是new出来的具体对象的重写方法,不会调用到父类,或者父父类。(这个结论的佐证就是,在父类中输入this.后,IDE不会提示到子类的各种属性和方法,除了重写的方法)

  接下来,我们讨论一下this();,通过this调用构造方法。

  首先,我们在C类的C(int n)方法里,第一行加入this(),然后带参数运行,结果如下:

 

  通过追踪代码的执行顺序,我们不难得出一个结论,this()把原来的隐藏的super()真正的屏蔽了起来,而如果显式调用super(),编译不会通过,这说明,this()和super()不能同时存在。其实这也很好理解,如果同时存在,那这个类的生成后就会非常混乱。所以这也应该是Java设定的特性。

 

 

 

三、我就是你爸爸!

 

  super就是爸爸,他比this的理解简单多了,在任何情况下,super都指代当前类的父类的对象,没有其他特例。就算是在父类里写super.function(),调用的也是父父类的function()方法,测试如下:

  在B类的function_B方法的程序块第一行写super.function();,然后main中用生成的C类对象调用到function_B查看结果。

 

1 public void function_B(){
2         super.function();
3         System.out.println("我是类B里的“function_B”方法,我不接收参数");
4     }
function_B()

 

  结果如下:

 

  从结果不难看出,即使实际调用的是C类对象,但是super是写在B类的function_B方法里的,所以会找到B类的父类里的对应的方法或者属性。

  而super()就更不用讨论什么了,因为在任何子类的构造函数里面的第一行默认都是super(),不管显式或者隐式,他都必须在那里。

  this()和super()的共同点就是:

    ①必须在构造函数里存在。在其他位置没有实际意义,编译也不会通过。

    ②都必须在构造函数的第一行。作为用来生成对象的方法,肯定是先有了对象你才能使用对象。

    ③由于第二条的存在,那么this()和super()肯定不能同时存在。如果构造函数内是this(),那编译器应该会默认注释掉隐式的super()。

 

 

四、结尾

  到这里,已经写完了我所理解的继承时内存构建、this和super特性的大部分内容,其实这里面应该有很多不正确的地方或者没有考虑到的地方,但是作为心得体会,留下记录,可便于日后重新审视,发现自己的不足,从而提升自己,更欢迎欢迎大家多多批评指正,让我更快速的进步!


原文链接:https://www.cnblogs.com/Azir-s-soldier/p/11449549.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:从技术的角度分析下为什么不要在网上发“原图”

下一篇:volatile底层实现原理