欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java构造器的调用顺序

程序员文章站 2022-05-14 09:28:16
...

基类的构造器总是在派生类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用.这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确的构造.派生类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型).只有基类的构造器才具有恰当的权限来对自己的元素进行初始化.因此,必须让所有的构造器都得到调用,否则就不可能正确构造完整对象.这正是编译器为什么要强制每个派生类部分都必须调用构造器的原因.在派生类的构造器主体中,如果没有明确指定调用某个基类的构造器,它就会"默默"的调用默认构造器.如果不存在默认构造器,编译器就会报错.

下面举出一个例子,展示组合,继承以及多态在构造顺序上的作用.

//: polymorphism/Sandwich.java
//构造器的调用顺序验证

package polymorphism;

class Meal {
    Meal() {
        System.out.println("Meal()");
    }
}

class Bread {
    Bread() {
        System.out.println("Bread()");
    }
}

class Cheese {
    Cheese() {
        System.out.println("Cheese()");
    }
}

class Lettuc {
    Lettuc() {
        System.out.println("Lettuc()");
    }
}

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 Lettuc l = new Lettuc();
    public Sandwich() {
        System.out.println("Sandwich()");
    }

    public static void main(String[] args) {
        new Sandwich();
    }
}

 output:

java构造器的调用顺序

从输出结果我们可以看到这个对象调用构造器要遵照下面的顺序:

1)调用基类构造器.这个步骤会不断的反复递归下去,首先是构造这个层次的根基类,然后是下一层派生类,直至最底层的派生类.

2)按声明顺序调用成员的初始化方法

3)调用派生类构造器的主体


构造器的调用顺序是很重要的.当进行继承时,我们已经知道基类的一切,并且可以访问基类中任意声明为publicprotected的成员.这意味着在派生类中,必须假定基类的所有成员都是有效的.一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建.然而,在构造器内部,我们必须确保所要使用的成员都已经构造完毕.为确保这一目的,惟一的方法就是首先调用基类构造器.那么在进入派生类构造器时,在基类中可供我们访问的成员都已得到初始化.此外,知道构造器中所有成员都有效也是因为,当成员对象在类内进行定义的时候(比如上例中的b,c,l),只要有可能,就应该对它们进行初始化.若遵循这一规则,那么就能保证所有基类成员以为当前对象的成员对象都被初始化了.


不过这种做法并不适用于所有情况,这一点我们再举另一个例子.

构造器内部的多态方法的行为

如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法会发生什么情况呢?

在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类还是那个类的派生类.

如果在调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义.然而,这个调用的结果可能难以预料,因为被覆盖的方法在对象被完全构造之前就会被调用.

从概念上讲,构造器的工作实际上是创建对象.在任何构造器的内部,整个对象可能只是部分形成--我们只知道基类对象已经进行初始化.如果构造器只是在构造对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类派生的,那么派生部分在当前构造器正在调用的时候仍然是没有初始化的.然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用派生类的方法.如果我们在构造器内部这样做,那么就可能会调用某个方法,这个方法所操纵的成员可能还未进行初始化.


//: polymorphism/PolyConstructor.java

package polymorphism;

class Glyph {
    void draw() {
        System.out.println("Glyph.draw()");
    }

    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }

    @Override
    void draw() {
        System.out.println("RoundGlyph,draw(), radius = " + radius);
    }
}

public class PolyConstructor {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

output:

java构造器的调用顺序

Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的.但是Glyph的构造器会调用这个方法,结果导致了对RoundGlyph.draw()方法的调用,这看起来似乎是我们的目的.但是看输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius并不是默认初始化值1,而是0.

从此例中我们重新定义初始化的实际过程:

1)在其他任何事物发生前,将分给对象的存储空间初始化为二进制的零

2)如前所述那样调用基类构造器.此时,调用覆盖后的draw()方法,由于步骤1的缘故,我们此时会发现radius值为0

3)按照声明的顺序调用成员的初始化方法

4)调用派生类的构造器主体


所以上例步骤为:

初始化RoundGlyph对象为二进制0(radius=0)   -->    调用基类Glyph的构造函数(调用覆盖的方法draw(),输出radius为0)  -->   初始化成员(radius=1)    --> 调用RoundGlyph的构造函数(radius=5)


因此,编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法.

在构造器中唯一能安全调用的那些方法是基类中的final方法,(适用于private方法,它们自动属于final方法).这些方法不能被覆盖,因此就不会上述问题.