추상 클래스, 인터페이스 비교
- 공통점:
- 타입을 정의하기 위한 다중 구현 메커니즘
- 인스턴스 메소드(디폴트 메서드)를 구현 형태로 제공
- 차이점:
- 추상 클래스:
- 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스 > 새로운 타입 정의에 제약
- 인터 페이스:
- 어떤 클래스 타입에 있건 인터페이스를 정의한 클래스는 해당 인터페이스 타입 가능 취급 > 다중 상속 가능
- 추상 클래스:
인터페이스 장점 : 기존 클래스도 인터페이스 타입이 될 수 있다.
기존 클래스에도, 새로운 인터페이스를 구현하도록 하면 되기 때문에 기존 클래스도 인터페이스 타입이 될 수 있다.
- Comparable, Iterable, AutoCloeable 인터페이스가 새로 추가되었을 때, 자바의 수많은 기존 클래스가 해당 인터페이스들을 구현한 채 릴리즈 되었다.
반면 기존 클래스가 새로운 추상 클래스가 되도록 하는 것은 매우 어렵다.
그래서 인터페이스는 mixin 정의에 안성맞춤이다 (선택적인 기능 추가)
mixin 타입이란?
- 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래 ‘주된 타입’외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
타입을 계층적으로 정의하면 수많은 개념을 구조적으로 표현할 수 있지만 현실에는 계층을 엄격히 구분하기 어려운 개념이 있다.
예를 들어 가수와 작곡가를 생각해보자.
public interface Singer{
AudioClip sing(Song s);
}
public interface SongWriter{
Song compose(int chartPosition);
}
작곡도 하는 가수라면?
public interface SingerSongwriter extends Singer, Songwriter{
AudioClip strum();
void actSensitive();
}
인터페이스로 정의하면 Singer 와 Songwirter 모두를 구현해도 문제가 되지 않고 추가로 메서드를 정의해도 문제가 되지 않음.
같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 수 있음
(2의n제곱(n은 속성의 개수))(조합폭발이라부름(combinatorial explosion))
추상 골격 구현 클래스
인터페이스로는 타입을 정의하고, 골격 구현 클래스는 나머지 메서드들까지 구현 => 골격 구현을 확장하는 것만으로 인터페이스를 구현하는데 필요한 일이 거의 완료 => "템플릿 메서드 패턴"
관례상 인터페이스 이름이 Interface라면 골격 구현 클래스 이름은 AbstractInterface로 짓는다. (예. AbstractCollection)
public class IntArrays {
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i]; // 오토박싱(아이템 6)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // 오토언박싱
return oldVal; // 오토박싱
}
@Override public int size() {
return a.length;
}
};
}
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List<Integer> list = intArrayAsList(a);
Collections.shuffle(list);
System.out.println(list);
}
}
- 추상클래스처럼 구현을 도와줌
- 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유로움
구조상 골격 구현을 확장하지 못한다면 인터페이스를 직접 구현해야함
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 내부 클래스의 인스턴스에 전달하면 된다.
골격 구현 작성 방법
- 인터페이스에서 다른 메서드들의 구현에 사용되는 기반 메서드들 선정
- 기반 메서드를 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공
- Object의 메서드는 제공 X
- 기반 메서드나, 디폴트 메서드로 만들지 못한 메서드가 남아 있다면 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성
- 만약 남은 메서드가 없다면 골격 구현 클래스를 만들 필요 X
public abstract class AbstractMapEntry<K,V>implements Map.Entry<K,V> {
// 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
@Override 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<?,?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey())
&& Objects.equals(e.getValue(), getValue());
}
// Map.Entry.hashCode의 일반 규약을 구현한다.
@Override public int hashCode() {
return Objects.hashCode(getKey())
^ Objects.hashCode(getValue());
}
@Override public String toString() {
return getKey() + "=" + getValue();
}
}
조금 더 간단한 예제를 보자. 아래는 골격화를 사용하기 전이다.
public interface Student {
void eat();
void study();
void goHome();
void dailyRoutine();
}
public class Gil implements Student {
@Override
public void study() {
System.out.println("클래스와 인터페이스 공부를 해요.");
}
@Override
public void eat() {
System.out.println("학식을 먹어요");
}
@Override
public void goHome() {
System.out.println("집으로 돌아가요");
}
@Override
public void dailyRoutine() {
eat();
study();
goHome();
}
}
public class Jang implements Student {
@Override
public void study() {
System.out.println("객체의 생성과 파괴를 공부해요");
}
@Override
public void eat() {
System.out.println("학식을 먹어요");
}
@Override
public void goHome() {
System.out.println("집으로 돌아가요");
}
@Override
public void dailyRoutine() {
eat();
study();
goHome();
}
}
그럼 골격화를 사용하면 어떻게 될까?
public interface Student {
void eat();
void study();
void goHome();
void dailyRoutine();
}
public abstract class DguStudent implements Student {
@Override
public void eat() {
System.out.println("학식을 먹어요");
}
@Override
public void goHome() {
System.out.println("집으로 돌아가요");
}
@Override
public void dailyRoutine() {
eat();
study();
goHome();
}
}
public class Jang extends DguStudent implements Student {
@Override
public void study() {
System.out.println("객체의 생성과 파괴를 공부해요");
}
}
public class Gil extends DguStudent implements Student {
@Override
public void study() {
System.out.println("클래스와 인터페이스 공부를 해요.");
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
Student jang = new Jang();
Student gil = new Gil();
students.add(jang);
students.add(gil);
for (Student student : students) {
student.dailyRoutine();
}
}
}
학식을 먹어요
객체의 생성과 파괴를 공부해요
집으로 돌아가요
학식을 먹어요
클래스와 인터페이스 공부를 해요.
집으로 돌아가요
중복되는 코드도 줄일 수 있고, 한층 더 유연성을 갖게 되었다.
정리
- 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
- 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하자.
- 골격 구현은 인터페이스의 디폴트 메서드로 제공할 수 있고, 추상 골격 구현 클래스로 제공할 수 있다.
'JAVA > 이펙티브자바' 카테고리의 다른 글
이펙티브자바 Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) | 2024.10.07 |
---|---|
이펙티브자바 Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (3) | 2024.10.07 |
이펙티브자바 Item18. 상속보다는 컴포지션을 사용하라 (0) | 2024.10.07 |