包装类的缺点很少。 一个警告是包装类不适合在回调框架(callback frameworks)中使用,其中对象将自我引用传递给其他对象以用于后续调用(“回调”)。 因为一个被包装的对象不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时并不记得外面的包装对象。 这被称为SELF问题[Lieberman86]。 有些人担心转发方法调用的性能影响,以及包装对象对内存占用。 两者在实践中都没有太大的影响。 编写转发方法有些繁琐,但是只需为每个接口编写一次可重用的转发类,并且提供转发类。 例如,Guava为所有的Collection接口提供转发类[Guava]。
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在“is-a”关系的情况下,B类才能继承A类。 如果你试图让B类继承A类时,问自己这个问题:每个B都是A吗? 如果你不能如实回答这个问题,那么B就不应该继承A。如果答案是否定的,那么B通常包含一个A的私有实例,并且暴露一个不同的API:A不是B的重要部分 ,只是其实现细节。
在Java平台类库中有一些明显的违反这个原则的情况。 例如,stacks实例并不是vector实例,所以Stack类不应该继承Vector类。 同样,一个属性列表不是一个哈希表,所以Properties不应该继承Hashtable类。 在这两种情况下,组合方式更可取。
如果在合适组合的地方使用继承,则会不必要地公开实现细节。由此产生的API将与原始实现联系在一起,永远限制类的性能。更严重的是,通过暴露其内部,客户端可以直接访问它们。至少,它可能导致混淆语义。例如,属性p指向Properties实例,那么 p.getProperty(key)和p.get(key)就有可能返回不同的结果:前者考虑了默认的属性表,而后者是继承Hashtable的,它则没有考虑默认属性列表。最严重的是,客户端可以通过直接修改超父类来破坏子类的不变性。在Properties类,设计者希望只有字符串被允许作为键和值,但直接访问底层的Hashtable允许违反这个不变性。一旦违反,就不能再使用属性API的其他部分(load和store方法)。在发现这个问题的时候,纠正这个问题为时已晚,因为客户端依赖于使用非字符串键和值了。
在决定使用继承来代替组合之前,你应该问自己最后一组问题。对于试图继承的类,它的API有没有缺陷呢? 如果有,你是否愿意将这些缺陷传播到你的类的API中?继承传播父类的API中的任何缺陷,而组合可以让你设计一个隐藏这些缺陷的新API。
总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。