继承时实现代码重用的重要手段,但它并非永远是完成这项工作的最佳工具,不恰当的使用会导致程序变得很脆弱,当然,在同一个程序员的控制下,使用继承会变的非常安全。想到了很有名的一句话,你永远不知道你的用户是如何使用你写的程序的,一个程序员继承另一个程序员写的类也是同样的危险。

于方法调用不同的是,继承打破的封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有修改过。因而,子类有必要随着超类的更新而演变。

为了说明的更加具体一些,假设有一个程序使用了HashSet,想要查询它被创建以来添加了多少个元素,编写一个hashSet变量,它记录下试图插入的元素数量,并有一个访问数量的方法

/**
* 复合优先与继承
* @author weishiyao
*
* @param <E>
*/
// Broken - Inappropriate use of inheritance
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0; public InstrumentedHashSet() {
} public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
} @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);
} public int getAddCount() {
return addCount;
}
}

  看起来非常合理,但是不能正常工作,假设创建一个实例用addAll方法添加三个元素,

	public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("a", "b", "c"));
System.out.println(s.getAddCount());
}

  这时候我们期望的返回值应该是3,但是事与愿违,结果是6,因为在HashSet内部,addAll方法是机遇add方法来实现,相当于每次插入我们都计算了2次,所以结果是6.

  这个问题来源于Override这个动作,但是如果我们不覆盖现有的方法,可能认为是安全的,虽然这种做法比较安全一些,但是,也并非没有风险,如果超类在后续版本中增加了一个新的方法,并且不幸的是,这个方法如果和我们写的方法重名,要么是返回类型不同,这样整个程序将会直接down掉,如果返回类型相同,问题返回上面。

  这时候有一种方案可以避免以上的所有问题。不用扩展现有的类,而是在新的类中增加一个私有对象,它引用现有对象的一个实例,这种设计被称作为复合,因为现有的类变成了新类的一个组件,新类中的每个实力方法都可以调用被包涵的现有类实例中对应的方法,并返回它的结果,这称为转发,新类中的方法被称为转发方法。

  重新实现上面那个需求

/**
* 复合优先与继承
* @author weishiyao
*
* @param <E>
*/
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) {
this.s = s;
} @Override
public int size() {
return s.size();
} @Override
public boolean isEmpty() {
return s.isEmpty();
} @Override
public boolean contains(Object o) {
return s.contains(o);
} @Override
public Iterator<E> iterator() {
return s.iterator();
} @Override
public Object[] toArray() {
return s.toArray();
} @Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
} @Override
public boolean add(E e) {
return s.add(e);
} @Override
public boolean remove(Object o) {
return s.remove(o);
} @Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
} @Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
} @Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
} @Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
} @Override
public void clear() {
s.clear();
} }

  

/**
* 复合优先与继承
* @author weishiyao
*
* @param <E>
*/
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0; public InstrumentedSet(Set<E> s) {
super(s);
} @Override
public boolean add(E e) {
addCount++;
return super.add(e);
} @Override
public boolean addAll(Collection<? extends E> c) {
addCount++;
return super.addAll(c);
} public int getAddCount() {
return addCount;
}
}

  Set接口的存在使得InstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。除了获得健壮性外,这种设计也带来了灵活性。InstrumentedSet类实现了Set接口,并拥有单个构造器,它的参数也是Set类型,从本质上来将,这个类把一个Set转变成了另一饿Set,同时增加了计数的功能。

  前面提到的基于继承的方法只适用与单个具体的类,并且对于超类中所支持的每个构造器都要求有一个单独的构造器,于此不同的是这里的包装类可以用来包装任何Set实现,并且可以结合任何以前存在的构造器一起工作。例如:

Set<Date> s1 = new InstrumentedSet<>(new TreeSet<Date>());
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>());

  因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所有InstrumentedSet类被称为包装类。这也正是装饰器模式,因为InstrumentedSet类对一个集合进行了装饰,为它增加了计数特性。

EffectiveJava——复合优先于继承的更多相关文章

  1. EffectiveJava(16)复合优先于继承

    为什么复合优先于继承? 1.继承违反了封装原则,打破了封装性 2.继承会不必要的暴露API细节,称为隐患.比如通过直接访问底层使p.getProperty(K,V)的键值对可以不是String类型 3 ...

  2. Java复合优先于继承

    复合优于继承 继承打破了封装性(子类依赖父类中特定功能的实现细节) 合理的使用继承的情况: 在包内使用 父类专门为继承为设计,并且有很好的文档说明,存在is-a关系 只有当子类真正是父类的子类型时,才 ...

  3. 【Effective Java】6、使用复合优先于使用继承

    这个,不管是什么书都会这样说,因为常常我们并不需要继承,而只是想把类进行一定的扩展,而我们想扩展的属性或方法对应的类都有,这个时候如果两者是is a的关系,这种关系是确实存在的,那么就可以使用继承,不 ...

  4. Java - 复合模式优于继承

    继承是实现代码重用的方法之一,但使用不当则会导致诸多问题. 继承会破坏封装性,对一个具体类进行跨包访问级别的继承很危险. 即,子类依赖父类的实现细节. 如果父类的实现细节发生变化,子类则可能遭到破坏. ...

  5. <EffectiveJava>读书笔记--01继承的使用注意

    1, 父类的构造器方法中不能调用能够被子类重写的方法. 分析: 当初始化一个子类时, 首先要初始化父类, 即调用父类的构造方法; 如果父类的构造方法中调用了可被重写的其它方法, 那么此时调用的其实是该 ...

  6. Chapter 02:复合 VS 继承

    复合优先于继承,继承是实现代码重用的有力手段,并不是所有情况都适用,使用不当会导致软件变得很脆弱.与方法调用不同的是,继承打破了封装性. 总而言之,组合和继承,都能实现对类的扩展.但是要分具体情况用哪 ...

  7. 为什么说JAVA中要慎重使用继承

    JAVA中使用到继承就会有两个无法回避的缺点: 打破了封装性,迫使开发者去了解超类的实现细节,子类和超类耦合. 超类更新后可能会导致错误. 继承打破了封装性 关于这一点,下面是一个详细的例子(来源于E ...

  8. 为什么说JAVA中要慎重使用继承 C# 语言历史版本特性(C# 1.0到C# 8.0汇总) SQL Server事务 事务日志 SQL Server 锁详解 软件架构之 23种设计模式 Oracle与Sqlserver:Order by NULL值介绍 asp.net MVC漏油配置总结

    为什么说JAVA中要慎重使用继承   这篇文章的主题并非鼓励不使用继承,而是仅从使用继承带来的问题出发,讨论继承机制不太好的地方,从而在使用时慎重选择,避开可能遇到的坑. JAVA中使用到继承就会有两 ...

  9. Effective java笔记(三),类与接口

    类与接口是Java语言的核心,设计出更加有用.健壮和灵活的类与接口很重要. 13.使类和成员的可访问性最小化 设计良好的模块会隐藏起所有的实现细节,仅使用API与其他模块进行通信.这个概念称为信息隐藏 ...

随机推荐

  1. Hibernate缓存原理与策略 Hibernate缓存原理:

    Hibernate缓存原理: 对于Hibernate这类ORM而言,缓存显的尤为重要,它是持久层性能提升的关键.简单来讲Hibernate就是对JDBC进行封装,以实现内部状态的管理,OR关系的映射等 ...

  2. cocos2dx 2.x 骨骼动画优化

    本文原链接:http://www.cnblogs.com/zouzf/p/4450861.html 公司用的骨骼动画的版本貌似还停留在2.1之前的年代而已没有更新,该因各种历史原因吧,而有个大项目“一 ...

  3. SQL设置SQLServer最大连接数查询语句

    设置最大连接数 下面的T-SQL 语句可以配置SQL Server 允许的并发用户连接的最大数目. exec sp_configure 'show advanced options', 1exec s ...

  4. RadioButton 的background属性表现特征

    对于radiaoButton,应该很多人都用过.下面看一个场景     上方时radiogroup,细致观察发现左1,文字开始位置和右1文字开始位置不同,这是为何呢? 查看布局: <RadioB ...

  5. square开源vim,tmux配置在linux上使用

    首先安装需要的软件 apt-get install vim ack-grep git tmux gnome-terminal ctags xclip silversearcher-ag 这里tmux需 ...

  6. 用x86的模拟器内核记得安装intel的haxm

    Android 模拟器一直以运行速度慢著称, 本文介绍使用 Intel HAXM 技术为 Android 模拟器加速, 使模拟器运行度媲美真机, 彻底解决模拟器运行慢的问题. Intel HAXM ( ...

  7. 基于jQuery HTML5人物介绍卡片特效

    基于jQuery HTML5人物介绍卡片特效.这是一款基于jquery.material-cards插件实现的人物介绍卡片形式特效代码.效果图如下: 在线预览   源码下载 实现的代码. html代码 ...

  8. 一个简单的 ASP.NET MVC 例子演示如何在 Knockout JS 的配合下,使用 TypeScript 。

    前言 TypeScript 是一种由微软开发的自由和开源的编程语言.它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程.安德斯·海尔斯伯格,C#的首席架 ...

  9. 封装Js事件代理方法

    // 封装事件代理 function delegateEvent(element, tag, event, listener) { // 判断是否支持addEventlistener if(eleme ...

  10. [转载]在线文档预览方案-Office Web Apps

    最近在做项目时,要在手机端实现在线文档预览的功能.于是百度了一下实现方案,大致是将文档转换成pdf,然后在通过插件实现预览.这些方案没有具体实现代码,也没有在线预览的地址,再加上项目时间紧迫.只能考虑 ...