Effective Java를 읽고 정리한 정리본입니다.

📌 Item 18 : 상속보다는 컴포지션을 사용하라

🫧 상속의 단점

  1. 캡슐화를 깨트림
    • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있음.

✨ 코드 예시

  • 처음 생성된 이후 원소가 몇 개 더해졌는지 알 수 있는 변형 HashSet
public class InstrumentedHashSet<E> extends HashSet<E> {
  private int addCount = 0; // 추가된 원소의 수

  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개를 더했다면? getAddCount에서는 3이 아닌 6을 반환하게 된다. HashSet의 addAll 메서드가 add 메서드를 사용해 구현해서 한 원소에 대해 두 번 카운트되었기 때문이다.

  그렇다면 addAll 메서드를 다른 방식으로 재정의하면 어떨까? 주어진 컬렉션을 순회하며 원소 하나당 add 메서드를 한 번만 호출하는 것이다.

  만약 이렇게 문제를 회피하려 한다면, 상위 클래스의 메서드 동작을 다시 구현하는 것이 어려울 뿐더러, 하위 클래스에서는 접근할 수 없는 private 필드를 써야 하는 상황이라면 구현 자체가 불가능해질 것이다.

🫧 컴포지션

  • 상속의 단점을 해결하기 위해 사용 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 함.

전달 : 새 클래스의 인스턴스 메서드들은 private 필드로 참조하는 기존 클래스의 다응하는 메서드를 호출해 그 결과를 반환하는 것. 여기서 새 클래스의 메서드들은 전달 메서드 라고 부른다.

🫧 래퍼 클래스 - 상속 대신 컴포지션 사용

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

🫧 전달 클래스 코드 (재사용 가능)

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

InstrumentedSet은 HashSet의 모든 기능을 정의한 Set 인터페이스를 활용했다.