Effective-Java-규칙18_추상_클래스_대신_인터페이스를_사용하라

규칙 18. 추상 클래스 대신 인터페이스를 사용하라

추상 클래스와 인터페이스의 차이

  1. 추상 클래스는 구현된 메서드를 포함할 수 있지만 인터페이스는 아니다.
  2. 추상 클래스가 규정하는 자료형을 구현하기 위해서는 추상 클래스를 반드시 계승해야 한다.
  3. 인터페이스는 포함된 모든 메서드를 정의하고 인터페이스가 규정하는 일반 규약을 지키기만 하면 되고, 그렇게 만든 클래스는 상속관계에 속할 필요가 없다.
  4. 자바는 다중 상속을 허용하지 않기 때문에, 추상 클래스를 사용하게 되면 자료형으로 사용하는데 많은 제약이 발생하게 된다.

이펙트 1. 이미 있는 클래스를 개조해서 새로운 인터페이스를 구현하도록 하는 것은 간단하다.
이펙트 2. 인터페이스는 믹스인(mixin)을 정의하는 데 이상적이다. 믹스인은 클래스가 주 자료형 이외에 추가로 구현할 수 있는 자료형으로, 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다.
예를 들어 Comparable은 어떤 클래스가 자기 객체는 다른 객체와 비교 결과에 따른 순서를 갖는다고 선언할 떄 쓰는 믹스인 인터페이스다.

  • 이런 인터페이스를 믹스인이라고 하는 이유는 자료형의 주된 기능에 선택적인 기능을 혼합할 수 있도록 하기 때문이다.
  • 추상 클래스는 믹스인 정의에는 사용할 수 없다. 클래스가 가질 수 있는 상위 클래스는 하나뿐이며, 클래스 계층에는 믹스인을 넣기 좋은 곳이 없다.

이펙트 3. 인터페이스는 비 계층적(nonhierarchical)인 자료형 프레임워크(type frame-work)를 만들 수 있도록 한다.

  • 어떤 것들은 자료형 계층(type hierarchy)으로 잘 정리되지만, 계층 안에 잘 들어맞지 않는 것들도 있다. 예를 들어, 가수를 표현하는 인터페이스와 작곡가를 표현하는 인터페이스가 있다고 해보자.
public interface Singer {
  AudioClip sing(Song s);
}
public interface Songwriter {
  Song compose(boolean hit);
}

그런데 가수 가운데는 작곡가인 사람도 있다. 추상 클래스 대신 인터페이스를 사용해 자료형을 만들었으므로, 아무런 문법적 문제 없이 Singer와 Songwriter를 동시에 구현하는 클래스를 만들 수 있따.

또 Singer와 Songwriter를 확장한 또 다른 인터페이스를 추가할 수도 있다.

public interface SingerSongwriter extends Singer, Songwriter {
  AudioClip strum();
  void actSensitive();
}

이펙트 4. 인터페이스를 사용하면 포장 클래스 숙어(wrapper class idiom)을 통해(규칙 16) 안전하면서도 강력한 기능 개선이 가능하다.

  • 추상 클래스를 사용해 자료형을 정의하면 프로그래머는 계승 이외의 수단을 사용할 수 없다. 그렇게 해서 만든 클래스는 포장 클래스보다 강력하지도 않고, 깨지기도 쉽다.

  • 인터페이스 안에는 메서드 구현을 둘 수 없지만, 그렇다고 프로그래머가 사용할 수 있는 코드를 제공할 방법이 없는 것은 아니다.

이펙트 5. 추상 골격 구현(abstract skeletal implementation) 클래스를 중요 인터페이스마다 두면, 인터페이스 장점과 추상 클래스의 장점을 결합할 수 있다.

// 골격 구현 위에서 만들어진 완전한 List 구현
static List<Integer> intArrayAsList(final int[] a) {
  if(a == null)
    throw new NullPointException();
  return new AbstractList<Integer>() {
    public Integer get(int i) {
      return a[i]; // 자동 객체화 (규칙 5)
    }
    @Override public Integer set(int i, Integer val) {
      int oldVal = a[i];
      a[i] = val; // 자동 비객체화
      return oldVal; // 자동 객체화
    }
    public int size() {
      return a.length;
    }
  };
}
  • 이미 존재하는 List 구현체가 어떤 일을 할 수 있을지 잘 보여주는 예제이다. 골격 구현의 강력함을 아주 잘 보여준다.
  • int 배열을 Integer 객체의 리스트처럼 볼 수 있도록 하는 어댑터(Adapter) 패턴 적용 사례이기도 하다.
  • int 값과 Integer 객체 사이의 자동 객체화 변환 과정 때문에 성능은 좋지 않다.
  • 정적 팩터리 메서드를 통해 반환되는 객체의 클래스가 정적 팩터리 안에 숨겨진, 외부에서 접근이 불가능한 익명 클래스라는 점에 주의하라.

골격 구현 클래스를 만드는 작업은 상대적으로 멍청해 보일 수도 있는 단순한 작업이다.

Map.entry 인터페이스에 대한 골격 구현 클래스

// 골격 구현
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
  // 기본 primitive 메서드
  public abstract K getKey();
  public abstract V getValue();

  // 변경 가능 맵에 들어갈 Entry는 이 메서드를 재정의해야 한다.
  public V setValue(V value) {
    throw new UnsupportedOperationException();
  }

  // Map.Entry.equals의 일반 규약 구현
  @Override public boolean equals(Object o) {
    if (o == this)
      return true;
    if (!(o instanceof Map.Entry))
      return false;
    Map.Entry<?, ?> arg = (Map.Entry) o;
    return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
  }
  private static boolean equals(Obejct o1, Object o2) {
    return o1 == null ? o2 == null : o1.equals(o2);
  }

  // Map.Entry.hashCode의 일반 규약
  @Override public int hashCode() {
    return hashCode(getKey()) ^ hashCode(getValue());
  }
  private static int hashCode(Object obj) {
    return obj == null ? 0 : obj.hashCode();
  }
}