[Java] Item 20 : 추상 클래스보다는 인터페이스를 우선하라
Effective Java를 읽고 정리한 정리본입니다.
📌 Item 20 : 추상 클래스보다는 인터페이스를 우선하라
🫧 디폴트 메서드
: 인터페이스에 있는 구현 메서드
✨ 추상 메서드와의 차이점
- 메서드 앞에 default 예약어를 붙인다.
- 구현부 {} 가 있어야 한다.
✨ 디폴트 메서드 예시
public interface Interface {
// 추상 메서드
void abstractMethodA();
void abstractMethodB();
void abstractMethodC();
// default 메서드
default int defaultMethodA(){
...
}
}
✨ 다중 구현 메커니즘
자바가 제공하는 다중 구현 메커니즘에는 인터페이스와 추상 클래스가 있다. 자바 8부터는 인터페이스도 디폴트 메서드를 제공할 수 있게 되어 둘다 인스턴스 메서드를 구현 형태로 제공할 수 있다.
차이점은 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다. 반면 인터페이스는 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 상속 여부, 종류에 상관 없이 같은 타입으로 취급된다.
🫧 인터페이스 장점
-
변경이 쉽다
-> 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다. 메서드 구현 및 implements 구문만 추가한다면 별다른 변경 없이 인터페이스 구현이 가능하다.
ex) Comparable, Iterable, AutoCloseable
-> 반면 추상 클래스는 기존 클래스 위에 추가하기 어렵다. - 믹스인 (mixin) 정의가 가능하다.
- 계층구조가 없는 타입 프레임워크를 만들 수 있다.
-> 흔히 말하는 조합 폭발 현상이 일어나지 않는다. 공통 기능을 정의해놓은 타입이 없어 매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스가 생길 염려를 하지 않아도 된다.
cf) 인터페이스 간 상속도 가능하다!
✨ 믹스인이란?
: 다중 상속의 문제를 해결하기 위한 대안으로, 기능 단위로 분리된 클래스를 생성한 다음, 필요한 클래스에 Mixin을 적용하여 기능을 조합하는 방식
- 클래스가 아니며, 단순히 메서드와 속성의 집합이다.
- 이러한 Mixin들을 다른 클래스에 적용하여 그 클래스의 기능을 확장할 수 있다.
예를 들면 Comparable은 자신을 구현한 클래스들의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스의 한 예라고 할 수 있다.
🫧 인터페이스 구현 주의사항
인터페이스의 메서드는 디폴트 메서드로 구현될 수 있다. 주의사항은 다음과 같다.
- equals와 hashCode와 같은 Object의 메서드는 디폴트 메서드로 제공해서는 안 된다.
- 인터페이스는 인스턴스 필드를 가질 수 없다
- public이 아닌 정적 멤버도 가질 수 없다 (private 정적 메서드 예외)
- 직접 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.
추가로, 상속하는 사람을 위해 @implSpec 자바독 태그를 붙여 문서화해야 하는 사실을 잊지 말자.
🫧 추상 골격 구현 클래스
- 인터페이스와 추상 클래스의 장점을 모두 취하는 방식
구현 방법은 다음과 같다.
- 인터페이스로 타입을 정의, 필요하다면 디폴트 메서드도 추가할 것 골격 구현 클래스는 나머지 메서드들까지 구현
단순히 골격 구현을 확장하는 것만으로 인터페이스를 구현할 수 있게 된다. 이를 템플릿 메서드 패턴이라고 한다.
✨ 템플릿 메서드 패턴
템플릿 메서드(Template Method) 패턴은 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다.
즉, 변하지 않는 기능(템플릿)은 상위 클래스에 만들어두고 자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 하여, 상위의 메소드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용된다.
템플릿 메소드 패턴은 상속이라는 기술을 극대화하여, 알고리즘의 뼈대를 맞추는 것에 초점을 둔다. 이미 수많은 프레임워크에서 많은 부분에 템플릿 메소드 패턴 코드가 우리도 모르게 적용되어 있다.
✨ 골격 구현을 이용한 클래스 코드 예시
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
// 다이아몬드 연산자 사용은 자바 9부터 가능
// <> == <Integer>
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i]; // 오토박싱
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // 오토언박싱
return oldVal; // 오토박싱
}
@Override public int size() {
return a.length;
}
}
}
🫧 인터페이스 구현
구조상 골격 구현을 확장하지 못하는 처지라면 인터페이스를 직접 구현해야 한다.
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달하면 골격 구현 클래스를 우회적으로 이용할 수도 있다.
🫧 골격 구현 작성
- 다른 메서드들의 구현에 사용되는 기반 메서드 선정 (골격 구현에서 추상 메서드가 될 예정)
- 기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공 (Object의 메서드 제외)
- 만약 인터페이스의 메서드가 모두 기반 메서드와 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 필요는 없다.
- 그렇지 않다면 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드드들을 작성해 넣는다.
- 필요시 public이 아닌 필드와 메서드를 추가한다.
💡 Object 메서드는 디폴트 메서드에 구현해서는 안 되므로 골격 구현 클래스에 작성하자
✨ 기반 메서드
: 객체 지향 프로그래밍(OOP)에서 상속된 클래스가 직접적으로 상속받는 메서드
- 주로 부모 클래스(또는 기반 클래스, 슈퍼 클래스)에 정의되어 있으며, 자식 클래스(또는 파생 클래스)에서 그대로 사용할 수도 있고, 오버라이딩(Overriding)을 통해 재정의할 수도 있다.
✨ 골격 구현 클래스 코드 예시
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
@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();
}
}