大多数好的设计者象躲避瘟疫一样来避免使用实现继承(extends 关系)。实际上80%的代码应该完全用interfaces写,而不是通过extends。“Java设计模式”一书详细阐述了怎样用接口继承代替实现继承。这篇文章描述设计者为什么会这么作。

  Extends是有害的;也许对于Charles Manson这个级别的不是,但是足够糟糕的它应该在任何可能的时候被避开。“JAVA设计模式”一书花了很大的部分讨论用interface继承代替实现继承。

  好的设计者在他的代码中,大部分用interface,而不是具体的基类。本文讨论为什么设计者会这样选择,并且也介绍一些基于interface的编程基础。

  接口(Interface)和类(Class)?

  一次,我参加一个Java用户组的会议。在会议中,Jams Gosling(Java之父)做发起人讲话。在那令人难忘的Q&A部分中,有人问他:“如果你重新构造Java,你想改变什么?”。“我想抛弃classes”他回答。在笑声平息后,它解释说,真正的问题不是由于class本身,而是实现继承(extends) 关系。接口继承(implements关系)是更好的。你应该尽可能的避免实现继承。

  失去了灵活性

  为什么你应该避免实现继承呢?第一个问题是明确的使用具体类名将你固定到特定的实现,给底层的改变增加了不必要的困难。

  在当前的敏捷编程方法中,核心是并行的设计和开发的概念。在你详细设计程序前,你开始编程。这个技术不同于传统方法的形式----传统的方式是设计应该在编码开始前完成----但是许多成功的项目已经证明你能够更快速的开发高质量代码,相对于传统的按部就班的方法。但是在并行开发的核心是主张灵活性。你不得不以某一种方式写你的代码以至于最新发现的需求能够尽可能没有痛苦的合并到已有的代码中。

  胜于实现你也许需要的特征,你只需实现你明确需要的特征,而且适度的对变化的包容。如果你没有这种灵活,并行的开发,那简直不可能。

  对于Inteface的编程是灵活结构的核心。为了说明为什么,让我们看一下当使用它们的时候,会发生什么。考虑下面的代码:

  

f(){LinkedList list = new LinkedList();//...
g( list );}g( LinkedList list ) {list.add( ... );g2( list )}

  假设一个对于快速查询的需求被提出,以至于这个LinkedList不能够解决。你需要用HashSet来代替它。在已有代码中,变化不能够局部化,因为你不仅仅需要修改f()也需要修改g()(它带有LinkedList参数),并且还有g()把列表传递给的任何代码。象下面这样重写代码:

  

f() {Collection list = new LinkedList();//...
g( list );}g( Collection list ) {list.add( ... );g2( list )}

  这样修改Linked list成hash,可能只是简单的用new HashSet()代替new LinkedList()。就这样。没有其他的需要修改的地方。

作为另一个例子,比较下面两段代码:

  

f() {Collection c = new HashSet();//...
g( c );}g( Collection c ) {for( Iterator i = c.iterator(); i.hasNext() )do_something_with( i.next() );}和f2() {Collection c = new HashSet();//...
g2( c.iterator() );}g2( Iterator i ) {while( i.hasNext() )do_something_with( i.next() );}

  g2()方法现在能够遍历Collection的派生,就像你能够从Map中得到的键值对。事实上,你能够写iterator,它产生数据,代替遍历一个Collection。你能够写iterator,它从测试的框架或者文件中得到信息。这会有巨大的灵活性。

  耦合

  对于实现继承,一个更加关键的问题是耦合---令人烦躁的依赖,就是那种程序的一部分对于另一部分的依赖。全局变量提供经典的例子,证明为什么强耦合会引起麻烦。例如,如果你改变全局变量的类型,那么所有用到这个变量的函数也许都被影响,所以所有这些代码都要被检查,变更和重新测试。而且,所有用到这个变量的函数通过这个变量相互耦合。也就是,如果一个变量值在难以使用的时候被改变,一个函数也许就不正确的影响了另一个函数的行为。这个问题显著的隐藏于多线程的程序。

  作为一个设计者,你应该努力最小化耦合关系。你不能一并消除耦合,因为从一个类的对象到另一个类的对象的方法调用是一个松耦合的形式。你不可能有一个程序,它没有任何的耦合。然而,你能够通过遵守OO规则,最小化一定的耦合(最重要的是,一个对象的实现应该完全隐藏于使用他的对象)。例如,一个对象的实例变量(不是常量的成员域),应该总是private。我意思是某段时期的,无例外的,不断的。(你能够偶尔有效地使用protected方法,但是protected实例变量是可憎的?同样的原因你应该不用get/set函数---他们对于是一个域公用只是使人感到过于复杂的方式(尽管返回修饰的对象而不是基本类型值的访问函数是在某些情况下是由原因的,那种情况下,返回的对象类是一个在设计时的关键抽象)。

  这里,我不是书生气。在我自己的工作中,我发现一个直接的相互关系在我OO方法的严格之间,快速代码开发和容易的代码实现。无论什么时候我违反中心的OO原则,如实现隐藏,我结果重写那个代码(一般因为代码是不可调试的)。我没有时间重写代码,所以我遵循那些规则。我关心的完全实用?我对干净的原因没有兴趣。

  脆弱的基类问题

  现在,让我们应用耦合的概念到继承。在一个用extends的继承实现系统中,派生类是非常紧密的和基类耦合,当且这种紧密的连接是不期望的。设计者已经应用了绰号“脆弱的基类问题”去描述这个行为。基础类被认为是脆弱的是,因为你在看起来安全的情况下修改基类,但是当从派生类继承时,新的行为也许引起派生类出现功能紊乱。你不能通过简单的在隔离下检查基类的方法来分辨基类的变化是安全的;而是你也必须看(和测试)所有派生类。而且,你必须检查所有的代码,它们也用在基类和派生类对象中,因为这个代码也许被新的行为所打破。一个对于基础类的简单变化可能导致整个程序不可操作。

让我们一起检查脆弱的基类和基类耦合的问题。下面的类extends了Java的ArrayList类去使它像一个stack来运转:

  

class Stack extends ArrayList {private int stack_pointer = 0;public void push( Object article ) {add( stack_pointer++, article );}public Object pop() {return remove( --stack_pointer );}public void push_many( Object[] articles ) {for( int i = 0; i < articles.length; ++i )push( articles[i] );}}

  甚至一个象这样简单的类也有问题。思考当一个用户平衡继承和用ArrayList的clear()方法去弹出堆栈时:

  

Stack a_stack = new Stack();a_stack.push("1");a_stack.push("2");a_stack.clear();

  这个代码成功编译,但是因为基类不知道关于stack指针堆栈的情况,这个stack对象当前在一个未定义的状态。下一个对于push()调用把新的项放入索引2的位置。(stack_pointer的当前值),所以stack有效地有三个元素-下边两个是垃圾。(Java的stack类正是有这个问题,不要用它).

  对这个令人讨厌的继承的方法问题的解决办法是为Stack覆盖所有的ArrayList方法,那能够修改数组的状态,所以覆盖正确的操作Stack指针或者抛出一个例外。(removeRange()方法对于抛出一个例外一个好的候选方法)。

  这个方法有两个缺点。第一,如果你覆盖了所有的东西,这个基类应该真正的是一个interface,而不是一个class。如果你不用任何继承方法,在实现继承中就没有这一点。第二,更重要的是,你不能够让一个stack支持所有的ArrayList方法。例如,令人烦恼的removeRange()没有什么作用。唯一实现无用方法的合理的途径是使它抛出一个例外,因为它应该永远不被调用。这个方法有效的把编译错误成为运行错误。不好的方法是,如果方法只是不被定义,编译器会输出一个方法未找到的错误。如果方法存在,但是抛出一个例外,你只有在程序真正的运行时,你才能够发现调用错误。

  对于这个基类问题的一个更好的解决办法是封装数据结构代替用继承。这是新的和改进的Stack的版本:

  

class Stack {private int stack_pointer = 0;private ArrayList the_data = new ArrayList();public void push( Object article ) {the_data.add( stack_poniter++, article );}public Object pop() {return the_data.remove( --stack_pointer );}public void push_many( Object[] articles ) {for( int i = 0; i < o.length; ++i )push( articles[i] );}}

到现在为止,一直都不错,但是考虑脆弱的基类问题,我们说你想要在stack创建一个变量, 用它在一段周期内跟踪最大的堆栈尺寸。一个可能的实现也许象下面这样:

  

class Monitorable_stack extends Stack {private int high_water_mark = 0;private int current_size;public void push( Object article ) {if( ++current_size > high_water_mark )high_water_mark = current_size;super.push( article );}publish Object pop() {--current_size;return super.pop();}public int maximum_size_so_far() {return high_water_mark;}}

  这个新类运行的很好,至少是一段时间。不幸的是,这个代码发掘了一个事实,push_many()通过调用push()来运行。首先,这个细节看起来不是一个坏的选择。它简化了代码,并且你能够得到push()的派生类版本,甚至当Monitorable_stack通过Stack的参考来访问的时候,以至于high_water_mark能够正确的更新。

转载于:https://www.cnblogs.com/heilyeah/p/6525446.html

ZT 为什么Java中继承多数是有害的?相关推荐

  1. java中parent结构_详解java中继承关系类加载顺序问题

    详解java中继承关系类加载顺序问题 实例代码: /** * Created by fei on 2017/5/31. */ public class SonClass extends ParentC ...

  2. Java中继承、接口、多态的作用详解(纯理论)

    一.继承.接口与多态的相关问题: 1. 继承的作用?好处?坏处? 继承:通过继承实现代码复用.Java中所有的类都是通过直接或间接地继程java.lang.Object类得到的.继承而得到的类称为子类 ...

  3. java 包装类可以被继承_【Java基本功】一文了解Java中继承、封装、多态的细节...

    本节主要介绍Java面向对象三大特性:继承 封装 多态,以及其中的原理. 本文会结合虚拟机对引用和对象的不同处理来介绍三大特性的原理. 继承 Java中的继承只能单继承,但是可以通过内部类继承其他类来 ...

  4. Java中继承thread类与实现Runnable接口的区别

    Java中线程的创建有两种方式: 1.  通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中 2.  通过实现Runnable接口,实例化Thread类 在实际应用中, ...

  5. Java中继承、this关键字、super关键字

    继承: 概述:当多个类中存在相同属性和行为时,将这些相同的内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承抽离出来的这个类即可. 子类:当某个类继承了另一个类的时候,可以把这个某类 ...

  6. 【Java中继承总结与练习 】

    继承 3.1 概述 3.1.1 引入 假如我们要定义如下类: 学生类,老师类和工人类,分析如下. 学生类 属性:姓名,年龄 行为:吃饭,睡觉 老师类 属性:姓名,年龄,薪水 行为:吃饭,睡觉,教书 班 ...

  7. Java中继承和实现的区别【单继承,多实现】

    继承:如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们相同的部分都放到父类中,让他们都继承这个类. 实现:如果多个类处理的目标都是一样的,但是处理的方法不同,那么就可以定义一个接口,让 ...

  8. Java中继承关系的构造函数的调用顺序

    情况一:在JAVA中,我们通常把C中的函数叫做方法.对于子类调用父类的构造方法可以做出如下解释: 子类无条件地继承父类的不含参数的构造方法.如果一个类中没有构造函数的话编译器会默认为该类创建一个无参空 ...

  9. Java中继承toString方法_java中toString方法详解

    1.Object类中定义有toString方法,用于返回对象的字符串表示(一个可以表示该对象属性内容的字符串),返回的字符串形式为 "类名@hashCode值".下面看Object ...

最新文章

  1. 教您如何查看MySQL用户权限
  2. xgboost 正则项_XGBoost入门系列第一讲
  3. Random:产生随机数的类
  4. Nginx与php的整合
  5. 计算机程序设计考试题目,计算机程序设计员理论试题(题库)
  6. linux读取环境变量替换,linux Shell脚本学习笔记二(变量和环境变量)
  7. python dictwriter_手把手教你写爬虫 |Python 采集大众点评数据采集实战
  8. 生产系统遇到的问题:producers blocked
  9. ubuntu安装pytorch镜像修改及下载
  10. 删除变量PHP之session的使用
  11. Jenkins 流水线 获取git 分支列表_某小型公司持续集成工具 jenkins 实践
  12. tensorflow分类的loss函数_tensorflow中loss函数
  13. SQL:两种获取时间类型日期部分的方法
  14. 中兴获25个5G商用合同
  15. jmeter安装插件的方法
  16. 微pe工具箱具体分区教程
  17. 基于AES的图像加密
  18. CCF201809-1 卖菜(JAVA)
  19. 格式化后如何修复损坏的JPEG文件?
  20. 生信格式 | wig(基因组浏览器绘制)

热门文章

  1. oracle大量数据删除
  2. 【《Effective C#》提炼总结】提高Unity中C#代码质量的21条准则
  3. BZOJ1084 [SCOI2005]最大子矩阵 动态规划
  4. eclipse error pages打红X的解决方法
  5. codevs 1557 热浪
  6. Front End Developer Questions 前端开发人员问题(三)JavaScript部分
  7. 项目、SVN clean的一些事
  8. GridView合并列下的行单元格的方法
  9. Gartner预测:SD-WAN将取代路由
  10. 获取一段html的内容简介