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调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
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]
注意:如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,不管是显示调用还是隐式调用父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致出现子类的重写方法访问不到子类的实例变量的状况。
本文深入探讨Java中构造器的调用顺序、父类构造器的隐式与显式调用、子类实例变量的访问及重写方法的调用等问题,并通过具体示例解释这些概念。
2006

被折叠的 条评论
为什么被折叠?



