对象及内存管理——父类构造器

本文深入探讨Java中构造器的调用顺序、父类构造器的隐式与显式调用、子类实例变量的访问及重写方法的调用等问题,并通过具体示例解释这些概念。

2、 父类构造器

        当创建任何Java对象时,程序总会先依次调用每个父类的非静态初始化块、构造器(总是从Object类开始)执行初始化,然后才调用本类的非静态初始化块、构造器执行初始化过程。

2、1 隐式调用与显示调用

        当调用某个类的构造器来创建Java对象时,系统总会先调用父类的非静态初始化块进行初始化。这个调用时隐式执行,而且父类的静态初始化块总是会被执行的。接着会调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显示调用,也可以是隐式调用。当所有父类的静态初始化块、构造器依次调用完成之后,系统才调用本类的非静态初始化块、构造器执行初始化,最后再返回本类的实例。

class Creature {
          {
             System.out.println("Creature的非静态初始化块");
          }
 
          public Creature() {
             System.out.println("Creature无参数构造器");
          }
          public Creature(String name) {
             this();  //使用this调用另一个无参数的构造器
             System.out.println("Creature带有name参数的构造器,name :" + name);
          }
       }


class Animal extends Creature {
      {
        System.out.println("Animal的非静态初始化块");
      }
 
      public Animal(String name) {
        super(name);
        System.out.println("Animal带有一个参数的构造器,name : " + name);
      }
 
      public Animal(String name , int age) {
        this(name);
        System.out.println("Animal带有两个参数的构造器,age : " + age);
      }
}


class Wolf extends Animal {
      {
        System.out.println("Wolf的非静态初始化块");
      }
  
      public Wolf() {
        super("狼" , 10);
        System.out.println("Wolf无参数构造器");
      }
 
      public Wolf(double weight) {
        this();
        System.out.println("Wolf带有weight参数的构造器,weight : " + weight);
      }
}


public class WolfTest {
      public static void main(String[] args) {
         new Wolf(7.5);
      }
}

输出结果为:
Creature的非静态初始化块
Creature无参数构造器
Creature带有name参数的构造器,name :狼
Animal的非静态初始化块
Animal带有一个参数的构造器,name : 狼
Animal带有两个参数的构造器,age : 10
Wolf的非静态初始化块
Wolf无参数构造器
Wolf带有weight参数的构造器,weight : 7.5
       Java在创建对象时,调用父类的哪个构造器执行初始化,分为以下三种情况:

  • 子类构造器执行体的第一行代码使用super显示调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的那个构造器。
  • 子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统将根据this调用里传入的实参列表来确定调用本类的另一个构造器(执行本类中的另一个构造器时进入第一种情况)。
  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
注意:super调用用于显示调用父类构造器,this调用用于显示调用本类中的另一个重载构造器。super和this调用都只能在构造器中使用,并且super调用和this调用都必须作为构造器的第一行代码,因此构造器中super调用和this调用最多只能使用其中之一,并且最多只能调用一次。

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

          子类的方法可以访问父类的实例变量,这是因为子类继承了父类也就获得了父类的成员变量和方法;但是父类的方法不能访问子类的实例变量,这是因为父类根本无法知道它将会被哪个子类继承,并且无法知道子类会增加怎么样的成员变量。

class Ancestors {
	private int i = 2;
	
	public Ancestors() {
		this.display();
	}
	
	public void display() {
		System.out.println(i);
	}
}

class Children extends Ancestors {
	private int i = 22;
	
	public Children() {
		i = 222;
	}
	
	public void display() {
		System.out.println(i);
	}
}

public class Test {
	public static void main(String[] args) {
		new Children();
	}
}

          从程序的运行过程来看:main方法中只有一行语句,当执行new Children();之后代码会调用Children里面的构造器,由于Children继承了Ancestors类,而且Children类中的构造器没有显示使用super调用父类的构造器,所以系统会自动调用Ancestors类中无参数构造器来执行。
       再从内存空间来进行分析。首先,要弄清楚的是,构造器只是负责对Java对象实例变量执行初始化,换言之就是构造器只起到了赋值的作用,在执行构造器之前,该对象所占用的内存空间已经被分配出来了,这些内存中的值都是默认的空值,即对于基本类型的变量默认空值就是0或false;对于引用类型的对象默认空值就是null。当执行new Children();时,系统先会为对象分配内存空间,此时系统内存需要要为此对象分配两块内存,它们分别用于存放Children对象的两个实例变量i,其中一个属于Ancestors类定义的实例变量i,另一个属于Children类定义的实例变量i,并且两个实例变量的值此时都为0。
       接下来程序在指定Children类的构造器之前,首先会执行Ancestors类的构造器。从表面上来看,Ancestors类的构造器只有一行代码,但是由于Ancestors类定义了实例变量i时指定了初始值2,因此构造器中应该包含如下代码:

public Ancestors() {
		i = 2;
		this.display();
	}

         但是问题出现了,此时的this代表谁?当this在构造器中,this代表正在初始化的Java对象。此时,this位于Ancestors类构造器中,但是这些代码实际处于Children类构造器中(因为Children类构造器隐式调用Ancestors类构造器)。所以,this在此时应该指的是Children对象,而不是Ancestors对象。
       当变量的编译类型和运行时类型不同时,通过该变量访问它引用的对象是实例对象时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法将由它实际所引用的对象来决定。针对本程序,编译时类型是Ancestors类(也就是声明类型),运行时类型是Children类,所有如果当程序访问this.i时,输出的值应该是2;但是执行this.display();时,则表现出Children对象的行为,也就是输出Children类的实例变量i,由于此时i还是为默认值0,所以最后整个程序输出0。

2、3 调用子类重写方法

         当子类方法重写了父类方法之后,父类表面上只是调用属于自己的方法,但是由于该方法已经被子类重写,所以随着调用上下文的改变,将会出现父类调用子类方法的情形。

class Animal {
           private String desc;
 
           public Animal() {
             this.desc = getDesc();  //②
           }
 
           public String getDesc() {
             return "Animal";
           }
 
           public String toString() {
             return desc;
           }
}




public class Wolf extends Animal {
     private String name;
     private double weight;
 
     public Wolf(String name , double weight) {
       this.name = name;  //③
       this.weight = weight;
     }
 
     public String getDesc() {
       return "Wolf[name = " + this.name + ",weight = " + this.weight + "]";
     }
 
     public static void main(String[] args) {
       System.out.println(new Wolf("狼" , 60.5));  //①
     }
}

输出结果为:
Wolf[name = null,weight = 0.0]
       理解这个程序的关键在于②行代码。表面上看此处调用的是弗雷中定义的getDesc()方法,但是在实际运行过程中,此处会变为调用被子类重写的getDesc()方法。
       从程序的执行过程来分析上述代码:程序从①行代码开始运行,也就是调用Wolf类对应的构造器来完成初始化,但是在执行③行代码之前,系统会隐式执行其父类无参数构造器,也就是②行代码。在执行②行代码是,是调用Wolf类重写的getDesc()方法,由于还没有执行③行代码,所以name的值为null,weight的值为0.0。当执行完②行代码后,系统再开始为name和weight分别进行赋值操作,最终name实例变量的值是“狼”,weight实例变量的值是60.5,desc的值为Wolf[name = null,weight = 0.0]。
       为了程序能够正确运行,将代码修改为如下所示:

class Animal {
	
	public String getDesc() {
		return "Animal";
	}
	
	public String toString() {
		return getDesc();
	}
}

public class Wolf extends Animal {
	private String name;
	private double weight;
	
	public Wolf(String name , double weight) {
		this.name = name; 
		this.weight = weight;
	}
	
	public String getDesc() {
		return "Wolf[name = " + this.name + ",weight = " + this.weight + "]";
	}
	
	public static void main(String[] args) {
		System.out.println(new Wolf("狼" , 60.5)); 
	}
}

输出结果为:
Wolf[name = 狼,weight = 60.5]
注意:如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,不管是显示调用还是隐式调用父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致出现子类的重写方法访问不到子类的实例变量的状况。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值