Java进阶---对象与内存控制(二)(磨灭你的自信)

Java进阶---对象与内存控制()

2、父类构造器

无论是创建什么Java对象,程序总会依次调用该类的父类的非静态初始化块、父类构造器执行初始化,然后调用该类的非静态初始化块、构造器执行初始化。

1)、显式调用和隐式调用

如果大家记得显示初始化和隐式初始化的概念,那么显示调用和隐式调用就很好理解其概念了。一个对象的初始化过程如下:

l  当创建Java对象时,系统会在调用该类的非静态初始化块之前,先调用父类的非静态初始化块(这个调用是隐式执行的),然后调用父类的一个户哦多个构造器执行初始化(这个调用既可以是通过super进行显式调用,也可以是隐式调用)。

l  当所有父类的非静态初始化块、一个或多个构造器一次调用完成之后,系统会调用本类的非静态初始化块、构造器执行初始化。

通过对下面实例的执行过程的讲解,大家将更好地理解Java对象的初始化过程:

class Persion{

       {

              System.out.println("Persoin的非静态初始化块");

       }

       public Persion() {

              System.out.println("Persoin的无参数的构造器");

       }

       public Persion(String name){

              this();//调用Persion的无参重载构造器

              System.out.println("Persion带有name参数的构造器,name参数:" + name);

       }

}

 

class Man extendsPersion{

       {

              System.out.println("Man的非静态初始化块");

       }

       public Man(String name) {

              super(name);

       }

       public Man(String name , int age) {

              this(name);

              System.out.println("Man带2个参数的构造器,其age:" + age);

       }

}

 

class YangManextends Man{

       {

              System.out.println("YangMan的非静态初始化块");

       }

       public YangMan() {

              super("小武" , 21);

              System.out.println("YangMan的无参构造器");

       }

       public YangMan(int weight){

              this();

              System.out.println("YangMan的带weight参数的构造器,weight参数:" + weight);

       }

}

 

public classtest01 {

       public static void main(String[] args) {

              new YangMan(55);

       }

}

运行结果如图:


运行过程为:

A.    newYangMan(55),跳转到YangMan类,在执行非静态代码块前跳转到Man类,在执行非静态代码块前跳转到Persion类;

B.     Persion的非静态初始化代码块执行,输出图中第一行。因为YangMan显式调用了Man的双参构造器(super(“小武”, 21);)Man的双参构造器又调用了自身的另一个重载构造器(this(name);)Man的单参构造器又显式调用了Persion的单参构造器(super(name);),所以Persion会先调用自身的单参构造器执行初始化;

C.     Persion的单参构造器调用了自身的无参构造器,所以输出图中的第二行,然后输出图中的第三行;

D.    之后初始化Man类,先对非静态代码块初始化,输出第四行;调用双参构造器,在其中调用了自身的单参构造器,输出第五行;输出第六行;

E.     之后初始化YangMan类,先对非静态代码块初始化,输出第七行;调用单参构造器,在其中调用了自身的无参构造器,输出第八行,输出第九行;

F.     test01的主函数中返回YangMan实例;

下面关于调用父类的哪个构造器执行初始化,做一个分类讨论:

l  子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器;

l  子类构造器执行体的第一行代码使用this()显式调用本类中重载的构造器,系统将根据this里传入的实参列表来确定本类的另一个构造器(执行本类中另一个构造器时即进入第一种情况)

l  子类构造器执行体重既没有super调用,也没有this调用,系统将会在执行子类构造器之前,显式调用父类无参数的构造器

注意super调用用于显示调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器。Super调用和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码,因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次。

2)、访问子类对象的实例变量

大家都知道子类的方法可以访问其父类的实例变量,但父类的方法不能访问子类的实例变量,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的成员变量

下面介绍的这一情况却反其道而行,父类访问了子类的实例变量。先看代码,再看结果,最后看分析:

class Persion{

       private String name = "小武";

       public Persion() {

              this.display();

       }

       public void display(){

              System.out.println(name);

       }

}

 

class Man extendsPersion{

       private String name = "灵灵";

       public Man() {

              name = "小武灵灵";

       }

       public void display(){

              System.out.println(name);

       }

}

 

public classtest01 {

       public static void main(String[] args) {

              new Man();

       }

}

看完了代码,相信大家心里都有一个此程序的输出结果,是什么呢?正确答案应该是null……下面我们从内存分配的角度来分析程序的执行过程:

A.    newMan();执行,系统为Man中的name分配内存空间;由于Man继承了Persion,所以执行Man的构造器之前程序首先初始化Persion对象;

B.     程序为Persion对象的name分配内存空间,为了区分,我们将name称之为Persion.name;程序调用Persion类的构造器执行初始化,首先将Persion.name赋值为小武,再执行this.display();但是此处有一个疑问,this代表的是Persion还是Man

C.     正确答案应该是Man。大家要记住这样一句话:当this在构造器中时,this代表正在初始化的Java对象。所以初始化过程中大家始终要记住初始化的是谁。在此程序中,Man构造器隐式调用了Persion构造器的代码,虽然this位于Persion构造器内,但这些代码实际是放在Man构造器内执行的,所以初始化对象应该是Man,此时我们可以将Persion称之为Man.Persion

D.    为了证明C的正确性,大家可以在this.display()前加一句System.out.print(this.name),查看一下输出结果。输出结果为“小武null”。大家肯定又看糊涂了,既然this指的是Man,那为什么会输出“小武”呢?因为Man.Persion 指的是在Man构造器中的Persion对象,即PersoinMan包裹在内,所以这里的this最终是位于Persion中的,它的编译时类型是Persion,而运行时类型是Man。举个例子,这里的Man是鸡蛋,Persion是蛋清,this是蛋黄,蛋黄最终会将鸡蛋进化成鸡,所以蛋黄(this)也代表了鸡蛋(Man),但蛋黄(this)却位于蛋清(Persion)中,虽然最终会进化为鸡蛋(编译时类型为Persion),但是需要吸收蛋清的养分(运行时类型是Man)

E.     继续执行Persion中的构造器this.display();代码,既然this指的是Man,则调用Man中的display()方法,而此时Man中的name,即Man.name还是系统默认值null,所以最终结果会输出null

总结,当变量的编译时类型和运行时类型不同时,通过该变量访问它所引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定;当通过该变量调用它引用的对象的实例方法时,该方法行为将由它所引用的对象来决定。

如果这一段话不好理解,我可以这样改:还有这样一个规则:变量前都存在着变量所在对象的索引,被子类重写的方法前只存在重写该方法的子类的索引。例如有这样一个继承关系:ABCD......N,其中A为顶层父类;在A对象中有变量a和被B,C,D...N重写的方法b(),则a实际上为A.a,而b()实际上为N.b()

下面通过对程序进行改写,并给出输出结果,来帮助大家更好地理解上述概念:

class Persion{

       private String name = "小武";

       public Persion() {

              System.out.println("Persion中的代码:这里输出的是this.name,即Persion.name:" + this.name);

              System.out.println("Persion中的代码:下面执行this.display(),即Man.display()");

              this.display();

       }

       public void display(){

              System.out.println("这是Persion中的display()方法:" + name);

       }

}

 

class Man extendsPersion{

       private String name = "灵灵";

       public Man() {

              name = "小武灵灵";

       }

       public void display() {

              System.out.println("这是Man中的display()方法:" + name);

       }

}

 

public classtest01 {

       public static void main(String[] args) {

              new Man();

       }

}


输出结果为:


3)、调用被子类重写的方法

大家都知道子类可以调用父类的方法,但父类不能调用子类的方法,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的方法。

下面介绍一种特殊情况,当子类重写了父类的方法之后,父类表面上只是调用属于自己的、被子类重写的方法,但随着执行context的改变,将会变成父类实际调用子类的方法。下面来看一个例子:

class Persion{

       private String name;

       public Persion() {

              this.name = getName();

       }

       public String getName(){

              return "小武";

       }

       public String toString(){

              returnname;

       }

}

 

public classtest01 extends Persion{

       private String name;

       private int age;

       public test01(String name , int age) {

              this.name = name;

              this.age = age;

       }

       @Override

       public String getName() {

              return "名字:" + name +",年龄:" + age;

       }

       public static void main(String[] args) {

              System.out.println(newtest01("灵灵", 21));

       }

}

现公布输出结果:名字:null,年龄:0

再来看执行过程:通过之前讲的父类调用子类的实例变量,相信大家会从内存分配的角度分析这一程序的执行过程,如果大家对父类调用子类的实例变量一节看的仔细,会发现我已经把结论告诉了大家,即当变量的编译时类型和运行时类型不同时,并通过该变量调用它引用的对象的实例方法时,该方法行为将由它所引用的对象来决定。

那么,我们很快就找出了以上程序的关键代码:this.name = getName(); 这行代码属于Persion,但test01继承了Persion,我们在初始化test01时调用了Persion的无参重载构造函数,这里的getName()前省略的this.,根据上一节讲的内容,大家肯定知道this指代的是test01,且getName()应该为test01.getName(),所以结论很快得出。

总结,如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象时,调用(不管是隐式调用还是显示调用)了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致了子类的重写方法访问不到子类的实例变量值的情形。所以为了避免这种结果,应该避免在父类的构造其中调用被子类重写过的方法。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值