Inheritance is
a powerful way to achieve code reuse. It is safe to use inheritance within a
package, where the subclass and the superclass implementations are under the
control of the same programmers. It is also safe to use inheritance when
extending classes specifically designed and documented for extension (Item 17).
Inheriting from ordinary concrete classes across package boundaries, however,
is dangerous.
Unlike method
invocation, inheritance violates encapsulation.
To make this
concrete, let’s suppose we have a program that uses a HashSet. we need to query the HashSet as to how many elements have been added since
it was created. To provide this functionality, we write a HashSet variant that keeps count of the number of
attempted element insertions and exports an accessor for this count. The HashSet class contains two methods capable of adding
elements, add and addAll, so we override
both of these
methods:
// 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;
}
}
This class
looks reasonable, but it doesn’t work. Suppose we create an instance and add
three elements using the addAll method:
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap",
"Crackle", "Pop"));
We would
expect the getAddCount method to
return three at this point, but it returns six. What went wrong? Internally, HashSet’s addAll
method
is implemented on top of its add method,
although HashSet, quite
reasonably, does not document this implementation detail.
We could “fix”
the subclass by eliminating its override of the addAll
method.
While the resulting class would work, it would depend for its proper function
on the fact that HashSet’s addAll method is implemented on top of its add method.
It would be
slightly better to override the addAll method to
iterate over the specified collection, calling the add method once for each element. This would
guarantee the correct result whether or not HashSet’s addAll method were implemented atop its add method, because HashSet’s addAll
implementation
would no longer be invoked. This technique, however, does not solve all our
problems. It
amounts to reimplementing superclass methods that may or may not result in
self-use, which is difficult, time-consuming, and error-prone. Additionally, it
isn’t always possible, as some methods cannot be implemented without access to
private fields inaccessible to the subclass.
If the
superclass acquires a new method in a subsequent release and you have the bad
luck to have given the subclass a method with the same signature and a
different return type, your subclass will no longer compile
Luckily, there
is a way to avoid all of the problems described earlier. Instead of extending
an existing class, give your new class a private field that references an
instance of the existing class. This design is called composition because the existing class becomes a component of the new one.
Each instance method in the new class invokes the corresponding method on the
contained instance of the existing class and returns the results. This is known
as forwarding, and the methods in the new class are known
as forwarding methods. The resulting class will be
rock solid,
with no dependencies on the implementation details of the existing class. Even
adding new methods to the existing class will have no impact on the new class.
To make this concrete, here’s a replacement for InstrumentedHashSet
that
uses the composition-and-forwarding approach. Note that the implementation is
broken into two pieces, the class itself and a reusable forwarding class, which contains all of the forwarding methods
and nothing else:
// 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 += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// Reusable forwarding class
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(); }
}
The InstrumentedSet class is known as a wrapper class because each InstrumentedSet
instance
contains (“wraps”) another Set instance. This
is also known as the Decorator pattern
[Gamma95, p. 175], because the Instrumented-
Set class
“decorates” a set by adding instrumentation. Sometimes the combination of
composition and forwarding is loosely referred to as delegation.
Technically
it’s not delegation unless the wrapper object passes itself to the wrapped
object.
The
disadvantages of wrapper classes are few. One caveat is that wrapper classes
are not suited for use in callback
frameworks, wherein objects pass selfreferences to other objects for
subsequent invocations (“callbacks”). Because a wrapped object doesn’t know of
its wrapper, it passes a reference to itself (this) and
callbacks elude the wrapper. This is known as the SELF problem.
Inheritance is
appropriate only in circumstances where the subclass really is a subtype of the superclass. In other words, a class B should extend a class A only if an
“is-a” relationship exists between the two classes. If you are tempted to have
a class B extend a class A, ask yourself
the question: Is every B really an A?
If you cannot truthfully answer yes to this question, B should not extend A. If the
answer is no, it is often the case that B should contain
a private instance of A and expose a smaller and simpler
API: A is not an essential part of B,
merely a detail of its implementation.
There are a
number of obvious violations of this principle in the Java platform libraries.
For example, a stack is not a vector, so Stack
should
not extend Vector. Similarly, a
property list is not a hash table, so Properties
should
not extend Hashtable. In both
cases, composition would have been preferable.
To summarize,
inheritance is powerful, but it is problematic because it violates
encapsulation. It is appropriate only when a genuine subtype relationship
exists between the subclass and the superclass. Even then, inheritance may lead
to fragility if the subclass is in a different package from the superclass and
the superclass is not designed for inheritance. To avoid this fragility, use
composition and forwarding instead of inheritance, especially if an appropriate
interface to
implement a
wrapper class exists. Not only are wrapper classes more robust than subclasses,
they are also more powerful.
Reference: Effective Java 2nd Edition by Joshua Bloch