class Meal { public Meal() { System.out.println("Meal()--构造啦!"); } } class Bread { public Bread() { System.out.println("Bread()--构造啦!"); } } class Cheese { public Cheese() { System.out.println("Cheese()--构造啦!"); } } class Lettuce { public Lettuce() { System.out.println("Lettuce()--构造啦!"); } } class Lunch extends Meal { public Lunch() { System.out.println("Lunch()--构造啦!"); } } class PortableLunch extends Lunch { public PortableLunch() { System.out.println("PortableLunch()--构造啦!"); } } public class Sandwich extends PortableLunch { private Bread bread = new Bread(); private Cheese cheese = new Cheese(); private Lettuce lettuce = new Lettuce(); public Sandwich() { System.out.println("Sandwich()--构造啦!"); } public static void main(String[] args) { new Sandwich(); } }
输出结果如下:
Meal()--构造啦!
Lunch()--构造啦!
PortableLunch()--构造啦!
Bread()--构造啦!
Cheese()--构造啦!
Lettuce()--构造啦!
Sandwich()--构造啦!
父类的构造器总是在子类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个父类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。子类只能访问他自己的成员,不能访问父类的成员(父类的成员通常是private类型)。只有父类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个子类部分都必须调用构造器的原因。
上面的例子有以下顺序:
①、调用父类构造器。这个步骤会不断反复递归下去,首先是构造这种层次结构的根,然后是下一层子类,直到最底层子类。
②、按声明顺序调用成员的初始化方法。
③、调用子类构造器的主体。
构造器的调用顺序是很重要的。当进行继承时,我们已经直到父类的一切,并且可以访问父类中任何声明为public和protected的成员。这意味着在子类中,必须能假定父类的所有成员都是有效的。采用一种标准方法,构造动作一发生,那么对象所有部分的全体成员都会得到构建。但在构造器内部,必须保证使用的所有成员都已构建完毕。为达到这个要求,唯一的办法就是首先调用父类构造器。然后在进入子类构造器以后,我们在父类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构造器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有父类成员以及当前对象的成员对象均已获得正确的初始化。
再看一例:
class Insect { private int i = 9; protected int j; public Insect() { System.out.println("i=" + i + " j=" + j); j = 39; } private static int x1 = printInit("static Insect x1"); protected static int printInit(String s) { System.out.println(s); return 47; } } class Beetle extends Insect { private int k = printInit("Beetle k "); public Beetle() { System.out.println("k=" + k + " j=" + j);//这个j是父类的 j = 25; } private static int x2 = printInit("static Beetle x2"); public static void main(String[] args) { System.out.println("Beetle constructor"); Beetle beetle = new Beetle();
System.out.println();
System.out.println("Beetle constructor2");
Beetle beetle2 = new Beetle();
} }
输出结果如下:
static Insect x1 static Beetle x2 Beetle constructor i=9 j=0 Beetle k i=47 j=39
Beetle constructor2
i=9 j=0
Beetle k
k=47 j=39
在Beetle上运行java时,所发生的第一件事就是试图访问Beetle.main()(一个static方法),于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class文件中)。在对它进行加载的过程中,编译器注意到它有一个父类(这是由extends得知),于是它继续加载(父类)。不管你是否打算产生一个该父类的对象,这都要发生(请尝试将对象创建代码注释掉,以便证明这一点)。
如果该父类还有其自身的父类,那么第二个父类就会被加载,如此类推。接下来,根父类中的static初始化(在本例是Insect)即会被执行(只执行一次,以后不再执行),接着是非static成员初始化(每次访问都会执行一次),然后是下一个子类,以此类推。这种方式很重要,因为子类的static初始化可能会依赖于父类成员能否被正确初始化。
至此为止,必要的类都已经加载完毕,对象才可以创建了。首先,对象中的所有基本类型都会被设为默认值,对象引用被设为null--这是通过将对象内存设为二进制零值而一举产生的。然后,父类的构造器会被调用。在父类构造器完成之后,实例变量按其次序被初始化,最后构造器的其余部分被执行。