이펙티브자바 아이템 18. 상속보다는 컴포지션을 사용하라에서 유명한 예시가 나온다.
public class InstrumentedHashSet<E> extends HashSet<E> {
// 추가된 원소의 수
private int addCount = 0;
public InstrumentedHashSet() {
}
@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;
}
}
InstrumentedHashSet<String> languages = new InstrumentedHashSet<>();
languages.addAll(Arrays.asList("Java", "Ruby", "Scala"));
HashSet
public class HashSet<E>
extends AbstractSet<E> ... {
...
}
AbstractSet
public abstract class AbstractSet<E> extends AbstractCollection<E> ... {
...
}
AbstractCollection
public abstract class AbstractCollection<E> implements Collection<E> {
...
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
}
위 코드를 실행하면 languages에서 getAddCount
를 했을 때 일반적으로는 3이 나올 것을 기대한다. 하지만 실제로는 6이 나온다. 그 이유는 HashSet
의 addAll
에서 반복문을 통해 아이템 개수만큼 또 add
를 호출하기 때문이다.
여기서 의문이 생겼다. 부모클래스에 메서드가 정의되어 있으면 부모 클래스의 메서드를 써야지 왜 자손클래스의 메서드를 호출하는가?
객체지향 시스템에서는 다형성 덕분에 동적으로 어떤 메서드가 실행될지 결정된다. 이때 실행할 메서드를 선택하는데 규칙이 있다.
위의 과정을 수행하기 전에, 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 동작한다.
정적 타입 언어에 속하는 자바, C++ 에서는 this, 동적타입언어에서는 주로 self를 사용한다.
코드에서 메시지를 수신한 객체는 languages
다. self 참조는 InstrumentedHashSet
의 인스턴스를 가리키도록 설정된다. languages
에 addAll()
메시지를 보낸다. languages
는 그럼 자신을 생성한 클래스인 InstrumentedHashSet
에서 addAll()
을 실행하는데 코드를 보면 super.addAll()
이 있다. 그래서 조상클래스에서 탐색하기 위해 바로 위인 HashSet
으로 간다.
HashSet
에 addAll
을 처리할 수 있는 적절한 메서드가 없기 때문에 조상클래스들을 따라가다가 AbstractCollection에서 addAll()
을 찾아낸다. 하지만 여기서 add()
를 전송하는 구문과 마주치게 된다. 이제 메서드 탐색은 self 참조가 가리키는 객체에서 시작된다. 따라서 이때 InstrumentedHashSet
의 add()
가 존재하므로 InstrumentedHashSet
의 메서드가 사용된다.
처음에는 왜 부모 클래스에서 부모클래스에 정의되어있는 메서드를 사용하지 않고 자손 클래스의 메서드를 쓰는지 이해하지 못했다. 그냥 언어에서 그렇게 정했나보다라고 막연히 생각했다. 하지만 그렇게 규칙을 정하는데는 분명히 원리가 있었을텐데 그 원리를 알 수 있게 되었다.
오브젝트 12장 - 다형성