메서드를 재정의하면 어떤일이 일어나는지 문서화해야 한다.
상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
공개된 메서드에서 해당 클래스의 또 다른 메서드를 호출할 수도 있다. 근데 그 다른 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다.
API 문서의 메서드 설명 끝에 "Implementation Requirements"로 시작하는 절이 있는데, 내부 동작 방식을 설명하는 곳이다.
좋은 API란 "어떻게"가 아닌 "무엇"을 하는지를 설명해야 한다.
내부 메커니즘을 문서화하는 것 말고, 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다. 하지만 하위 클래스에서 부분리스트의 clear 메서드를 쉽게 고성능으로 만들게 하기 위해 protected로 제공한다.
protected 하나하나가 내부 구현에 해당하므로 가능한 적게 하면서, 너무 적게 노출해서 상속으로 얻는 이점까지 없애지 않게 잘 조절해야 한다.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.
그리고 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하자.
상속을 허용하는 클래스가 지켜야 할 제약이 몇 개 더 있다.
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이 때 그 재정의한 메서드가 하위 클래스의 새성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// 초기화되지 않은 final 필드, 생성자에서 초기화한다.
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();
}
}
이 프로그램은 instant를 두 번 출력하지 않고, 첫 번째는 null을 출력한다. 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
Cloneable
과 Serializable
인터페이스 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지않다.
clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
일반적인 구체 클래스는 final도 아니고 상속용으로 설계되거나 문서화되지도 않았다. 하지만 그대로 두면 클래스에 변화가 생길 때마다 하위 클래스가 오작동할 수 있다.
그래서 상속용으로 설계하지 않은 클래스는 상속을 금지하는게 좋다.
클래스는 final 아니면 abstract
상속 금지는 방법이 두가지다.
package-private
으로 선언. 그리고 정적 팩터리 이용물론 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용하기 불편해진다. 이런데도 상속을 허용하고 싶다면, 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고 문서로 남겨야 한다. 이렇게 하면 메서드를 재정의해도 다른 메서드의 동작에 영향을 주지 않는다.