implements 和extend

来源:互联网 发布:nginx 定义全局变量 编辑:程序博客网 时间:2024/06/05 09:03
转帖《谈谈implements 和extend 》

  转贴自:

  前几天和朋友谈到Java的问题,她突然提到implements 和extend的比较问题,我觉得这个问题是一个老生常谈的问题了,但是它又特别的重要,重要到能够影响一个人编程的思维方式,所以还是值得我们讨论讨论的(本文讨论的extend是类的扩展,而不是interface的扩展;因为接口可以扩展多个接口)。

  很多人用Java都因为它提供了丰富的开源库,却还用面向过程的思维方式编写Java程序。这样的程序不但重用性很差,而且会有很大的安全隐患。

  谈谈接口和虚拟类

  用一个形象比喻interface 和 abstract class的关系(虽然不是特别准确,但还是有助于理解的,当然也是原创的哦):我们把一个具体的类比喻成一个人,interface就是这个人的各种身份,abstract class就是这个人的父亲(当然母亲也可以啊)。

  1.  一个人可以同时具有很多种身份:,爷爷,儿子,厂长,师傅,朋友;但是他只能有一个父亲。——一个类只能有一个父亲,却能有多个身份

  2.  我们可以赋予一个人某种身份(当然也可以是几种),当他获得这个身份的时候,他就能获得某些特定的操作。比如我们给你赋予老板的身份,你就可以给你的员工加工资,炒别人鱿鱼(这是其他身份所不能办到的)等等。当然这些特定的操作不仅是你的权利,也是你的义务。——这就好比类如果实现一个接口,能够获得某些操作一样,而且必须实现接口中定义的所有方法。当然你也可以不通过接口,而在类的内部定义方法来实现特定操作。这样带来的坏处是:要重复的写类的APIs说明,而且程序的可读性和可理解性也就降低了。

  3.  在不同的地方描述一个人的时候,应该尽量用他不同的身份,而不是他的父亲。比如在商业会谈上他是A厂的厂长、在酒会上他是某某的朋友而不要随时告诉别人他自己是谁。——这样,程序的通用性就大大提高了。

  例如:在定义一个人时我们可以这样定义:

  public class Man extend Father implements Manager, Friend, Teacher{

  …

  }

  在不同声明的地方,我们可以声明使用不同声明方法:

  Manager a= new Man();// 不推荐使用Man a=new Man();

  Friend b=new Man();

  以这样的声明方式,我们就可以使用上述的接口作为参数的函数

  (如addObject(Set a) ),大大提高了代码的重用性。

  4.     父亲愿意教你他所有会的东西——可以从虚拟类继承方法的实现; 但是你的身份却不会教你怎么做:比如有一天你当了老板,你就具有一些特有权利,但却没有人会教你具体怎么做,你只能依靠自己——类只能从接口继承方法的声明,而具体方法的实现却要放到类中定义。接口定义的只是一些纯粹的声明,不包括任何的实现。

  谈谈implements的优点和extend的缺点吧。

  讨论implements和extend,其实也就是讨论封装的问题。一个安全API封装应该满足的要求是:“它应该描述它做了什么,而不是怎么做的”。很显然,一个subclass要继承superclass这个具体类,它很显然要违反上述的规定。因为在类方法的改写时我们必须要知道改写的方法在超类中是怎样被定义的,被其他哪些方法所使用,又有哪些限制条件。造成这个问题的根源正是因为extend的功能太强大了,它不但能够继承公有的实例字段(强烈推荐不要使用公有的实例字段),而且能够继承方法的具体实现。因为extend把super类的细节统统都继承过来了,在子类重写方法时就容易出现问题(如果超类定义的方法之间依赖很强的话更容易出问题)。就好比你在模仿一个你所崇拜的大牛的时候,extend就好比不但知道人家做了什么,还模仿人家是怎么做的。但是人与人之间当然会有差异性,别人怎么做的不一定适合自己啊,所有问题就产生了(这也就是道家所说的物极必反吧)。难怪Java之父曾半开玩笑的说:“如果老天能给我重来一次的机会,我一定要‘leave out extend’”。确实,相对于implements来说,extend确实有太多的缺点了。

  我们可以用接口来定义数据类型,这样可以大大增加代码的通用性(代码量也就大大减少了啊)。比如下列的定义:Set x=new HashSet(); 在以后的使用该对象的时候,我们可以忽略x具体的实现类型而使用接口所定义的方法。另外在方法定义的时候,例如:

  public Boolean addObject(Set a){ // Set 是一个interface哦,不要弄混了

  ……

  a.add(Object x);

  ……

  }

  此时,我们可以绕过a的具体实现类定义方法(实现了Set接口的类所定义的对象都可以使用该方法,比如HashSet, TreeSet, EnumSet等类实例化的对象),这样方法的可用性就大大提高了。

  在类的扩展方面,我们可以使用包装类(wrapper class)来实现接口。比如:

  谈谈extend的优点和implements的缺点

  在这里谈extend的优点并不是我鼓励大家去使用extend(恰恰相反,我认为编写Java程序大部分的精力应该放在implements上)。但是虚拟类也有它存在的理由(注意:我们强调的虚拟类,具体类最好避免使用extend)。正如我们上面所说的,由于接口只包含方法的声明,所以在实现interface的时候必须要重新定义接口中的每一个方法,当接口中方法特别多而且这些方法的实现大都相同的情况下,interface的效率是很低的。这是虚拟类就要大派用场了。当然也有一种折中的方法:使用骨架实现(skeletal implementation)类。用接口定义数据类型,用虚拟类实现该接口的方法。例如javadoc里面的JCF定义都是如此:

  public class HashSet<E>

  extends AbstractSet<E>

  implements Set<E>, Cloneable, Serializable

  此AbstractSet就是Set的骨架实现。它实现了Set的一下基本操作,当类HashSet实现Set的时候,直接extendAbstractSet就可以了,这样会大大减少方法的重复定义。总之,它可以综合implements的灵活和extend的扩展方便。

  使用虚拟类应该注意一个问题:由于一个类只能extend一次,所以extend也就应该特别的慎重。我们应该考虑A真的是B的子类型吗(即存在“is a”的关系)?如果真的满足“a B is A”,才能使用extend(必要非充分条件)。

  另外设计Interface也应该比较慎重,因为interface的扩展(演化)很不方便(这里所说的扩展是interface内部方法的扩展)。这是因为每一个实现该interface的类都必须实现该接口声明的所有方法,如果该interface一旦改变,所有与interface有关的类则都要伤筋动骨了(实际的编程中怎么可能去改这么多的类呢?所以只能硬着头皮不改,另寻它法了)。

  最后,给大家提几点关于extend和implements使用的建议(这里参照了一些大牛的书,不是我的成果拉):

  1.     尽量多的使用接口,少使用虚拟类。尽管接口在方法的实现效率上相对较低,但是可以使用骨架实现的方式来弥补此不足。

  2.     如果要使用extend,就需要编写专门用于继承的虚拟类,并给出详细的API说明。强烈建议不要extend具体类。

  3. 用复合/转发的方式来扩展类比extend方式更加安全(这里就不强调了,自己去google一下吧,如果以后有时间也可以写写)。

0 0