本文提供一个项目中的错误实例,提供对其观察和分析,揭示出java语言实例化一个对象具体过程,最后总结出设计Java类的一个重要规则。通过阅读本文,可以使Java程序员理解Java对象的构造过程,从而设计出更加健壮的代码。本文适合Java初学者和需要提高的Java程序员阅读。
程序掷出了一个异常
作者曾经在一个项目里面向项目组成员提供了一个抽象的对话框基类,使用者只需在子类中实现基类的一个抽象方法来画出显示数据的界面,就可使项目内的对话框具有相同的风格。具体的代码实现片断如下(为了简洁起见,省略了其他无关的代码):
public abstract class BaseDlg extends JDialog {
public BaseDlg(Frame frame, String title) {
super(frame, title, true);
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH);
this.getContentPane().add(createClientPanel(), BorderLayout.CENTER);
this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH);
}
PRivate JPanel createHeadPanel() {
... // 创建对话框头部
}
// 创建对话框客户区域,交给子类实现
protected abstract JPanel createClientPanel();
private JPanel createButtonPanel {
... // 创建按钮区域
}
}
这个类在有的代码中工作得很好,但一个同事在使用时,程序却掷出了一个NullPointerException违例!经过比较,找出了工作正常和不正常的程序的细微差别,代码片断分别如下:
一、正常工作的代码:
public class ChildDlg1 extends BaseDlg {
JTextField jTextFieldName;
public ChildDlg1() {
super(null, "Title");
}
public JPanel createClientPanel() {
jTextFieldName = new JTextField();
JPanel panel = new JPanel(new FlowLayout());
panel.add(jTextFieldName);
... // 其它代码
return panel;
}
...
}
ChildDlg1 dlg = new ChildDlg1(frame, "Title"); // 外部的调用
二、工作不正常的代码:
public class ChildDlg2 extends BaseDlg {
JTextField jTextFieldName = new JTextField();
public ChildDlg2() {
super(null, "Title");
}
public JPanel createClientPanel() {
JPanel panel = new JPanel(new FlowLayout());
panel.add(jTextFieldName);
... // 其它代码
return panel;
}
...
}
ChildDlg2 dlg = new ChildDlg2(); // 外部的调用
你看出来两段代码之间的差别了吗?对了,两者的差别仅仅在于类变量jTextFieldName的初始化时间。经过跟踪,发现在执行panel.add(jTextFieldName)语句之时,jTextFieldName确实是空值。
我们知道,Java答应在定义类变量的同时给变量赋初始值。系统运行过程中需要创建一个对象的时候,首先会为对象分配内存空间,然后在“先于调用任何方法之前”根据变量在类内的定义顺序来初始化变量,接着再调用类的构造方法。那么,在本例中,为什么在变量定义时便初始化的代码反而会出现空指针违例呢?
对象的创建过程和初始化
实际上,前面提到的“变量初始化发生在调用任何方法包括构造方法之前”这句话是不确切的,当我们把眼光集中在单个类上时,该说法成立;然而,当把视野扩大到具有继续关系的两个或多个类上时,该说法不成立。
对象的创建一般有两种方式,一种是用new操作符,另一种是在一个Class对象上调用newInstance方法;其创建和初始化的实际过程是一样的:
首先为对象分配内存空间,包括其所有父类的可见或不可见的变量的空间,并初始化这些变量为默认值,如int类型为0,boolean类型为false,对象类型为null;
然后用下述5个步骤来初始化这个新对象:
1)分配参数给指定的构造方法;
2)假如这个指定的构造方法的第一个语句是用this指针显式地调用本类的其它构造方法,则递归执行这5个步骤;假如执行过程正常则跳到步骤5;
3)假如构造方法的第一个语句没有显式调用本类的其它构造方法,并且本类不是Object类(Object是所有其它类的祖先),则调用显式(用super指针)或隐式地指定的父类的构造方法,递归执行这5个步骤;假如执行过程正常则跳到步骤5;
4)按照变量在类内的定义顺序来初始化本类的变量,假如执行过程正常则跳到步骤5;
5)执行这个构造方法中余下的语句,假如执行过程正常则过程结束。
这一过程可以从下面的时序图中获得更清楚的熟悉:
对分析本文的实例最重要的,用一句话说,就是“父类的构造方法调用发生在子类的变量初始化之前”。可以用下面的例子来证实: