为什么说Java中要慎重使用继承

Java中使用到继承就会有两个无法回避的缺点:

1.打破了封装性,迫使开发者去了解超类的实现细节,子类和超类耦合。
2.超类更新后可能会导致错误。

继承打破了封装性

关于这一点,下面是一个详细的例子(来源于Effective Java第16条)
public class MyHashSet<E> extends HashSet<E> {
    private int addCount = 0;

public int getAddCount() {
        return addCount;
    }

@Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

@Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
}

这里自定义了一个HashSet,重写了两个方法,它和超类唯一的区别是加入了一个计数器,用来统计添加过多少个元素。

写一个测试来测试这个新增的功能是否工作:
public class MyHashSetTest {
    private MyHashSet<Integer> myHashSet = new MyHashSet<Integer>();

@Test
    public void test() {
        myHashSet.addAll(Arrays.asList(1,2,3));
       
        System.out.println(myHashSet.getAddCount());
    }
}

运行后会发现,加入了3个元素之后,计数器输出的值是6。

进入到超类中的addAll()方法就会发现出错的原因:它内部调用的是add()方法。所以在这个测试里,进入子类的addAll()方法时,数器加3,然后调用超类的addAll(),超类的addAll()又会调用子类的add()三次,这时计数器又会再加三。

问题的根源

将这种情况抽象一下,可以发现出错是因为超类的可覆盖的方法存在自用性(即超类里可覆盖的方法调用了别的可覆盖的方法),这时候如果子类覆盖了其中的一些方法,就可能导致错误。

为什么说Java中要慎重使用继承

比如上图这种情况,Father类里有可覆盖的方法A和方法B,并且A调用了B。子类Son重写了方法B,这时候如果子类调用继承来的方法A,那么方法A调用的就不再是Father.B(),而是子类中的方法Son.B()。如果程序的正确性依赖于Father.B()中的一些操作,而Son.B()重写了这些操作,那么就很可能导致错误产生。

关键在于,子类的写法很可能从表面上看来没有问题,但是却会出错,这就迫使开发者去了解超类的实现细节,从而打破了面向对象的封装性,因为封装性是要求隐藏实现细节的。更危险的是,错误不一定能轻易地被测出来,如果开发者不了解超类的实现细节就进行重写,那么可能就埋下了隐患。

超类更新时可能产生错误

这一点比较好理解,主要有以下几种可能:
•超类更改了已有方法的签名。会导致编译错误。
•超类新增了方法: ◦和子类已有方法的签名相同但返回类型不同,会导致编译错误。
◦和子类的已有方法签名相同,会导致子类无意中复写,回到了第一种情况。
◦和子类无冲突,但可能会影响程序的正确性。比如子类中元素加入集合必须要满足特定条件,这时候如果超类加入了一个无需检测就可以直接将元素插入的方法,程序的正确性就受到了威胁。


设计可继承的类

设计可以用来继承的类时,应该注意:
•对于存在自用性的可覆盖方法,应该用文档精确描述调用细节。
•尽可能少的暴露受保护成员,否则会暴露太多实现细节。
•构造器不应该调用任何可覆盖的方法。

详细解释下第三点。它实际上和 继承打破了封装性 里讨论的问题很相似,假设有以下代码:
public class Father {
    public Father() {
        someMethod();
    }

public void someMethod() {
    }
}
public class Son extends Father {
    private Date date;

public Son() {
        this.date = new Date();
    }

@Override
    public void someMethod() {
        System.out.println("Time = " + date.getTime());
    }
}

上述代码在运行测试时就会抛出NullPointerException :
public class SonTest {
    private Son    son = new Son();

@Test
    public void test() {
        son.someMethod();
    }
}

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/52489467515a1f3ed8c3200846fbf5e4.html