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

JAVA学习网 2019-09-03 14:55:05

鄙人为兴趣爱好,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()

结果如下:

  完美!

  到这里,已经写完了我所理解的继承时内存构建的大部分内容,其实还有关于构造函数返回值的一些想法,但就我目前所学,还未能想出合理的逻辑关系或者原理,故而作罢,待日后水平精进,有新的心得再来添砖加瓦。

 

 

二、我到底是谁!(to be continued。。。)

 

阅读(2298) 评论(0)