概述

大多数好的设计者象躲避瘟疫一样来避免使用实现继承(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如何解决脆弱基类(基类被冻结)问题相关推荐

  1. java技术难点_Java核心技术第四章----对象与类重难点总结

    一.类之间的关系 类和类之间的关系,耦合度从高到低: is -a.继承.实现 has-a.组合.聚合.关联 user-a.依赖. 要求是:高内聚.低耦合. 继承("is-a") 继 ...

  2. java thread 头文件_Linux 下 C++ 的多线程基类 - Thread

    在原来的基础上,进行了加强,使之进一步接近 Java 中 Thread 和 Runnable 的用法. 下面是我写的基类,把代码保存在名为 Thread.h 的头文件中. ============== ...

  3. java 永久代_Java新生代、老生代和永久代详解

    前言: 还是面试经常被q,小结一下 image.png JVM中的堆一般分为三部分,新生代.老年代和永久代. 1 新生代 主要是用来存放新生的对象.一般占据堆空间的1/3,由于频繁创建对象,所以新生代 ...

  4. java永久区_Java方法区和永久代

    目前有三大Java虚拟机:HotSpot,oracle JRockit,IBM J9. JRockit是oracle发明的,用于其WebLogic服务器,IBM JVM是IBM发明的用于其Websph ...

  5. java exception 乱码_java如何解决乱码问题

    java如何解决乱码问题 做项目经常会遇到Java中的乱码问题,那么遇到乱码问题怎么解决呢?下面小编为大家解答一下,希望能帮到大家! 分析编码与解码 编码就是将字符转为字节,解码就是就是将字节转换为字 ...

  6. java 多态 静态方法_java:从具有多态性的未知类调用静态方法

    本问题已经有最佳答案,请猛点这里访问. 我有超类Token和一些亚类,如Knight.King.Queen等. 我需要一个随机的Token类型,所以我调用这个方法: public Class rand ...

  7. java随机数语句_Java语言程序设计(七)Math类生成随机数及if语句

    Java有几种类型的选择语句,单向if语句,双向if语句,嵌套if语句,switch语句和条件表达式. 1.单向if语句 if(radius>=0){ area = radius*radius* ...

  8. Java深入研究_Java深入研究【1、object类】

    一.概述 Object类是所有Java类的祖先.每个类都使用 Object 作为超类.所有对象(包括数组)都实现这个类的方法. 参考英文: * Class {@code Object} is the ...

  9. java 重启线程_java 可重启线程及线程池类的设计(详解)

    了解JAVA多线程编程的人都知道,要产生一个线程有两种方法,一是类直接继承Thread类并实现其run()方法:二是类实现Runnable接口并实现其run()方法,然后新建一个以该类为构造方法参数的 ...

最新文章

  1. gcc中-pthread和-lpthread的区别
  2. JS动态生成的元素,其对应的方法不响应(比如单击事件,鼠标移动事件等)...
  3. spring的PathMatchingResourcePatternResolver基于ant通配符匹配路径遍历项目所有xml文件
  4. 带标题的图片轮询展示
  5. 中webgl解析json_WebGL蒙皮(下)
  6. arXiv与文献调研神器Connected Papers强强联合
  7. 一步一步搭建mysql_Linux——搭建PHP开发环境第一步:mysql
  8. 【转载更新】Linux工具之SED 2.应用实例
  9. java B2B2C springmvc mybatis电子商务平台源码-------zuul网关实现
  10. java rcp 教程 书,Eclipse RCP Tutorial(中文教程)
  11. 网站运维:git工具-常用命令(5):创建远程仓库、git remote 查看远程仓库、添加远程仓库、git push 推送到远程仓库、git pull 从远程仓库拉取
  12. 字节跳动小程序平台审核常见被拒情形
  13. kinect v1原理_微软官方博客揭秘Kinect工作原理
  14. [聊天机器人]:开源ChatterBot工作原理
  15. 阿里云高级技术专家白常明:边缘云的技术挑战和应用创新
  16. SpringMVC 处理器适配器详解
  17. 加速,永无止境,媲美PanDownload!
  18. iphon44s图片的缩放
  19. 2019牛客多校训练营第一场 E题 ABBA 题解
  20. 【免费咨询】 Cloudera Hadoop大数据培训:管理员、开发者、数据分析

热门文章

  1. 关于reduce中遍历2次数据的问题
  2. Lucene学习总结之五:Lucene段合并(merge)过程分析
  3. Scala入门到精通——第二十八节 Scala与JAVA互操作
  4. Scrapy保存到txt文件或者数据库里面
  5. 熔断器 Hystrix 的原理与使用
  6. 聊聊高并发(四)Java对象的表示模型和运行时内存表示
  7. MySQL事务隔离级别和Spring事务关系介绍
  8. OkHttp实现登录注册验证
  9. SharePoint 2013创建应用程序时IIS端口文件夹下没文件
  10. 22. Kotlin学习笔记 (一) 约定