clone 재정의는 주의해서 진행하라
clone 메서드가 선언된 곳은 Object이고 protected이므로 Cloneable을 구현하는 것만으로 외부 객체에서 clone메서드를 호출할 수 없다.
Cloneable: 복제해도 되는 클래스임을 명시하는 용도의 mixin interface
mixin: 본인의 기능 이외 추가로 구현할 수 있는 자료형
: Object의 protected메서드인 clone의 동작 방식 결정
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
예를 들어, 아이템 7에서 소개한 Stack 클래스에서
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];
}
...
}
super.clone의 결과를 그대로 반환한다면, elements필드는 원본 stack 인스턴스와 같은 배열을 참조하게 된다. 따라서 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게된다.
이를 해결하기 위한 가장 쉬운 방식은 elements 배열의 clone을 재귀적으로 호출해 주는 것이다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
하지만 아래와 같이 재귀적으로 호출하는 것만으로는 충분하지 않을 때도 있다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Obejct key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
...//생략
}
위의 경우를 아래와 같이 재귀적으로 호출하게 된다면, 본인만의 배열은 가지게 되지만 이 배열은 원본과 같은 연결리스트를 참조하게 된다. 이를 해결하기 위해 연결리스트 또한 복사해야 한다.
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Obejct key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
//이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사
Entry deepCopy() { //깊은 복사
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length]; //1. 적절한 크기의 새로운 버킷 배열 할당
for (int i = 0; i < buckets.length; i++) { //2. 다음 원래의 버킷 배열을 순회하며
if (buckets[i] != null) { //비지 않은 각 버킷에 대해 깊은 복사
result.buckets[i] = buckets[i].deepCopy();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
...
}
이 기법은 간단하지만 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하고 리스트가 길다면 오버플로를 일으킬 위함이 있다. 이 문제를 피하기 위해선 deepcopy를 재귀 호출이 아닌 반복자를 써서 순회하는 방향으로 수정해야 한다.
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next) {
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
마지막 방법은 super.clone을 호출해 얻은 객체의 모든 필드를 초기 상태로 설정한 후, 원본 객체의 상태를 다시 생성하는 고수준 메서드를 호출해 둘의 내용을 똑같게 해주면 된다. 이 방식은 저수준에서 바로 처리할 때보다 느리며 필드 단위 객체 복사를 우회하기에 전체 Cloneable 아키텍처와는 어울리지 않는 방식이다.
final
이거나 private
이어야 한다. 복사 생성자
public Yum(Yum yum) {...};//본인과 같은 클래스의 인스턴스를 인수로 받는다.
복사 팩터리
public static Yum newInstance(yum yum){...};//복사 생성자를 모방한 정적 팩터리
복사 생성자, 복사 팩터리를 사용하면 불필요한 검사 예외를 던지지 않고, 형변환도 필요하지 않는다. 또한 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다. 따라서 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있게 된다.
복제 기능은 생성자, 팩터리를 이용하는 게 최고지만 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외이다.