Effective Java

王朝学院·作者佚名  2016-05-28
窄屏简体版  字體: 小  |  中  |  大  |  超大  

Effective java - 复合模式优于继承继承是实现代码重用的方法之一,但使用不当则会导致诸多问题。

继承会破坏封装性,对一个具体类进行跨包访问级别的继承很危险。即,子类依赖父类的实现细节。如果父类的实现细节发生变化,子类则可能遭到破坏。

举个例子,扩展HashSet,记录HashSet实例创建以来一共进行了多少次添加元素的操作。HashSet有两个添加元素的方法——add(E e)和addAll(Collection<? extends E> c)。那就覆盖这两个方法,在添加操作执行前记录次数:

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方法添加3个元素:

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

结果是: wKioL1PLrWjBf_fsAADIJ3WV64w978.jpg

导致这种结果的原因很简单。参考AbstractCollection中的add(E e)和addAll(Collection<? extends E> c),add(E e)中只有一段throw new UnsupportedOperationException();而addAll(Collection<? extends E> c)的文档注释中有这么一段话:

* <p>Note that this implementation will throw an* <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is* overridden (assuming the specified collection is non-empty).

解决这个问题的方法很简单,只需要去掉覆盖的addAll方法即可。但这样却不能解决根本问题,即HashSet的addAll方法不保证在以后的发行版本中不发生变化。即,子类实现依赖父类实现,父类发生变化时子类遭到破坏。也许我们可以覆盖父类方法重新实现,虽然解决问题,但这样费力不讨好,毫无意义。

另外,父类增加或者移除方法也会对子类产生影响。举个例子,子类扩展了某个集合类,覆盖了所有添加元素的方法,在添加元素之前对元素进行检查,让所有元素满足某个条件。如果在后来的版本中,父类增加了新的添加元素的方法,而子类没有覆盖该方法,导致非法元素添加到集合中。

反之,也有可能出现这种情况。即便父类的实现没有问题,但也可以因为子类实现不当而破坏父类的约束。比如,父类恰好增加了和子类相同签名和返回类型的方法。

于是,为了应对这些情况,可以使用复合模式(composition)代替继承。即,在一个forwarding class中增加一个private field引用现有类的实例,forwarding class中的方法对应现有类的方法。代码如下:

import java.util.Collection;import java.util.Iterator;import java.util.Set;public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator<E> iterator() { return s.iterator(); } public boolean add(E e) { return s.add(e); } public boolean remove(Object o) { return s.remove(o); } public boolean containsAll(Collection<?> c) { return s.containsAll(c); } public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } public boolean removeAll(Collection<?> c) { return s.removeAll(c); } public boolean retainAll(Collection<?> c) { return s.retainAll(c); } public Object[] toArray() { return s.toArray(); } public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean equals(Object o) { return s.equals(o); } @Override public int hashCode() { return s.hashCode(); } @Override public String toString() { return s.toString(); }}

使用时直接继承forwarding class:

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 += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; }}

forwarding class通过Set接口提供了相应方法。这种设计也有其灵活性,继承只能选择Set的某个特定实现,但使用复合我们可以选择任何接口实现。比如:

public static void main(String[] args) { InstrumentedSet<String> s = new InstrumentedSet<String>( new HashSet<String>()); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); System.out.println(s.getAddCount());}

如何从继承和复合之间做出选择?比较抽象的说法是,只有子类和父类确实存在"is-a"关系的时候使用继承,否则使用复合。或者比较实际点的说法是,如果TypeB只需要TypeA的部分行为,则考虑使用复合。

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航