이펙티브 자바 - item 20. 추상클래스보다는 인터페이스를 우선하라
on JAVA
이펙티브 자바 - item 20. 추상클래스보다는 인터페이스를 우선하라
인터페이스 vs 추상 클래스
자바는 다중 구현 메커니즘으로 인터페이스와 추상 클래스릴 제공한다.
하지만 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 단점이 있어, 단일 상속만 지원하는 자바에서 새로운 타입을 정의하는 데 커다란 제약이 있다.
반면 인터페이스를 사용하면 여러가지 장점이 존재하는데 이에 대해 알아보자.
인터페이스 사용의 장점
- 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
클래스 선언에 implements
만 추가한 뒤 인터페이스가 요구하는 메서드만 구현하면 된다.
하지만 기존 클래스 위에 새로운 추상 클래스를 끼워넣기엔 어려움이 있다.
ex)
- 두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야함
- 위의 방식은 새로 추가된 추상 클래스의 모든 자손이 이를 강제로 상속하게 된다는 단점이 있음
- 믹스인 정의에 안성맞춤이다.
믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래 ‘주된 타입’외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줌.
ex) Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
class Fish implements Swimmable {
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
class Duck implements Flyable, Swimmable {
private final Flyable flyable = new Bird();
private final Swimmable swimmable = new Fish();
@Override
public void fly() {
flyable.fly();
}
@Override
public void swim() {
swimmable.swim();
}
}
public class Main {
public static void main(String[] args) {
Duck duck = new Duck();
duck.fly(); // Bird is flying
duck.swim(); // Fish is swimming
}
}
위의 예제에서는 Flyable과 Swimmable 인터페이스를 정의하고, Bird와 Fish 클래스에서 각각 이를 구현한다. 그리고 Duck 클래스에서는 Flyable과 Swimmable을 구현하여 Bird와 Fish의 기능을 모두 사용할 수 있도록 한다.
반면 추상 클래스로는 믹스인을 정의할 수 없다. 추상 클래스는 기존 클래스에 덧씌울 수 없고, 클래스는 두 부모를 섬길 수 없기 때문이다.
- 계층구조가 없는 타입 프레임워크를 만들 수 있다.
계층을 엄격히 구분하기 어려운 경우에도 인터페이스를 사용하면 쉽게 구현할 수 있다.
책에 나오는 예시로 가수, 작곡가와 작곡을 할 수 있는 싱어송라이터가 있다고 생각해보자. 가수와 작곡가는 계층구조가 딱히 있어보이지 않으며 작곡을 할 수 있는 가수, 없는 가수 클래스등을 각각 나눠서 작성하게 되면 클래스가 너무 많아지는 단점이 있다. (속성이 n개라면 2^n개 만큼 나눠야함)
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
- 래퍼 클래스 관용구와 함께 사용하면 인터페이스의 기능을 더욱 향상시킬 수 있다.
타입을 추상클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속뿐이다. 하지만 상속을 통해 만든 클래스는 래퍼클래스보다 활용도가 떨어지고 깨지기 쉽다.
인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 구현을 디폴트 메서드로 제공해 프로그래머의 일감을 덜어주자
대신 디폴트 메서드를 제공할 때는 상속하는 사람을위한 설명을 @implSpec
자바 독 태그를 붙혀 문서화해야한다.
인터페이스와 디폴트 메서드의 제약
- equals나 hashcode와 같은 Object의 메서드는 디폴트 메서드로 제공해서는 안된다.
- 인스턴스는 필드를 가질수 없고 public인 정적 멤버도 가질 수 없다.(private은 가능)
- 내가 만든 인터페이스가 아니면 디폴트 메서드를 추가할 수 없다(?)
정리
일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.
골격 구현은 ‘가능하면’ 인터페이스의 디폴트 메서드를 제공하는 것이 좋다.
공통점
선언 내용은 존재하지만 구현 내용은 없다(추상 메서드를 갖는다) 인스턴스로 생성할 수 없다.
차이점
인터페이스 : 함수의 껍데기만 존재해서 구현을 강제한다. 구현 객체가 같은 동작을 하도록 보장한다. Has - A 추상 클래스: 추상 클래스를 상속 받아 기능을 이용하고 추가시킨다. Is - A
Composition을 사용하자
기존 클래스(InstrumentedHashSetWithComposition)를 확장하는 대신, 새로운 클래스(ForwardingSet)를 만들고 private 필드로 기존 클래스의 인스턴스(s)를 참조하게 하자
기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(composition)이라 한다.
```java 기존 클래스 public class InstrumentedHashSetWithComposition
public InstrumentedHashSetWithComposition(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;
}
}
아래와 같은
```java 새 클래스
// 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
//set 인스턴스를 인수로 받는 생성자
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();
}
public int size() {
return s.size();
}
public Iterator<E> iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
InstrumentedHashSetWithComposition
은 HashSet
의 모든 기능을 정의한 Set 인터페이스를 활용해 설계되어 견고하고 아주 유연하다.
구체적으로는 Set 인터페이스를 구현했고, Set의 인스턴스를 인수로 받는 생성자를 하나 제공한다.
임의의 Set에 계측 기능(getAddCount() 메서드)을 덧씌워 새로운 Set으로 만드는 것이 이 클래스의 핵심이다.