Intro::
이펙티브 자바 정리본입니다.
결론
•
일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합합니다.
•
복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 고려해보면 좋습니다.
•
골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용할하도록 하는 것이 좋습니다.
◦
인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 흔하기 때문입니다.
인터페이스와 추상클래스
인터페이스 | 추상클래스 | |
상속 | 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급합니다. | 추상 클래스의 하위 클래스가 되어야 합니다. → 계층화 |
확장 | 손쉽게 새로운 인터페이스를 구현해넣을 수 있습니다. | 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어렵다. |
믹스인 | 적합합니다. | 기존 클래스에 덧씌우기 어렵기에 합리적이지 않습니다. |
계층구조 | 계층구조가 없는 타입 프레임워크를 만들 수 있습니다. | 고도비만 계층클래스가 될 수 있다. 즉, 조합 폭발이 발생할 수 있다. |
래퍼 클래스 관용구 | 인터페이스 기능을 향상시키는 안전하고 강력한 수단이 됩니다. | 래퍼 클래스보다 활용도가 떨어지고 깨지기 쉽습니다. |
인터페이스
디폴트 메서드
•
많은 인터페이스가 equals와 hashCode와 같은 Object의 메서드를 정의하고 있지만, 이들은 디폴트 메서드로 제공해서는 안됩니다.
인터페이스 코드 예시
public interface TestInterface {
/**
* public static final이 필드값의 기본이어서 생략 가능
*/
public static final int one = 1;
int two = 2;
default void defaultFunction() {
System.out.println("default function");
}
/**
* static void publicStaticFunction()와 동일
*/
public static void publicStaticFunction() {
System.out.println("publicStaticFunction");
}
private static void privateStaticFunction() {
System.out.println("privateStaticFunction");
}
}
Java
복사
public class TestImpl implements TestInterface {
public static void main(String[] args) {
TestInterface t = new TestImpl();
t.defaultFunction();
TestInterface.publicStaticFunction();
// private access
// TestInterface.privateStaticFunction();
}
}
Java
복사
추상 골격 구현 클래스
인터페이스와 추상클래스의 장점을 모두 취할 수 있습니다.
•
인터페이스로는 타입을 정의합니다.
•
필요하면 디폴트 메서드도 제공합니다.
•
골격 구현 클래스에서 나머지 메서드들까지 구현합니다.
이를 템플릿 메서드 패턴이라고 합니다.
결론적으로 골격 구현 클래스의 경우에 인터페이스를 구현한 추상 클래스이기때문에 추상 클래스의 단점인 계층화에 대한 단점을 보완할 수 있고 실질적으로 메서드들도 정의 할 수 있기 때문에 유연한 클래스 구조를 가질 수 있습니다.
// 코드 20-1 골격 구현을 사용해 완성한 구체 클래스 (133쪽)
public class IntArrays {
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
// 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
// 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
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);
}
}
Java
복사
// 코드 20-2 골격 구현 클래스 (134-135쪽)
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();
}
}
Java
복사
질문
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달해 골격 구현 클래스를 우회적으로 사용할 수 있다는게 정확히 이해가 안간다.
상속보다는 컴포지션을 사용한 방식이라고 이해하면 됩니다. 이러한 방식을 시뮬레이트한 다중 상속(simulated multiple inheritance)라 합니다.
interface A {
void method1();
void method2();
}
abstract class AbstractA implements A {
@Override
public void method1() {
System.out.println("AbstractA method1");
}
// method2는 서브클래스에서 구현해야 함.
}
public class MyClass implements A {
// Private 내부 클래스가 골격 구현을 상속받음
private class PrivateInnerClass extends AbstractA {
@Override
public void method2() {
System.out.println("PrivateInnerClass method2");
}
}
// 내부 클래스의 인스턴스 생성
private final PrivateInnerClass delegate = new PrivateInnerClass();
@Override
public void method1() {
// method1 호출을 내부 클래스 인스턴스에 위임
delegate.method1();
}
@Override
public void method2() {
// method2 호출을 내부 클래스 인스턴스에 위임
delegate.method2();
}
}
public class Main {
public static void main(String[] args) {
A instance = new MyClass();
instance.method1(); // Prints "AbstractA method1"
instance.method2(); // Prints "PrivateInnerClass method2"
}
}
Java
복사
인터페이스에서 디폴트 메서드를 다 정의할 수 있는데 굳이 골격 추상 클래스로도 만들어 줘야하는 이유가 있을까???
1. 상태를 가지는 기능:
인터페이스의 디폴트 메서드는 상태(필드)를 가질 수 없습니다. 모든 메서드는 인스턴스 변수가 없는 상태로 동작해야 합니다. 하지만 골격 추상 클래스는 인스턴스 변수를 가질 수 있기 때문에, 상태를 필요로 하는 메서드들을 구현할 수 있습니다. 예를 들어, 내부적으로 데이터를 저장하거나 관리해야 하는 경우 골격 추상 클래스가 더 유용합니다. 때문에 골격 추상 클래스의 기반 메서드로 제공을 합니다.
2. 다중 상속 문제 해결:
인터페이스는 다중 상속이 가능하지만, 여러 인터페이스에서 동일한 디폴트 메서드가 존재할 경우 충돌이 발생할 수 있습니다. 이러한 경우 서브클래스에서 직접 해결해줘야 합니다. 반면, 추상 클래스는 단일 상속만 가능하므로 이러한 충돌 문제가 발생하지 않으며, 코드 관리가 더 쉬워질 수 있습니다.
3. 코드 재사용 및 편리함:
골격 구현을 사용하면, 공통적으로 사용되는 코드를 한 곳에 모아 관리할 수 있어 코드 재사용성을 높일 수 있습니다. 골격 구현 클래스는 인터페이스에 비해 구현이 더 복잡하거나 메서드 간에 많은 상호작용이 필요한 경우 특히 유용합니다. 또한, 골격 구현을 사용하는 서브클래스는 필요한 메서드만 오버라이드하면 되기 때문에, 디폴트 메서드만으로 해결할 수 없는 복잡한 로직을 구현할 때 편리합니다.
4. 구현 세부사항의 은닉:
추상 클래스는 특정 메서드의 구현 세부사항을 서브클래스에게 감출 수 있습니다. 추상 클래스에서 보호된(protected) 메서드를 사용하여 서브클래스에 공개하고 싶지 않은 구현 세부사항을 숨길 수 있습니다. 반면, 인터페이스의 디폴트 메서드는 기본적으로 public이기 때문에 이러한 세부사항을 감추기가 어렵습니다.
// 인터페이스 정의
public interface MyCollection<E> {
void add(E element);
boolean addAll(Collection<? extends E> c);
int size();
boolean isEmpty();
}
// 골격 추상클래스 정의
public abstract class AbstractMyCollection<E> implements MyCollection<E> {
// 수정 횟수를 추적하는 필드
protected int modCount = 0;
@Override
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E element : c) {
if (add(element)) {
modified = true;
modCount++; // 상태를 변경할 때마다 수정 횟수 증가
}
}
return modified;
}
@Override
public boolean isEmpty() {
return size() == 0;
}
}
// 리스트 구현
public class MyArrayList<E> extends AbstractMyCollection<E> {
private List<E> list = new ArrayList<>();
@Override
public void add(E element) {
list.add(element);
modCount++; // 요소가 추가될 때마다 수정 횟수 증가
}
@Override
public int size() {
return list.size();
}
}
// 집합 구현
public class MyHashSet<E> extends AbstractMyCollection<E> {
private Set<E> set = new HashSet<>();
@Override
public void add(E element) {
if (set.add(element)) {
modCount++; // 요소가 추가될 때마다 수정 횟수 증가
}
}
@Override
public int size() {
return set.size();
}
}
Java
복사
References::
이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)