告诉你在java中为什么要慎重使用继承


  

Java继承是面向对象的最显著的一个特征,今天动力节点java培训机构小编告诉大家在java中为什么要慎重使用继承,本文主要通过示例代码介绍,希望通过此文大家能够在使用java继续时慎重选择,避开可能遇到的坑。下面就随小编一起来了解一下java中为什么要慎重使用继承?


u=4195299513,2392537383&fm=11&gp=0.jpg


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


  (1)打破了封装性,子类依赖于超类的实现细节,和超类耦合。


  (2)超类更新后可能会导致错误。


  继承打破了封装性


  关于这一点,下面是一个详细的例子:


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()三次,这时计数器又会再加三。



  问题的根源


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


1568773743488424.png

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


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



  超类更新时可能产生错误,主要有以下几种可能:


  1、超类更改了已有方法的签名。会导致编译错误。


  2、超类新增了方法:


  (1)和子类已有方法的签名相同但返回类型不同,会导致编译错误。


  (2)和子类的已有方法签名相同,会导致子类无意中复写,回到了第一种情况。


  (3)和子类无冲突,但可能会影响程序的正确性。比如子类中元素加入集合必须要满足特定条件,这时候如果超类加入了一个无需检测就可以直接将元素插入的方法,程序的正确性就受到了威胁。



  设计可以用来继承的类时,应该注意:


  1、对于存在自用性的可覆盖方法,应该用文档精确描述调用细节。


  2、尽可能少的暴露受保护成员,否则会暴露太多实现细节。


    3、构造器不应该调用任何可覆盖的方法。


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


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();

 }

}


  因为超类的构造函数会在子类的构造函数之前先运行,这里超类的构造函数对someMethod()有依赖,同时someMethod()被重写,所以超类的构造函数里调用到的将是Son.someMethod(),而这时候子类还没被初始化,于是在运行到date.getTime()时便抛出了空指针异常。


  因此,如果在超类的构造函数里对可覆盖的方法有依赖,那么在继承时就可能会出错。



  结论


  继承有很多优点,但使用继承时应该慎重并多加考虑。同样用来实现代码复用的还有复合,如果使用继承和复合皆可(这是前提),那么应该优先使用复合,因为复合可以保持超类对实现细节的屏蔽,上述关于继承的缺点都可以用复合来避免。这也是所谓的复合优先于继承。


  如果使用继承,那么应该留意重写超类中存在自用性的可覆盖方法可能会出错,即使不进行重写,超类更新时也可能会引入错误。同时也应该精心设计超类,对任何相互调用的可覆盖方法提供详细文档。


u=413209375,3586319870&fm=11&gp=0.jpg


以上就是动力节点java培训机构小编介绍的“在java中为什么要慎重使用继承”的内容,希望对大家有帮助,更多java最新资讯请继续关注动力节点java培训机构官网,每天会有精彩内容分享与你。


相关免费视频教程推荐


JAVA继承视频教程下载——为什么使用继承:http://www.bjpowernode.com/xiazai/2616.html


上一篇:Java零基础小白如何学习Java
下一篇:static{}静态代码块与{}普通代码块之间的区别

开班信息