본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
선결론
- 이번 챕터에서 말하는 상속은 '클래스가 다른 클래스를 확장하는' 구현 상속을 말한다
- 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계이면서 다음의 사항을 모두 따를 때 사용해야 한다
- 상위 클래스의 패키지와 하위 클래스의 패키지가 동일할 경우
- 상위 클래스가 확장할 목적으로 설계되었고 문서화가 잘 되어 있을 경우
- 따라서 취약점이 많은 상속 대신 컴포지션과 전달을 사용하라
상속의 단점
- 상속은 캡슐화를 깨뜨린다
- 상위 클래스의 내부 구현이 변경되면 하위 클래스의 동작에 이상이 생길 수 있다
- 상위 클래스에 변경이 발생하면 하위 클래스에서도 이에 따른 변경이 발생한다
HashSet을 상속한 집합 클래스
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;
}
}
- 구체 클래스 각각을 따로 확장해야 함 -> 여기서 구체 클래스는 HashSet
- 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의
결과
public class Item18 {
public static void main(String[] args) {
// 상속을 이용한 방식
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
System.out.println(s.getAddCount()); // expected 3, but 6
}
}
- HashSet의 addAll 메서드를 호출하면 내부적으로 add 메서드를 호출하기 때문에 결과는 6이 나온다
- 기존 클래스의 내부 구현 방식의 영향을 받음
컴포지션과 전달
- 책에서는 상속 대신 컴포지션과 전달을 사용할 것을 권장한다
- 컴포지션(Composition)
- 기존 클래스를 상속(확장)하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다
- 즉, 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 설계방식
- 전달(Forwarding)
- 새로운 클래스의 인스턴스 메서드들이 private 필드로 참조하는 기존 클래스의 대응되는 메서드를 호출해 그 결과를 반환하는 방식
- 새로운 클래스의 메서드들을 전달 메서드(forwarding method)라고 한다
Set 인터페이스를 구현한 집합 클래스
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;
}
}
- 인터페이스를 구현하여 해당 인터페이스를 구현한 모든 구체 클래스에 대응할 수 있게 됨
- Set의 인스턴스를 인수로 받는 생성자를 하나 만들었음
- Set 인스턴스를 감싸고 있다는 뜻에서 래퍼 클래스라고 한다
전달 메서드만으로 이뤄진 재사용 가능한 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public boolean equals(Object obj) {
return s.equals(obj);
}
@Override
public String toString() {
return s.toString();
}
}
결과
public static void main(String[] args) {
// 컴포지션을 이용한 방식
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
InstrumentedSet<String> is = new InstrumentedSet<>(s);
is.addAll(List.of("틱", "탁탁", "펑"));
System.out.println(is.getAddCount()); // expected 3, and 3
}
- 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어남
- 기존 클래스에 새로운 메서드가 추가되더라도 영향이 없음
'책 > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 20: 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.05.09 |
---|---|
[이펙티브 자바] 아이템 19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2022.05.05 |
[이펙티브 자바] 아이템 17: 변경 가능성을 최소화하라 (0) | 2022.05.03 |
[이펙티브 자바] 아이템 16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.05.02 |
[이펙티브 자바] 아이템15: 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.04.28 |