본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
선결론
- 상속용 클래스를 설계하기란 어렵다
- 따라서 상속이 분명하게 필요한 것이 아니라면 금지하라
상속용 클래스를 설계하는 데 드는 어려움
- 문서화를 해야한다
- 재정의할 수 있는 메서드들을 호출할 수 있는 모든 상황을 문서로 남겨야 한다
- 이때, 다음과 같이 @implSpec 태그를 이용하여 불필요하게 내부 구현 방식을 설명해야 한다
- 내부 구현에서 사용되는 메서드를 잘 선별하여 protected 메서드 형태로 공개해야 할 수 있다
- protected 메서드 하나하나가 모두 내부 구현에 해당하므로 이는 캡슐화를 위반함
- 게다가 추가해야 할 protected 멤버를 놓칠 수 있고, 불필요한 protected 멤버가 포함될 수도 있음
- 예시의 removeRange 메서드는 오직 clear 메서드의 성능향상을 위해 사용됨
- 하위 클래스를 만들어 상속용 클래스를 시험해야 한다
- 한 번 구현한 문서화한 내부 사용패턴과 protected 메서드와 필드는 영구적으로 사용될 수 있음
- 따라서 직접 하위 클래스를 만들어 정상적으로 작동하는지 검증할 필요가 있다
- 상속을 허용하는 클래스가 지켜야 할 제약이 추가로 존재한다
- 상속용 클래스의 생성자는 직간접적으로 재정의 가능 메서드를 호출해서는 안 된다
- private, final, static 메서드는 재정의가 불가하므로 생성자에서 호출해도 됨
- 상속용 클래스의 생성자는 직간접적으로 재정의 가능 메서드를 호출해서는 안 된다
상속용 클래스 Super - 생성자가 재정의 가능 메서드를 호출함
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다
public Super() {
overrideMe();
}
public void overrideMe() { }
}
하위 클래스 Sub - 생성자에서 초기화하는 필드를 지니고 있고, 재정의한 메서드에서 이를 의존함
public class Sub extends Super {
// 초기화되지 않은 final 필드. 생성자에서 초기화한다.
private final Instant instant;
Sub() {
instant = Instant.now();
}
// 재정의 가능 메서드. 상위 클래스의 생성자가 호출된다.
@Override
public void overrideMe() {
System.out.println(instant);
}
}
public class Item19 {
public static void main(String[] args) {
Sub sub = new Sub(); // Super 생성자 호출 -> overrideMe() 호출 -> null 출력 -> Sub 생성자 호출
sub.overrideMe(); // 현재 시간 출력
/**
* null
* 2022-05-05T23:45:22.066694Z
*/
}
}
- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행됨
- 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출됨
- 이때, 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값을 의존하므로 NullPointerException이 발생하게 됨
- Cloneable과 Serializable 인터페이스가 상속용 설계의 어려움을 더한다
- clone과 readObject 모두 직간접적으로 재정의 가능 메서드를 호출해서는 안 된다
- clone과 readObject 메서드는 생성자와 비슷한 효과를 내므로 → 새로운 객체 생성
- Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다
- clone과 readObject 모두 직간접적으로 재정의 가능 메서드를 호출해서는 안 된다
- 이는 상속용으로 설계된 클래스가 아닌 일반적인 구체 클래스에서도 발생하는 문제이다
상속 금지
- 상속용으로 설계하지 않은 클래스는 상속을 금지하라
- 클래스를 final로 선언
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리 생성
- 아이템 17 참고
- 대신 핵심 기능을 정의한 인터페이스를 구현해라
- 표준 인터페이스를 구현하지 않았는데 상속이 필요하다면?
- 재정의 가능 메서드를 호출하는 사용 코드를 모두 제거하라 → 메서드를 재정의해도 다른 메서드의 동작에 영향을 주지 않음
- 책에서는 다음과 같은 방법을 소개하고 있다
- 먼저 각각의 재정의 가능 메서드의 내부 코드를 private 도우미 메서드로 옮기고, 이 도우미 메서드를 호출하도록 수정한다
- 그런 다음 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정한다
Concrete 클래스 - 재정의 가능 메서드가 그대로 노출됨
public class Concrete {
public Concrete() {}
public void overrideMe() {
System.out.println("overrideMe");
}
public void doSomething() {
overrideMe();
}
}
BetterConcrete - 재정의 가능 메서드의 내용을 private 도우미 메서드로 옮김
public class BetterConcrete {
public BetterConcrete() {}
public void overrideMe() {
helper();
}
private void helper() {
System.out.println("overrideMe");
}
public void doSomething() {
helper();
}
}
'책 > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 21: 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2022.05.09 |
---|---|
[이펙티브 자바] 아이템 20: 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.05.09 |
[이펙티브 자바] 아이템 18: 상속보다는 컴포지션을 사용하라 (0) | 2022.05.05 |
[이펙티브 자바] 아이템 17: 변경 가능성을 최소화하라 (0) | 2022.05.03 |
[이펙티브 자바] 아이템 16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.05.02 |