[상속의 문제점] 하위클래스의 캡슐화를 깨뜨린다.
1. 상위 클래스의 메서드를 재정의할 때 재정의한 자신의 메서드를 사용하여 하위 클래스가 오작동할 수 있다.
예시)
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet(){}
public InstrumentedHashSet(int initCap, float loadFactor){
super(initCap, loadFactor);
}
//재정의
@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;
}
}
public class TestItem18 {
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("1", "2", "3"));//addAll
System.out.println(s.getAddCount()); // 3이 아닌 6이 반환됨..!
}
}
3을 기대했지만 6이 출력되는데, 그 원인은 HashSet의 addAll메서드가 내부적으로 자신의 add메서드를 사용하기 때문이다.
자세히 설명하면,
InstrumentedHashSet의 addAll로 addCount += c.size()로 인해 3을 더한 뒤, hashSet의 addAll을 호출한다.
hashSet의 addAll은 내부적으로 add를 사용하는데 이 때 InstrumentedHashSet에서 재정의한 add메서드를 부른다.
2. 상위클래스에 새로운 메서드가 추가되어 하위클래스에서는 허용되지 않은 원소를 상위클래스를 통해 넣을 수 있다.
만약에 다음 릴리즈에서 상위 클래스에 원소를 추가하는 메서드들이 생겼다고 해보자.
새로생긴 메서드 때문에 하위 클래스에서 허용되지 않은 원소들을 상위클래스를 통해 추가할 수 있게 된다.
즉, 하위 클래스에서 재정의 하지 않은, 새로운 메서드를 사용해 허용되지 않은 원소를 추가할 수 있게 된다!!
3. 하위 클래스에 새로운 메서드를 추가했을 때 상위클래스와 같은 시그니처를 가진 메서드가 새로 생길 수 있다.
반환 타입이 다르다면 컴파일조차 안된다.
반환 타입이 같다면 상위 클래스의 새 메서드를 재정의하게 된다.
상속보다는 컴포지션을 사용하라.
상속으로 인해 하위 클래스의 캡슐화가 깨지는 문제를 해결하기 위해서 컴포지션을 사용한다.
컴포지션이란 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 설계이다.
- 사용방법
새로운 클래스를 만들고, private 필드로 기존 클래스의 인스턴스를 참조한다.
새로운 클래스에서 기존 클래스의 메서드들을 호출하는 것을 전달(forwarding)이라 하며,
이러한 새 클래스의 메서드들을 전달 메서드(forwarding method)라 한다.
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(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;
}
}
// 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
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(); }
}
상속은 언제 해야할까?
하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 사용한다.
클래스 B가 클래스 A와 is-a 관계일때만 클래스 A를 상속한다. "B가 정말 A인가"
has-a관계라면 컴포지션을 이용
'Java' 카테고리의 다른 글
[Effective Java] 아이템20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.04.25 |
---|---|
[Effective Java] 아이템19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2022.04.25 |
[Effective Java] 아이템 17. 변경 가능성을 최소화하라. (0) | 2022.04.16 |
[Effective Java] 아이템16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.04.15 |
[Effective Java] 아이템15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.04.15 |