Java构造器的调用顺序和多形性

在读到《Java编程思想》一文中的构造器部分时,觉得有几个知识点印象深刻,故在此记录一下,仅当随笔。

一、构造器的调用顺序

在深层继承的复杂对象中,构建器的调用顺序到底是怎样的呢。首先,我们知道衍生类(子类)在继承基础类(父类)时可以访问基础类的任何public和protected成员。这意味着在使用衍生类的时候,必须能假定基础类的所有成员都是有效的。为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构建器以后,我们在基础类能够访问的所有成员都已得到初始化。当然,在调用构造方法之前的所有类属性都会被初始化(基本数据类型初始化为0,引用类型初始化为null)。

因此,对于一个复杂的对象,构建器的调用遵照下面的顺序:

1、初始化所有类属性。

2、调用基础类构建器。这个步骤会不断重复下去,直到分级结构的根部(顶级父类),此时根部最先得到构建,然后是下一个衍生类,直到抵达最深一层的衍生类。

3、按声明顺序调用成员初始化模块。

4、调用衍生类构建器的主体。

二、构造器内部的多形性方法调用(动态绑定)

构造器的条用顺序会带来一个有趣的问题:若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?首先我们可以确定的是,动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。大家也许会认为被调用的方法应该是当前构造器所属对象的方法。但实际情况并非完全如此。若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义,但调用该方法所产生的效果可能并不如我们所愿。如下范例展示了这种错觉效果。

abstract class Brush {

	public Brush() {
		System.out.println("Brush() begin");
		description();
		draw();
		System.out.println("Brush() end");
	}
	
	void description() {
		System.out.println("我是画笔");
	}
	
	abstract void draw();
}

class RoundBrush extends Brush {

	int radius = 1;
	
	public RoundBrush(int r) {
		radius = r;
		System.out.println("构造了一支可以画半径为" + radius + "的圆形画笔");
	}
	
	@Override
	void description() {
		System.out.println("我是一支可以画半径为" + radius + "的圆形画笔");
	}
	
	@Override
	void draw() {
		System.out.println("圆形画笔开始画半径为" + radius + "的圆");
	}
}

public class TestBrush {
	public static void main(String[] args) {
		new RoundBrush(10);
	}
}

运行结果:

Brush() begin
我是一支可以画半径为0的圆形画笔
圆形画笔开始画半径为0的圆
Brush() end
构造了一支可以画半径为10的圆形画笔

由运行结果可知,构建器内部调用一个动态绑定的方法确实是被衍生类覆盖的方法,但description()和draw()输出的radius值既不是10,也不是1,而是0。这是因为基础类Brush构造方法执行的时候,衍生类RoundBrush还未实例化,但类属性的初始化已完成(radius为基本数据类型,初始化值为0),所以就出现了description()和draw()输出的radius值为0的结果。

三、总结
像上述这种不符合期望的效果或者说是错误, 很轻易被人忽略,而且要花很长的时间才能找出。因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中被final关键字修饰的那些方法(也适用于private方法,它们自动具有final关键字的效果)。因为这些方法不能被覆盖,所以不会出现上述潜在的问题。

你可能感兴趣的:(语言)