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

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

为了说明的更加具体一些,假设有一个程序使用了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. Amazon的Fire Phone之于Android开发者

    在上周Amazon也耐不住加入了手机竞争行列之中,发布了自己的Fire Phone,于是Android家族又多了一位变种成员,Android系统的碎片化程度也进一步加剧.因为工作的关系,我有幸在上个月 ...

  2. WPF之DataGrid

    1.WPF 4 DataGrid 控件(基本功能篇) 基本使用,绑定数据展示 2.WPF 4 DataGrid 控件(自定义样式篇) 定义行,列,头,单元格等样式 3.WPF 4 DataGrid 控 ...

  3. 发布FTP服务,防火墙配置

    最近需要在Web服务器上发布一下FTP,不想安装Server-U之类的,就用IIS的了,安装好后,发现外网无法连接.经过测试,发现是防火墙的问题. 查找了下关于FTP的资料,ftp server支持两 ...

  4. MySQL数据库之数据类型BOOL/BOOLEAN与TINYINT测试总结

    From: http://database.51cto.com/art/201203/323863.htm 网络上很多人咨询mysql是否提供布尔类型?MySQL数据库确实提供布尔类型,此数据类型的关 ...

  5. undercore & Backbone对AMD的支持(Require.js中如何使用undercore & Backbone)

    RequireJS填补了前端模块化开发的空缺,RequireJS遵循AMD(异步模块定义,Asynchronous Module Definition)规范,越来越多的框架支持AMD,像最近的jQue ...

  6. php socket

    socket demo代码如下 : server: server.php <?php //phpinfo(); //确保在连接客户端时不会超时 set_time_limit(0); $ip = ...

  7. 拼音 名字 排序 a-z的比较 ( sortUsingComparator )

    NSMutableArray * array = [NSMutableArrayarrayWithObjects:@"ad",@"az",@"ac&q ...

  8. Qt的零碎知识

    1.QObject是所有Qt对象的基类,他给C++的类带来了若干新的功能.使用Q_OBJECT宏能声明一个C++类为一个QObject.如: class Notepad : public QMainW ...

  9. Legolas工业自动化平台入门(一)搭建应用

    前两篇给大家介绍了TWaver家族的新面孔--Legolas工业自动化平台,通过两个应用案例钻井平台工程用车和水源地监控系统,相信大家对Legolas已经有了一定程度的了解.这几篇文章,我们会逐步介绍 ...

  10. SQL 触发器 instead of | insert

    create trigger tgr_Insert on A instead of insert as print 'Hello World' go insert into A values('100 ...