关于为何说JAVA中要慎重使用继承详解

 更新时间:2018年08月16日 08:58:47   作者:Decouple  
Java继承是面向对象的最显著的一个特征,然而下面这篇文章主要给大家介绍了关于为何说JAVA中要慎重使用继承的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧

前言

这篇文章的主题并非鼓励不使用继承,而是仅从使用继承带来的问题出发,讨论继承机制不太好的地方,从而在使用时慎重选择,避开可能遇到的坑。

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

  • 打破了封装性,子类依赖于超类的实现细节,和超类耦合。
  • 超类更新后可能会导致错误。

继承打破了封装性

关于这一点,下面是一个详细的例子(来源于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()三次,这时计数器又会再加三。

问题的根源

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

 

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

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

超类更新时可能产生错误

这一点比较好理解,主要有以下几种可能:

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

2、超类新增了方法:

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

设计可继承的类

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

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

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

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()时便抛出了空指针异常。

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

结论

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

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

相关文章

  • 基于Java利用static实现单例模式

    基于Java利用static实现单例模式

    这篇文章主要介绍了基于Java利用static实现单例模式,当在多个线程同时触发类的初始化过程的时候static不会被多次执行,下面我们一起进入文章看看具体要的原因
    2022-01-01
  • mybatis清除一级缓存的几种方式

    mybatis清除一级缓存的几种方式

    这篇文章主要介绍了mybatis清除一级缓存的几种方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • Java monitor机制使用方法解析

    Java monitor机制使用方法解析

    这篇文章主要介绍了Java monitor机制使用方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • kafka生产实践(详解)

    kafka生产实践(详解)

    下面小编就为大家带来一篇kafka生产实践(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • java如何写接口给别人调用的示例代码

    java如何写接口给别人调用的示例代码

    这篇文章主要介绍了java如何写接口给别人调用的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Spring源码之事件监听机制(实现EventListener接口方式)

    Spring源码之事件监听机制(实现EventListener接口方式)

    这篇文章主要介绍了Spring源码之事件监听机制(实现EventListener接口方式),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • SpringBoot请求转发的方式小结

    SpringBoot请求转发的方式小结

    本文主要介绍了SpringBoot请求转发的方式,一共有两大类,一种是controller控制器转发一种是使用HttpServletRequest进行转发,本文就详细的介绍一下,感兴趣的可以了解一下
    2023-09-09
  • IDEA关闭git管理,文件变成红色解决方案

    IDEA关闭git管理,文件变成红色解决方案

    在软件开发中,当一个文件夹内的Java项目启用Git版本控制,通常会导致该文件夹下所有项目同步开启Git,这种做法有助于保持项目的一致性和可追溯性,但也可能带来管理上的复杂性,如果需要解除某个项目的Git管理,可以通过IDE的设置选项进行调整
    2024-10-10
  • Java语言中flush()函数作用及使用方法详解

    Java语言中flush()函数作用及使用方法详解

    这篇文章主要介绍了Java语言中flush函数作用及使用方法详解,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • 使用React和springboot做前后端分离项目的步骤方式

    使用React和springboot做前后端分离项目的步骤方式

    这篇文章主要介绍了使用React和springboot做前后端分离项目的步骤方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08

最新评论