《Effective Java second Edition》读后而作。
为继承而设计的类,要避免在构造器中调用允许子类覆盖的方法。这是在设计可继承的类时,要注意的一点。因为,在子类实例化的时候,会先调用父类的构造器。而父类构造器如果调用了某个允许子类覆盖的方法,就可能发生问题。而这种问题往往在进行子类设计的时候,是对子类隐藏的。
比如,子类覆盖了某个方法,程序员认为该被覆盖的方法,只有在子类构造完成之后才调用。并且任意地覆盖该方法,最起码不会影响子类构造器的调用完成。但是,不良的父类设计,导致了子类在初始化工作完成之前,该方法就被父类调用。从而引发某些问题,例如,空指针异常等等。
这段话有点绕,下面用一个例子来说明。
package demo;
import java.util.Date;
public class Sample
{
public static void main(String[] args)
{
Parent p= new Parent();
System.out.println("-------------分割线---------------");
Child c = new Child();
c.allow2override();
}
}
class Parent {
Parent(){
System.out.println("Parent:构造开始");
notAllow2override();
System.out.println("这里调用了某个允许覆盖的方法,但是子类不知道我干了这件事");
allow2override();
System.out.println("Parent:构造结束");
}
public void allow2override(){
System.out.println("allow2override:这个方法在Parent够造的时候被调用");
}
public final void notAllow2override(){
System.out.println("notAllow2override:这个方法是不允许被子里覆盖的");
}
}
class Child extends Parent{
static {
System.out.println("我没有父类的源码。我使用默认无参的构造方法。我认为我的类没有问题");
}
private final Date date;
Child(){
super();//不写亦然
this.date = new Date();
}
@Override
public void allow2override()
{
System.out.println(this.date.getTime());
}
}
这段代码运行的结果是:
Parent:构造开始
notAllow2override:这个方法是不允许被子里覆盖的
这里调用了某个允许覆盖的方法,但是子类不知道我干了这件事
allow2override:这个方法在Parent够造的时候被调用
Parent:构造结束
-------------分割线---------------
我没有父类的源码。我使用默认无参的构造方法。我认为我的类没有问题
Parent:构造开始
notAllow2override:这个方法是不允许被子里覆盖的
这里调用了某个允许覆盖的方法,但是子类不知道我干了这件事
Exception in thread "main" java.lang.NullPointerException
at demo3.Child.allow2override(Sample.java:44)
at demo3.Parent.<init>(Sample.java:22)
at demo3.Child.<init>(Sample.java:38)
at demo3.Sample.main(Sample.java:12)
为什么为造成这种情况?子类在设计的时候是没有问题的,当子类构造的时候,第一步调用父类的构造器,然而父类构造器调用了一个可以覆盖的方法,但是子类覆盖的方法,依赖于子类的成功构造。因此,这个为继承而设计的父类就存在问题。
所以,在为了继承而设计类时就要注意:构造器决不能调用可被覆盖的方法,无论是直接还是间接调用。
由此引申:在为了继承而设计类时,Cloneable和Serializable接口的实现都出现了新的难点。无论是clone还是readObject,都不能调用可被覆盖的方法,无论是直接还是间接调用。