[Effective Java] item13 - clone 재정의는 주의해서 진행하라

신민철·2023년 4월 18일
1

Effective Java

목록 보기
13/23
post-thumbnail
protected native Objecy clone() throws CloneNotSupportedException;

clone 메소드는 원본 객체의 필드값과 동일하게 새로운 객체를 생성해주는 메소드이다.

여기서 native라는 키워드가 있는데 자바에서 다른 언어를 사용할 수 이게 만들어주는 키워드이다.

CloneNotSupportedException은 clone()을 사용하지 못하는 객체에서 사용했을 때 발생하는 예외이다. clone()은 Cloneable 인터페이스를 구현하면 사용할 수 있다.

public interface Cloneable {}

이 인터페이스는 Serializable 처럼 비어있는 인터페이스이다.

구현하면 복사한 객체를 반환하고 하지 않으면 CloneNotSupportedException을 터트린다.

내가 일반적으로 생각하던 객체 생성은 생성자인데 이것은 매우 독특하다고 할 수 있다.

다음은 규약이다.

  • 어떤 객체 x에 대하여 다음은 참이다.
    • x.clone() ≠ x
    • x.clone().getClass() == x.getClass()
  • 다음 식들도 일반적으로 참이지만, 필수는 아니다.
    • x.clone().equals(x)
  • 관례상 이 메소드가 반환하는 객체는 super.clone을 호출해 얻어야 한다.
    • x.clone().getClass() == x.getClass()
@Override public ExClone clone() {
	try {
		return (ExClone) super.clone();
	} catch (CloneNotSupportedException e){
		throw new AssertionError();
	}
}

이 예시를 보면 try-catch로 CloneNotSupportedException을 잡아준다.. 이것이 기본형이다. super.clone()으로 체이닝되어 부모 클래스.. 결국 Object까지 올라가 clone을 실행하고 해당 Object 객체를 형변환하여 사용하면 모든 값이 동일한 객체가 반환될 것이다!

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

	public Stack() {
		elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}

	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}

	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null; // Eliminate obsolete reference
		return result;
	}

	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}

해당 예시를 보자. 이 클래스의 객체를 clone()하면 우리가 생각한대로 deepcopy가 될까?

정답은 아니다이다. size같은 기본적인 필드는 잘 복사가 되겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조해서 둘중에 하나를 수정하게 된다면 다른 것도 수정되어서 프로그램이 이상해지거나 NullPointerException을 터트릴 것이다..

@Override public Stack clone() {
	try {
		Stack result = (Stack) super.clone();
		result.elements = elements.clone();
		return result;
	} catch (CloneNotSupportedException e) {
		throw new AssertionError();
	}
}

위의 방식대로 elements 배열의 clone을 재귀적으로 호출하면 우리가 생각한대로 완전히 복사가 되는 것이다. 배열에서 clone()을 사용하면 런타임 타입과 컴파일 타입이 일치하는 배열을 반환해준다! 그래서 배열에서는 clone()을 사용하는 것은 꽤나 좋은 옵션이다. 하지만 final 이라면 사용하지 못한다..

배열이 심지어 참조형 타입을 담고 있으면 위의 코드에 재귀적으로 한 번 더 돌려야 할 것이다..



요약하자면 Cloneable 인터페이스를 구현하는 모든 클래스는 clone()을 재정의해야 한다. 반환타입은 자신이 되도록, super.clone을 호출한 후에 필요 필드를 적절히 수정하자.

내부에 참조를 가르키는 ‘깊은 구조’가 있다면 숨어 있는 가변 객체를 빠짐없이 복사해야 한다!

하지만 지금까지 생각해봤을때 우리는 항상 생성자나 팩토리를 사용해서 했다..

그래서 책에서도 복사 생성자복사 팩토리라는 방식을 제시한다!

public Yum(Yum yum) { ... };
public static Yum newInstance(Yum yum) { ... };

이 방식들은 clone보다 고려해야 할게 적어서 안전하고 final로 정의해도 되고, 예외도 없고, 형변환마저 필요없다.

심지어 인터페이스 타입의 인스턴스를 인수로 받을 수 있다! HashSet 객체 s를 TreeSet으로 복제하려면 간단히 new Treeset<>(s)를 하면 되는 것처럼 말이다.



핵심 정리
Cloneable을 구현하는 클래스는 clone()을 재정의해야 한다. clone()은 깊은 구조를 따져보며 모두 재귀적으로 복사를 해줘야 한다. 이에 대한 대체품으로 복사 생성자와 복사 팩토리가 있는데 이건 다양한 점에서 이점이 크다!

0개의 댓글