이펙티브 자바, 쉽게 정리하기 - item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.
상속용 클래스가 지켜야 할 것들
- 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- 어떤 순서로 호출하는지, 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
- 재정의 가능 메서드란
public
,protected
중final
이 아닌 모든 메서드를 말한다. - 재정의 가능한 메서드를 호출할 수 있는 모든 상황을 문서로 남기는 것이 좋다.
- 백그라운드 스레드나 정적 초기화 과정에서 호출될 수도 있으므로 유의하자.
Implentation Requirements
와 @implSpec
태그 - remove()
의 예
API 문서 메서드 설명 끝에서 발견할 수 있는 문구 중 Implementation Requirements
가 있다. 이 절은 메서드 주석에 @implSpec
태그를 붙이면 자바독 도구가 생성해준다.
/**
* {@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;
}
- 주석 위쪽
@implSpec
태그를 볼 수 있다.- 엘리먼트를 찾기 위해 컬렉션을 순회하고,
iterator
의remove()
를 통해 원소를 제거한다고 적혀있다. iterator
에remove()
가 구현되어 있지 않다면,UnsupportedOperationException
을 던진다고 상세히 설명하고 있다.
- 엘리먼트를 찾기 위해 컬렉션을 순회하고,
@implSpec
은 이 클래스를 상속하여 메서드를 재정의했을 때 나타날 효과를 상세히 설명하고 있다.- 이 주의점을 통해 우리는 어떤 메서드를 어떤 방식으로 상속해야 할지 알 수 있다.
- 상속용 클래스가 지켜야 할 좋은 문서화의 예이다.
훅(hook) 메서드 공개하기 - removeRange()
의 예
/**
* 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();
}
}
- 주석에서 이 메서드가
clear()
에 의해 호출됨을 알리고 있다. clear()
를 고성능으로 만들기 쉽게 하기 위해 이 메서드를 외부로 공개하고 있다.- 이렇게 특정한 이유로
protected
접근 제어자로 메서드를 노출해야 할 필요가 있는 경우도 있다.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다. 직접 시험하며 어떤 메서드를 공개할지 선택하면 된다. 만일 하위 클래스를 여러개 만드는 동안 한번도 쓰이지 않는
protected
멤버가 존재한다면,private
이었어야 할 가능성이 크다. 널리 쓰일 클래스를 상속용으로 설계한다면 설계의 결정요소와 문서화의 책임이 더욱 크다. 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하자.
재정의 가능 메서드를 생성자에서 사용하면 안된다.
- 상속용 클래스의 생성자는 직접적이든, 간접적이든, 재정의 가능 메서드를 호출하면 안된다.
- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되기 때문이다.
public class Super {
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public class Item19Test {
static class Super {
public Super() {
overrideMe();
}
public void overrideMe() {
System.out.println("super's override me");
}
}
static class Sub extends Super {
private final Instant instant;
Sub() {
// 상속받은 클래스는 자동으로 부모 클래스의 생성자를 호출한다.
instant = Instant.now();
}
@Override
public void overrideMe() {
System.out.println("instant = " + instant);
}
}
@Test
public void constructorTest() {
Sub sub = new Sub();
sub.overrideMe();
}
}
출력 결과
instant = null
instant = 2022-01-01T11:06:14.830557Z
- 상위 클래스는 하위 클래스가 인스턴스 필드를 초기화하기도 전에
overrideMe()
를 호출한다. final
필드의 상태가 두가지다. (정상이 아니다.)print
는null
을 받아들이기 때문에 정상적으로 실행됐지만, 다른 경우null
값을 사용했다면,NullPointerException
의 위험이 존재한다.
Cloneable
, Serializable
- 직렬화, 객체 복사에 사용되는
clone()
,readObject()
와 같은 경우도 생성자와 비슷한 효과를 가지고 있으므로 직접적이든 간접적이든 재정의 가능한 메서드를 호출해선 안 된다.readObject()
는 역직렬화가 끝나기 전에 재정의한 메서드부터 호출하게 된다.clone()
는 하위 클래스의clone()
메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.clone()
이 잘못되면 원본 객체에도 피해를 줄 수 있다.
상속용 클래스와 그 제약
- 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스 안에 제약도 상당하다.
- 상속용으로 설계하지 않은 클래스는 상속을 금지하는 편이 버그를 줄일 수 있다.
- 클래스를
final
로 만들어 상속을 금지한다. - 모든 생성자를
private
혹은package-private
으로 선언하고public
정적 팩터리를 만든다.
- 클래스를
- 혹여나 일반 클래스에서 상속을 허용하고 싶다면, 재정의 가능 메서드는 절대 사용하지 않도록 문서에 표기하자.
핵심 정리
- 상속용 메서드를 만들 때는 클래스 내부에서 스스로를 어떻게 사용하는지 문서로 남기자.
- 문서화한 것은 그 클래스가 쓰이는 한 반드시 지키자.
- 그렇지 않을 경우 하위 클래스의 오동작을 만들 수 있다.
- 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하자.
- 클래스를
final
로 만들거나 생성자를 모두 외부에서 접근 불가능하게 바꾸면 된다.
- 클래스를
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2022.01.04 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.01.04 |
이펙티브 자바, 쉽게 정리하기 - item 18. 상속보다는 컴포지션을 사용하라 (2) | 2022.01.04 |
이펙티브 자바, 쉽게 정리하기 - item 17. 변경 가능성을 최소화하라 (0) | 2022.01.03 |
이펙티브 자바, 쉽게 정리하기 - item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.01.01 |