`
ikeycn
  • 浏览: 146291 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

[转]java继承的弊端

阅读更多
java继承的弊端
原文链接:http://www.sunxin.org/forum/thread/20672.html#
为什么Java中继承多数是有害的

  大多数好的设计者象躲避瘟疫一样来避免使用实现继承(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能够正确的更新。
分享到:
评论

相关推荐

    Java的继承的学习笔记

    然而,继承也有其弊端: - **耦合性增强**:当父类发生改变时,可能会迫使所有子类进行相应的调整,降低了子类的独立性。 在Java中,继承中的成员访问遵循一定的规则: - **变量访问**:在子类中,访问变量时会...

    Thinking in java java源代码

    10. **枚举类型**:Java枚举类型提供了一种更安全、更强大的方式来表示有限集合的常量,避免了使用int常量的弊端,支持方法和接口。 11. **注解(Annotation)**:注解是一种元数据,可以为编译器和JVM提供额外的...

    Thinking in Java 中文第四版+习题答案

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    java 编程入门思考

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    Java初学者入门教学

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    java联想(中文)

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    Java面向对象资源整合

    继承是 Java 中的一种机制,可以让一个类和另一个类建立起继承关系。使用继承的好处可以把多个子类中重复的代码抽取到父类中了,提高代码的复用性。子类可以在父类的基础上,增加其他的功能,使子类更强大。Java 只...

    JAVA_Thinking in Java

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    Thinking in Java简体中文(全)

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    absolute java sorce

    11. **枚举**:Java的枚举类型提供了一种安全的方式来表示一组相关的值,避免了使用常量的弊端。 12. **注解**:注解(Annotation)是元数据的一种形式,提供了在编译时或运行时对代码进行信息标注的功能,常用于...

    java学习笔记

    - **限制**:Java不支持多继承,但允许单继承和多重继承。 - **示例**:错误的多继承语法。 ```java class C extends A, B() { // 错误:多继承 } ``` - **正确的单继承语法**: ```java class B extends A ...

    黑马程序员 - Java基础教学 - 08 - 面向对象(4)-多态.doc

    Java基础教学 - 面向对象(4)- 多态 在本文中,我们将讨论Java语言中的一种重要概念 —— 多态(Polymorphism)。多态是面向对象编程(OOP)中的一个核心概念,它允许我们编写更加灵活和可扩展的代码。 一、多态...

    Java基础语法之接口和多态穿拖鞋的汉子上传

    将多继承这种机制在Java中通过多实现完成了。 解决多继承的弊端: * 弊端:多继承时,当多个父类中有相同功能时,子类调用会产生不确定性。 * 为什么多实现能解决了呢?因为接口中的功能都没有方法体,由子类来...

    JAVA_Thinking in Java(中文版 由yyc,spirit整理).chm

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用...

    Java JDK 6.0學習筆記.zip

    2. **类与对象**:Java是一种面向对象的语言,因此理解和掌握类的定义、对象的创建、封装、继承和多态性是至关重要的。 3. **异常处理**:Java的异常处理机制是程序健壮性的关键,通过try-catch-finally语句块来...

    Think in Java(中文版)chm格式

    7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 ...

    Java技术在移动应用中的前景.pdf

    Java语言的面向对象性体现在它支持封装、多态性和继承三个概念。封装就是用一个自主式框架把对象的数据和方法联在一起形成一个整体。多态性就是多种表现形式,可以用“一个对外接口,多个内在实现方法”表示。继承是...

    Java集合框架常见面试题

    Collection接口是Java集合框架中的最高级接口,所有集合类型都继承自Collection接口。下面是Collection接口下的集合: * List接口下的集合:ArrayList、Vector、LinkedList * Set接口下的集合:HashSet、...

    编写高效优雅Java程序Java系列2021.pdf

    - **接口优于抽象类**:Java支持单一继承,但可以实现多个接口。当业务变化时,更适合通过增加接口来实现,而不是修改抽象类。 5. 异常处理: - **优先使用标准异常**:使用如IllegalArgumentException、...

    Thinking in Java第三版+第四版(中文版+习题答案+源代码)

    枚举类型则是Java提供的一种强类型常量,避免了传统整数常量的弊端。书中详细解释了这两者的用法和最佳实践。 九、JDBC和数据库编程 Java数据库连接(JDBC)是Java访问数据库的标准API,书中讲解了如何使用JDBC进行...

Global site tag (gtag.js) - Google Analytics