[Effective Java] 3 - item13 clone
재정의는 주의하여 진행하라
item13 clone
재정의는 주의하여 진행하라
Cloneable
은 복제해도 되는 인터페이스 임을 명시하는 용도의 믹스인 인터페이스이지만, 인터페이스 구현만으로는 외부에서 clone 메서드를 호출 할 수 없다.clone
메서드가 선언된 곳이Cloneable
이 아닌Object
이고,protected
로 선언되어 있다.
Cloneable
인터페이스의 용도
Object
의protected
메서드인clone
의 동작 방식을 결정한다.Cloneable
을 구현한 클래스의 인터페이스에서clone
을 호출하면, 그 객체의 필드들을 하나하나 복사한 객체를 반환한다.Cloneable
을 구현하지 않은 클래스에서clone
을 호출하는 경우,CloneNotSupportedException
을 던진다.
clone
메서드의 규약
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* <p>
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
*
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
clone()
메서드는 현재 객체의 복사본을 생성하고 반환한다. “복사본”의 정확한 의미는 객체의 클래스에 따라 다를 수 있지만 일반적인 의도는 다음과 같다.
어떤 객체 x에 대해 다음 식이 true이어야 한다:
x.clone() != x
또한 다음 식도 true이어야 하지만 엄격한 요구 사항은 아니다.x.clone().getClass() == x.getClass()
일반적으로는 다음 식이 true일 것이나, 엄격한 요구 사항은 아니다.x.clone().equals(x)
관례적으로, 이 메서드에서 반환된 객체는super.clone
을 호출하여 얻어져야 한다. 만약 어떤 클래스와 그 슈퍼클래스(단,Object
제외)가 이 관례를 따른다면,x.clone().getClass() == x.getClass()
이 될 것이다.
또, 이 메서드에서 반환된 객체는 복제 대상 객체와 독립적이어야 한다.
이 독립성을 달성하기 위해, super.clone()
을 통해 반환된 객체의 내부 가변 객체를 복사하고 이러한 객체에 대한 참조를 해당 복사본에 대한 참조로 대체해야 할 수도 있다.
재정의 시 주의사항
기본적인 재정의
class PhoneNumber implements Cloneable {
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(ClassNotSupportedException e) {
...
}
}
}
super.clone()
을 실행하면PhoneNumber
에 대한 완벽한 복제가 이루어진다.super.clone()
의 리턴 타입은Object
이지만, 자바의 공변 반환타이핑 기능을 통해PhoneNumber
하위 타입으로 캐스팅하여 리턴하는 것이 가능하다. (사용 권장)try-catch
:super.clone()
메서드에서ClassNotSupportedException
이라는checked exception
을 리턴한다.- 하지만
PhoneNumber
가Cloneable
을 구현하기 떄문에 절대 실패하지 않는다. 따라서 이부분은RuntimeException
으로 처리하거나, 아무것도 설정하지 않아야 한다.
- 하지만
가변 객체 참조
public class Stack implements Cloneable {
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) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
@Override
public Stack clone() throws CloneNotSupportedException {
return super.clone();
}
}
Stack
클래스에서 단순히 clone메서드를 사용해super.clone()
만 실행하게 된다면,new Stack()
을 통해 새로운 객체가 생성되고 필드모두 원본 객체와 동일하게 초기화가 될 것이다.- 하지만, Object의 clone 기본규약에는 Deep copy가 아닌 Shallow Copy를 이용해 초기화를 진행하도록 되어 있으며 배열과 같은 가변필드는 원본 필드와 객체를 공유하게 된다.
Clone메서드는 사실상 생성자와 같은 효과를 낸다.
- clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
- 제대로 복제하기 위해서는,
elements
배열을 Deep copy로 복사해 만들어줘야 한다. - 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출하는 것이다.
- 제대로 복제하기 위해서는,
public class Stack implements Cloneable {
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) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
@Override
public Stack clone() {
try {
Stack clone = (Stack) super.clone();
// 배열의 clone은 런타임 타입과 컴파일 타입 모두가 원본 배열과 같은 배열을 반환한다.
// 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다.
// 배열은 clone 기능을 제대로 사용하는 유일한 예이다.
clone.elements = elements.clone();
return clone;
} catch(CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
문제점
- Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라 는 일반 용법과 충돌한다.
- 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다는 말이다.
- 또한, 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다.
- 이를 해결하기 위해서는 각 버킷을 구성하는 연결 리스트를 복사 해야 한다.
- 연결리스트 전체를 복사하려는 경우 재귀적으로 호출하게 된다.
- 재귀 호출은 리스트의 원소 수만큼 스택 프레임을 소비하게 되기 때문에 스택 오버플로우를 일으킬 위험이 있다.
- 문제를 피하기 위해서는 재귀 호출 대신에 반복문을 사용하여 순회하는 방향으로 수정해야 한다.
복잡한 가변 객체를 복제
- super.clone() 을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정하고, 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출한다.
- 이 방식은 저수준에서 바로 처리할 때보다는 느리며, Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에 전체 Cloneable 아키텍처와는 어울리지 않는 방식이다.
- [주의]
- 생성자에서는 재정의될 수 있는 메서드를 호출하지 않아야 하는 것처럼, clone() 메서드도 마찬가지이다.
- Object의 clone() 메서드는 CloneNotSupportedException을 던진다고 선언되어 있지만, 재정의한 메서드는 수정해야 한다.
public clone
메서드에서는throws
절을 없애거나, RuntimeException으로 throw 한다.- 하위 클래스에서 clone이 동작하지 않게 만들수도 있다.
@Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
스레드 동기화
- 스레드 안정성을 고려한다면, clone 메서드에 대해 적절히 동기화 처리가 필요하다.
- super.clone() 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화 해줘야 한다.
- 여러 스레드가 동시에
clone
을 호출할 때 예측할 수 없는 결과가 발생할 수 있다.
- 여러 스레드가 동시에
복사 생성자와 복사 팩터리 메서드
Cloneable
을 이미 구현한 클래스를 확장한다면 clone()
메서드를 잘 작동하도록 구현해야 하나, 그렇지 않은 상황에서 사용할 수 있는 방식이다. 이 방식은 Cloneable/clone
방식보다 더 나은 옵션을 제공한다. 이 방식은 변환 생성자 혹은 변환 팩터리라고도 한다. (Conversion constructor / factory)
public Yum(Yum yum) {}
public static Yum newInstance(Yum yum) {}
- 언어 모순적이고 위험한 객체 생성 메커니즘을 사용하지 않는다. (super.clone())
- clone 규약에 기대지 않는다.
- final 필드 용법과 충돌하지 않는다.
- 불필요한 예외 처리가 필요없다.
- 형변환이 필요없다.
- 복사 생성자와 복사 팩터리는 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
Leave a comment