今天在网上看到一篇文章,感觉很好,它讲到的是关于构造函数的作用以及类的构造问题,而这是初学者经常会犯甚至是有经验的程序员偶尔也会犯的错误,在此我举例总结一下,请看下面这段代码:
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(); // 外部的调用
二、工作不正常的代码:
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确实是空值.
当程序创建一个ChildDlg2的实例时,根据super(null, “Title”)语句,首先执行其父类BaseDlg的构造方法;在BaseDlg的构造方法中调用了createClientPanel()方法,这个方法是抽象方法并且被子类ChildDlg2实现了,因此,实际调用的方法是ChildDlg2中的createClientPanel()方法(因为Java里面采用“动态绑定”来绑定所有非final的方法);createClientPanel()方法使用了ChildDlg2类的实例变量jTextFieldName,而此时ChildDlg2的变量初始化过程尚未进行,jTextFieldName是null值!所以,ChildDlg2的构造过程掷出一个NullPointerException也就不足为奇了。
再来看ChildDlg1,它的jTextFieldName的初始化代码写在了createClientPanel()方法内部的开始处,这样它就能保证在使用之前得到正确的初始化,因此这段代码工作正常。
解决问题的两种方式:
通过上面的分析过程可以看出,要排除故障,最简单的方法就是要求项目组成员在继承使用BaseDlg类,实现createClientPanel()方法时,凡方法内部要使用的变量必须首先正确初始化,就象ChildDlg1一样。然而,把类变量放在类方法内初始化是一种很不好的设计行为,它最适合的地方就是在变量定义块和构造方法中。
在本文的实例中,引发错误的实质并不在ChildDlg2上,而在其父类BaseDlg上,是它在自己的构造方法中不适当地调用了一个待实现的抽象方法。
从概念上讲,构造方法的职责是正确初始化类变量,让对象进入可用状态。而BaseDlg却赋给了构造方法额外的职责。
本文实例的更好的解决方法是修改BaseDlg类:
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(createButtonPanel(), BorderLayout.SOUTH);
}
/** *//** 创建对话框实例后,必须调用此方法来布局用户界面
*/
public void initGUI() {
this.getContentPane().add(createClientPanel(), BorderLayout.CENTER);
}
private JPanel createHeadPanel() {
// 创建对话框头部
}
// 创建对话框客户区域,交给子类实现
protected abstract JPanel createClientPanel();
private JPanel createButtonPanel {
// 创建按钮区域
}
}
新的BaseDlg类增加了一个initGUI()方法,程序员可以这样使用这个类:
ChildDlg dlg = new ChildDlg();
dlg.initGUI();
dlg.setVisible(true);
总结:
类的构造方法的基本目的是正确初始化类变量,不要赋予它过多的职责。
设计类构造方法的基本规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造方法内唯一能安全调用的是基类中具有final属性的方法或者private方法(private方法会被编译器自动设置final属性)。final的方法因为不能被子类覆盖,所以不会产生问题。