아이템20. 추상 클래스보다는 인터페이스를 우선하라
2021-10-22 00:00:00 # Effective_Java
  • 자바 8부터 인터페이스도 default method를 제공할 수 있는 공통점 있음
  • 하지만 가장 큰 차이점은 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되는 것
    • 단일 상속만 지원하는 자바에 있어 커다란 제약이다.
    • 기존 클래스에 추상 클래스를 끼워넣기는 어렵다.
  • 반면, 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
    • 대상 타입의 주된 기능에 선택적 행위를 제공한다고 선언하는 효과
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
    • n개의 인터페이스를 상속하는 인터페이스를 만들어서 애매하게 구분하기 어려운 개념들을 혼합해 사용 가능
  • 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
  • 디폴트 메서드를 추가할 때는 javadoc 문서도 같이…

템플릿 메서드 패턴

  • 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 방법
  • 인터페이스로 타입을 정의하고, 필요하면 디폴트 메서드 몇 개도 함께 제공
    • 골격 구현 클래스는 나머지 메서드들까지 구현
  • 관례상 인터페이스 이름이 Interface 라면, 골격 구현 클래스 이름은 AbstractInterface 이다.

List 구현체를 반환하는 정적 팩터리 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.AbstractList;
import java.util.Objects;

public class TestClass {
static List<Integer> intArrayAsList(int[] a) { // Adapter 패턴이기도 하다.
Objects.requireNonNull(a);

return new AbstractList<Integer>() {
@Override
public Integer get(int index) {
return a[i];
}

@Override
public Integer set(int index, Integer value) {
int oldVal = a[index];
a[index] = value; // 오토언박싱
return oldVal; // 오토박싱
}

@Override
public int size() {
return a.length;
}
}

}
}
  • 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유롭다.
  • 구조상 골격 구현을 확장하지 못하는 처지라면 인터페이스를 직접 구현해야 한다.

골격 구현 클래스 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Map;
import java.util.Objects;

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
// setValue를 사용하려면 반드시 재정의해야함
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}

@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?, ?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey())
&& Objects.equals(e.getValue(), getValue());
}

@Override
public int hashCode() {
return Objects.hashCode(getKey())
^ Objects.hashCode(getValue());
}

@Override
public String toString() {
return getKey() + "=" + getValue();
}
}
  • Map.Entry 인터페이스나 그 하위 인터페이스로는 이 골격 구현을 제공할 수 없다.
  • 디폴트 메서드는 equals, hashCode, toString 같은 Object 메서드를 제정의할 수 없기 때문이다.
  • 단순 구현은 골격 구현의 작은 변종
  • 단순 구현은 추상 클래스가 아니기 때문에 가장 단순한 구현이며, 그대로 써도 되고 필요에 따라 확장해도 된다.

핵심 정리

  • 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
  • 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.
  • 골격 구현은 ‘가능한 한’ 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다.
    • 구현상 제약 때문에 추상 클래스로 제공하는 경우가 흔하기 때문에…