본문 바로가기
JAVA/Effective Java

item 13. clone 재정의는 주의해서 진행하라

by Garonguri 2022. 4. 23.
728x90

 

Cloneable

  • 복제해도 되는 클래스임을 명시하는 용도의 mix-in interface.

 

clone()

  • 한마디로 말하면 복제하는 메소드 이다.
  • 그러나, 모든 클래스가 clone() 메서드를 사용할 수 있는 것이 아니다. 
  • 왜? -> clone() 메서드가 선언된 곳은 Object고 접근 제한자는 protected이지만, clone()은 Clonealbe 인터페이스의 추상 메소드이기 때문.
  • 따라서, Cloneable을 구헌하는 것 만으로는 clone()을 호출할 수 없다.

 

//clone() 자동생성method
@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

Cloneable Interface

[ 용도 ]

- Object protected method인 clone()의 동작 방식을 결정한다.

  • cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면, 객체의 필드를 하나하나 복사한 객체를 반환한다.
  • cloneable을 구현하지 않은 클래스의 인스턴스에서 clone()을 호출하면, CloneNotSupportedException을 던진다.

인스턴스를 구현하는 것은 인터페이스에서 정의한 기능을 제공한다고 선언하는 의미인데, Cloneable의 경우 상위 클래스에서 정의된 protected clone()의 동작 방식을 변경한 것이다. => 상당히 이례적!

 

[ clone() 일반 규약 ]

일반적인 의도 : 객체의 복사본을 생성해 반환한다. ('복사'의 의미는 클래스마다 다름)

x.clone() != x // 참

x.clone().getClass() == x.getClass() // 참

x.clone().equals(x) // 일반적으로는 참.  그러나 반드시 만족해야 하는 것은 아니다.

clone()이 반환하는 객체는 super.clone()을 호출해 얻었을 때
x.clone().getClass() == s.getClass() // 참

[ 주의할 점 ]

- clone() 메서드가 super.clone()이 아닌 생성자를 호출해 인스턴스를 반환한다면?

  • 컴파일러는 동일한 인스턴스로 취급 할 것이다.
  • 그러나 클래스의 하위 클래스에서 super.clone()을 호출한다면, 잘못된 클래스의 객체가 만들어져 하위 클래스의 clone()은 제대로 동작하지 않는다.
  • 상위 클래스의 clone()인 super.clone()이 아니라, 하위 클래스 타입으로 변환해 반환하자.
  • (clone 재정의 클래스가 final이라면 상관 없지만, 이런 경우 애초에 cloneable을 구현 할 필요가 없다.)
  • 모든 필드가 기본 타입이거나 불변 필드라면 super.clone() 호출해 자바의 공변 변환 타이핑을 적용한다. 
  • 즉, 재정의한 메서드의 반환 타입을 상위 클래스의 메서드가 반환하는 타입의 하위 타입으로 만들어준다.

- 모든 필드가 기본 타입이거나 불변 객체를 참조하는 경우, 또는 불변 클래스는 굳이 clone()을 제공하지 않는 것이 좋다.

 

public class Beverage implements Cloneable{

    @Override
    public synchronized Beverage clone(){
        try{ // 재정의한 메서드의 반환 타입은 상위 클래스 메서드가 반환하는 타입의 하위 타입이다.
        	 // 클라이언트는 형변환을 하지 않아도 된다. 
            return (Beverage) super.clone();
        }catch (CloneNotSupportedException e){
        	// Object clone메서드가 검사 예외를 던지도록 선언되었기 때문에 try-catch로 감쌈.
            throw new AssertionError();
        }
    }
}

-> 위 코드는 성공적으로 동작하는 코드이지만, 클래스가 가변 구조를 참조하는 순간 오류를 발생시킨다.

- 클래스가 가변 객체를 참조하는 경우 

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

    /*
    -------------- Bad ------------------
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        return this.elements[--size];
    }
    */

    /*-------------- Good ------------------*/
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }

    /***
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 배열의 크기가 대략 2배씩 늘어난다.
     */
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

    public static void main(String[] args) {

        System.gc();
        System.runFinalization();
    }
}
  • clone()이 단순히 super.clone()의 결과를 그대로 반환한다면?
    • 가변 필드를 super.clone()으로 복제한다면, 값이 아닌 주소값을 복제하게 된다. 
    • 즉 반환된 stack 인스턴스의 사이즈 필드는 올바른 값을 갖지만, element 필드는 원본 stack와 똑같은 배열을 참조할 것이다.
    • 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되기 때문에 불변식을 해칠 수 있다.
    • -> 프로그램의 오동작 또는 NullPointerException이 발생한다.
  • clone()은 원본 객체에 아무런 해를 끼치지 않는 동시에, 복제된 객체의 불변식을 보장해야 한다.
    • 재귀적으로 가변 객체 참조 필드를 복사하면 해결할 수 있다.
    • 가변 객체를 갖는 클래스를 복사하기 위해서는 내부 정보를 복사해야 한다.
    • 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출하는 것이다.

 

@Override
public Stack clone(){
    try{
        Stack result = (Stack) super.clone();
        // 배열의 clone은 런타임, 컴파일 타입이 모두 원본 배열과 같은 배열을 반환한다.
        // 배열을 복제할 때는 배열의 clone()을 사용하라 권장한다.
        // 배열은 clone()을 제대로 사용하는 유일한 예이다.
        result.elements = elements.clone();
        return result;
    }catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}
  • 만약 가변 필드가 final인 경우에는 위 방식이 작동하지 않는다.
    • 새로운 값을 참조할 수 없기 때문이다.
    • 가변 객체를 참조하는 필드는 final로 선언하라는 용법과도 충돌한다.
    • -> CLoneable을 구현하려면 일부 필드에서 final을 떼버려야 하는 일이 생길 수 있다.

[ clone()의 재귀적 호출로는 충분하지 않을 때 ]

  • 배열 안에 가변 참조 객체가 있는 경우에 해당한다.
  • [해결 방법]
  • Deep Copy를 한다. ex) HashTable의 clone()
  • 단순히 버킷 배열의 clone()을 재귀적으로 호출한다면?
    • 복사본은 자신만의 버킷 배열을 갖지만, 원본과 같은 연결리스트를 참조하기 때문에 원본과 복제본 모두 동작할 가능성이 생긴다.
    • 따라서 각 버킷을 구성하는 연결 리스트를 복사해야 한다.
  • Deep copy를 통한 해결
    • 적절한 크기의 새로운 버킷 배열을 생성해 깊은 복사를 진행한다.
    • deepCopy()는 자신이 가리키는 연결 리스트 전체를 복사하기 위해 자기를 재귀적으로 호출한다.
    • -> 여기서 리스트 원소 수만큼 스택 프레임을 소비하여, 리스트가 긴 경우 스택오버플로우가 생길 수 있다.
    • -> deepCopy()는 재귀 호출 대신 반복자를 사용해 순회해라.

[ 복잡한 가변 객체를 복제하는 방법 ]

  • super.clone()을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한다.
  • 원본 객체 상태를 다시 생성하는 고수준 메서드를 호출한다.
  • -> 저수준 처리보다는 느리다.
  • -> Cloneable 아키텍처와 어울리지 않기도 하다.

 

[ clone() 메서드 재정의 시 주의사항 ]

- public인 clone() 메서드에서는 throws절을 없애야 한다. 검사 예외를 던지지 않아야 한다.

- 상속용 클래스는 Cloneable을 구현해서는 안된다.

- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone() 역시 적절히 동기화 해줘야 한다.

- super.clone() 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다.

 

[ 요 약 ]

1. Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.

2. 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다.

3. clone()은 가장 먼저 super.clone()을 호출하고 필요한 필드를 적절히 수정한다.
- 즉, 객체 내부에 숨은 모든 가변 객체를 복사하고 복제본이 가진 객체 참조 모두가 복사된 객체를 가리키게 한다.
- 주로 clone()을 재귀적으로 호출해 구현하지만 언제나 최선은 아니다.

4. 기본 타입 필드나 불변 객체 참조만 갖는 클래스라면 수정할 필요가 없다.
- 일련번호, 고유 ID는 수정해야 한다.
728x90

댓글