Favor composition over inheritance Effective Java

According to Effective Java Item 16: Favor composition over inheritance.

Inheritance is a powerful way to achieve code reuse, but it is not always the best tool for the job. Used inappropriately, it leads to fragile software. 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 the extension. Inheriting from ordinary concrete classes across package boundaries, however, is dangerous.

composition over inheritance

Unlike method invocation, inheritance violates encapsulation. In other words, a subclass depends on the implementation details of its superclass for its proper function. The super class’s implementation may change from release to release, and if it does, the subclass may break, even though its code has not been touched. As a consequence, a subclass must evolve in tandem with its superclass, unless the super class’s authors have designed and documented it specifically for the purpose of being extended.

Consider this broken class, which is trying to keep track of how many items has been added to the InstrumentedHashSet:

import java.util.Collection;
import java.util.HashSet;

public class InstrumentedHashSet<E> extends HashSet<E> {

	private static final long serialVersionUID = -7204141288621349906L;

	// 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;
	}
}

When we use the add () method to add an element to an instance of this class it works fine and returns the add count as expected. Observe the below code snippet.

public class CompositionOverInheritance {

	public static void main(String[] args) {
		InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
		
		s.add("Snap");
		s.add("Crackle");
		s.add("Pop");
		
		System.out.println("add count "+s.getAddCount()); // the output will be 3 as expected.
		
	}
}

However when we create an instance of this class and add elements using addAll () method as we have done in the below code snippet the output is not as expected.

import java.util.Arrays;

public class CompositionOverInheritance {

	public static void main(String[] args) {
		InstrumentedHashSet<String> s = new InstrumentedHashSet<>();

		s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
		
		System.out.println("add count "+s.getAddCount()); 
		
	}
}

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. The addAll method in Instrumented- HashSet added three to addCount and then invoked HashSet’s addAll implementation using super.addAll. This, in turn, invoked the add method, as overridden in InstrumentedHashSet, once for each element. Each of these three invocations added one more to addCount, for a total increase of six: each element added with the addAll method is double-counted.

The solution to the problems described above is, 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.

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.

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

// Wrapper class - uses composition in place of inheritance
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
	private int addCount = 0;

	public InstrumentedHashSet(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
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();
	}
}

Here when we create an instance of InstrumentedHashSet class and call addAll () method the addCount() method will return the correct item count as expected. Refer to the below code snippet and the output.

import java.util.Arrays;
import java.util.TreeSet;

public class CompositionOverInheritance {

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

Read more about inheritance.

Happy Learning !!

Leave a Comment