Item19 - 상속을 고려해 설계하고 문서화하라. 그렇지 않다면 상속을 금지하라

2023. 12. 2. 16:17java/이펙티브 자바

 

 

item18에서는 상속을 염두에 두지 않고 설계했고 상속할 때의 주의점도 문서화해놓지 않은 외부 클래스를 상속할 때의 위험을 경고했다. 그렇다면 상속을 고려한 설계와 문서화란 정확히 무엇일까?

 

상속용 클래스는 재정의할 수 있는 메서드들의 내부 구현을 문서로 남겨야 한다.

클래스의 API로 공개된 메서드에서 자신의또 다른 메서들르 호출할 수도 있다. 그런데 마침 호출되는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다. 즉 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다. @implSpec 태그를 붙여주면 자바독 도구가 내부 동작 방식을 설명하는 곳을 생성해준다.

 

예제1)

public class ExtendableClass {

    /**
     * This method can be overridden to print any message.
     *
     * @implSpec
     * please use System.out.println().
     */
    protected void doSomething() {
        System.out.println("Hello!");
    }
}

 

=>

  

예제2)

/**
 * {@inheritDoc}
 *
 * @implSpec
 * 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
 * {@code UnsupportedOperationException} if the iterator returned by this
 * collection's iterator method does not implement the {@code remove}
 * method and this collection contains the specified object.
 *
 * @throws UnsupportedOperationException {@inheritDoc}
 * @throws ClassCastException            {@inheritDoc}
 * @throws NullPointerException          {@inheritDoc}
 */
   public boolean remove(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext()) {
                if (it.next()==null) {
                    it.remove();
                    return true;
                }
            }
        } else {
            while (it.hasNext()) {
                if (o.equals(it.next())) {
                    it.remove();
                    return true;
                }
            }
        }
        return false;
    }

 

 

 

내부 메커니즘을 문서로 남기는 것이 상속을 위한 전부는 아니다.

효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.

/**
 * 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.
 *
 * @implSpec
 * 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();
    }
}

========================================================================

/**
 * Removes all of the elements from this list (optional operation).
 * The list will be empty after this call returns.
 *
 * @implSpec
 * This implementation calls {@code removeRange(0, size())}.
 *
 * <p>Note that this implementation throws an
 * {@code UnsupportedOperationException} unless {@code remove(int
 * index)} or {@code removeRange(int fromIndex, int toIndex)} is
 * overridden.
 *
 * @throws UnsupportedOperationException if the {@code clear} operation
 *         is not supported by this list
 */
public void clear() {
    removeRange(0, size());
}

List 구현체의 최종 사용자는  removeRange 메서드에 관심이 없다. 그래도 이 메서드를 제공한 이유는 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.

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

 

어떤 메서드를 protected로 노출해야 할지는 심사숙고해서 잘 예측해본 다음 시험해보는 것이다.

 protected  메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 적어야 하지만 너무 적게 노출해서 상속으로 얻는 이점을 없애지 않도록 주의해야 한다.

 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하는 것이 유일하다.

 

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

 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되 하위 클래스에서 재정의한 메서드가 상위 클래스의 생성자 내에서 호출되면 정상적인 작동을 하지 않을 수 있다.

public class Super {

    //생성자가 재정의 기능 메서드를 호출한다.
    public Super() {
        overrideMe();
    }

    public void overrideMe() {
        System.out.println("부모 생성자!");
    }
}

================================================

public 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();
    }
}


==>
null // Super() 생성자가 먼저 호출되고 하위 클래스에서 재정의한 overrideMe()가 호출!
2023-12-02T06:02:41.806380900Z

 

   Cloneable과 Serializable을 구현한 클래스를 상속할 수 잇게 설게하는 것은 일반적으로 좋지 않은 생각이다. 그 클래스르 확장하려는 프로그래머에게 부담이 커지기 때문이다.

 => 대안 : item13, item86

 

그렇다면 그 외의 일반적인 구체 클래스는 어떻게 해야 할까? final도 아니고 상속용으로 설계되거나 문서화되지도 않았다. 하지만 그대로 두면 위험하다. 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들 수 있기 때문이다.

 

그러니 상속용으로 설계하지 않은 클래스는 상속을 금지시키자