[Effective Java] 아이템19 - 상속을 고려해 설계하고 문서화하라.

아이템 18에서는 상속할 때의 주의점도 문서화해놓지 않은 ‘외부’클래스를 상속할 때의 위험을 경고했다. 그렇다면 상속을 고려한 설계와 문서화는 무엇일까?

재정의 가능한 메서드가 self-use를 어떻게 하는지 문서로 남겨야 한다.

재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다. HashSet의 addAll 처럼 (아이템18)

/**
 * {@inheritDoc}
 *
 * @implSpec
 * This implementation iterates over the specified collection, and adds
 * each object returned by the iterator to this collection, in turn.
 *
 * <p>Note that this implementation will throw an
 * {@code UnsupportedOperationException} unless {@code add} is
 * overridden (assuming the specified collection is non-empty).
 *
 * @throws UnsupportedOperationException {@inheritDoc}
 * @throws ClassCastException            {@inheritDoc}
 * @throws NullPointerException          {@inheritDoc}
 * @throws IllegalArgumentException      {@inheritDoc}
 * @throws IllegalStateException         {@inheritDoc}
 *
 * @see #add(Object)
 */
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

@implSpec 으로 해당 메서드가 무슨일을 하는지 명시는 했으나, add 메서드를 self-use한다는 설명이 없다.

반면에 AbstractCollection 의 remove 메서드를 보자.

/**
 * {@inheritDoc}
 *
 * <p>This implementation iterates over the collection looking for the
 * specified element.  If it finds the element, it removes the element
 * from the collection using the iterator's remove method.
 *
 * <p>Note that this implementation throws an
 * <tt>UnsupportedOperationException</tt> if the iterator returned by this
 * collection's iterator method does not implement the <tt>remove</tt>
 * method and this collection contains the specified object.
 *
 * @throws UnsupportedOperationException {@inheritDoc}
 * @throws ClassCastException            {@inheritDoc}
 * @throws NullPointerException          {@inheritDoc}
 */
public boolean remove(Object o) {
    ...
}

이 설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줌을 확실히 알 수 있다.

클래스를 안전하게 상속할 수 있도록 하려면 (상속만 아니었다면 기술하지 않았어야 할) 내부 구현 방식을 설명해야만 한다.

hook을 선별하여 protected 메서드 형태로 공개하는 방법도 생각해야 한다.

java.util.AbstractListremoveRange 메서드를 예로 보자.

/**
 * Removes from this list all of the elements whose index is between
 * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
 * Shifts any succeeding elements to the left (reduces their index).
 * This call shortens the list by {@code (toIndex - fromIndex)} elements.
 * (If {@code toIndex==fromIndex}, this operation has no effect.)
 *
 * <p>This method is called by the {@code clear} operation on this list
 * and its subLists.  Overriding this method to take advantage of
 * the internals of the list implementation can <i>substantially</i>
 * improve the performance of the {@code clear} operation on this list
 * and its subLists.
 *
 * <p>This implementation gets a list iterator positioned before
 * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
 * followed by {@code ListIterator.remove} until the entire range has
 * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
 * time, this implementation requires quadratic time.</b>
 *
 * @param fromIndex index of first element to be removed
 * @param toIndex index after last element to be removed
 */
protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}

removeRangeclear 메서드가 호출될때 호출된다. 이 메서드를 정의하면 리스트와 부분리스트의 clear 연산 성능을 개선할 수 있기 때문에 protected 접근제어자로 해당 메서드를 오픈한 것이다.

removeRange 메서드가 없다면 하위 클래스에서 clear메서드를 호출하면 제거할 원소 수의 제곱예 비례해 성능이 느려지거나 부분리스트의 메커니즘을 밑바닥부터 새로 구현해야 했을 것이다.

상속용 클래스를 설계할 때 이러한 hook 을 결정하려면 실제 하위 클래스를 만들어 시험해보는 것이 최선이다.

상속용 클래스의 생성자에서 재정의 가능 메서드를 호출해서는 안된다.

상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.

이 때, 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.

public class Super {
	// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
	public Super() {
		overrideMe(); // Null
	}

	public void overrideMe() {

	}
}

public final class Sub extends Super{

	private final Instant instant;

	Sub() {
		instant = Instant.now();
	}

	@Override public void overrideMe() { // 상위클래스의 생성자가 호출한다.
		System.out.println(instant);
	}

	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.overrideMe();
	}

}
  • Sub 하위 클래스의 생성자가 호출해서 instant를 초기화하기 전에 상위 클래스의 생성자가 호출되서 overrideMe 메서드가 한번 호출된다 -> Null 찍음
  • 그 이후 하위 클래스가 생성되어 instant를 찍는다.

상속에서의 CloneableSerializable

생성자와 똑같다

두 인터페이스 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.

clnoereadObject 메서드는 생성자와 비슷한 효과를 낸다. 즉, 새로운 객체를 만든다. 따라서 상속용 클래스에서 해당 인터페이스를 구현할 경우, 위의 생성자에서와 마찬가지로 clone, readObject에서 재정의 가능 메서드를 호출해서는 안된다.

readResolvewriteReplace

Serializable을 구현한 상속용 클래스의 readResolvewriteReplace를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다. private으로 선언한다면 하위 클래스에서 무시되기 때문이다.

자바 직렬화에서 다시 객체의 형태로 만드는 역직렬화 과정에서 사용하는 것이 readResolve라면 직렬화 과정에서는 writeReplace 메서드가 사용된다.

상속한 하위 클래스에서 직렬화를 제대로 하려면 상속용 클래스가 readResolvewriteReplace 메서드를 protected 접근제어자로 가지고 있어야 한다

상속용 클래스 외의 일반적인 구체 클래스?

이런 클래스는 final도 아니고 상속용으로 설계되거나 문서화되지도 않았다.

상속용으로 설계하지 않은 클래스는 상속을 금지하면 된다.

  • 클래스를 final로 선언
  • 모든 생성자를 private 이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법

[아이템17]에서도 다뤘다.

댓글남기기