ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
### [构造器调用顺序](https://lingcoder.gitee.io/onjava8/#/book/09-Polymorphism?id=%e6%9e%84%e9%80%a0%e5%99%a8%e8%b0%83%e7%94%a8%e9%a1%ba%e5%ba%8f) 在“初始化和清理”和“复用”两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。 在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为**private**,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。 下面的例子展示了组合、继承和多态在构建顺序上的作用: ~~~ // polymorphism/Sandwich.java // Order of constructor calls // {java polymorphism.Sandwich} package polymorphism; class Meal { Meal() { System.out.println("Meal()"); } } class Bread { Bread() { System.out.println("Bread()"); } } class Cheese { Cheese() { System.out.println("Cheese()"); } } class Lettuce { Lettuce() { System.out.println("Lettuce()"); } } class Lunch extends Meal { Lunch() { System.out.println("Lunch()"); } } class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } } public class Sandwich extends PortableLunch { private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ~~~ 输出: ~~~ Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich() ~~~ 这个例子用其他类创建了一个复杂的类。每个类都在构造器中声明自己。重要的类是**Sandwich**,它反映了三层继承(如果算上**Object**的话,就是四层),包含了三个成员对象。 从创建**Sandwich**对象的输出中可以看出对象的构造器调用顺序如下: 1. 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。 2. 按声明顺序初始化成员。 3. 调用派生类构造器的方法体。 构造器的调用顺序很重要。当使用继承时,就已经知道了基类的一切,并可以访问基类中任意**public**和**protected**的成员。这意味着在派生类中可以假定所有的基类成员都是有效的。在一个标准方法中,构造动作已经发生过,对象其他部分的所有成员都已经创建好。 在构造器中必须确保所有的成员都已经构建完。唯一能保证这点的方法就是首先调用基类的构造器。接着,在派生类的构造器中,所有你可以访问的基类成员都已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象置于类中)定义处初始化它们(例如,例子中的**b**、**c**和**l**)。如果遵循这条实践,就可以帮助确保所有的基类成员和当前对象的成员对象都已经初始化。 不幸的是,这不能处理所有情况,在下一节会看到。