Java

[Effective Java] 아이템19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

jun9.com 2022. 4. 25. 11:20

상속을 고려한 문서화

메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다.

즉, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야한다.

 

public boolean remove(Object o)
주어진 원소가 이 컬렉션 안에 있다면 그 인스턴스를 하나 제거한다(선택적 동의). 더 정확하게 말하면, 이 컬렉션 안에 'Object.equals(o, e)가 참인 원소' e가 하나 이상 있다면 그중 하나를 제거한다. 주어진 원소가 컬렉션 안에 있었다면(즉, 호출 결과 이 컬렉션이 변경됐다면) true를 반환한다.
Implementation Requirements: 이 메서드는 컬렉션을 순회하며 주어진 원소를 찾도록 구현되었다. 주어진 원소를 찾으면 반복자의 remove 메서드를 사용해 컬렉션에서 제거한다. 이 컬렉션이 주어진 객체를 갖고 있으나, 이 컬렉션의 iterator 메서드가 반환한 반복자가 remove 메서드를 구현하지 않았다면 UnsupportedOperationException을 던지니 주의하자.


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

아이템 18에서 add를 재정의한 것이 addAll에까지 영향을 준다는 사실을 알 수 없었는데 이를 보완할 수 있다!!

 

 

 


hook 선별

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

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

 

protected void removeRange(int fromIndex, int toIndex){...}




public void clear(){
	removeRange(0, size());
 }

 

AbstractList clear 메서드는 removeRange를 내부에서 호출하고 있다. 때문에 List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다.

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

 

이때 유의할 점은 removeRange의 접근 제한자가 protected로 설정되어있다는 것이다.(하위 클래스를 만들 때 전혀 쓰이지 않는 메서드는 private로 만들면 된다.)

하위 클래스에서 사용할 일이 있다면 protected로 만들어준다. 상속용으로 설계한 클래슨느 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다

 


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

Super클래스에서 하위 클래스에서 재정의가 가능한 overrideMe 메서드를 호출한다.

public class Super {
    public Super() {
        overrideMe();
    }

    public void overrideMe() {
        System.out.println("super method");
    }
}

 

Sub 클래스에서 부모 클래스의 overrideMe를 재정의하고 Sub 클래스를 생성하면 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 동작하므로 null이 출력된다.

public class Sub extends Super{
    private String str;
    public Sub() {
        str = "Sub String";
    }

    @Override
    public void overrideMe() {
        System.out.println(str);
    }

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

 

 

sol) 상속용으로 설계하지 않은 클래스는 상속을 금지해라

상속을 금지하는 방법

  • 클래스를 final로 선언
  • 모든 생성자를 private or package private로 지정

각각의 재정의 가능한 메서드는 자신의 본문 코드를 private '도우미 메서드'로 옮기고 이 도우미 메서드를 호출하도록 수정 한다.

그런 다음 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정한다.

 

 

public class Super {
    public Super() {
        //overrideMe();
    	helperMethod();
    }

    public void overrideMe() {
    	helperMethod();
    }
    
    private void helperMethod() {
    	System.out.println("super method");
    }
}

 

public class Sub extends Super{
    private String str;
    public Sub() {
        str = "Sub String";
    }

    @Override
    public void overrideMe() {
        System.out.println(str);
    }

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