아이템 13. clone 재정의는 주의해서 진행해라
Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이지만, clone 메서드가 정의된 곳이 Object 클래스이고 그마저도 protected이다. 그래서 Cloneable을 구현 하는 것 만으로는 외부 객체에서 clone을 사용 할 수 없다.
Cloneable을 이례적으로 Object 클래스의 protected 메서드인 clone의 정의를 정의한다.
일반적으로는 인터페이스에서 정의한 기능을 클래스에서 받아서 사용하는 행위를 진행하는데, Cloneable의 경우 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한다.
그래서 Object.clone() 을 만약 바로 불러서 내부적으로 구현하여 사용 하려고 하면 아래의 설명을 참고하면 throws CloneNotSupportedException을 낸다고 한다.
위 설명의 결론은 Object 객체에 clone 메소드가 선언 되어 있으나 특수하게도 Cloneable 인터페이스를 구현하여야 쓸 수 있다는 것입니다.
참고 자료 - Object.clone()
/**
* ... (생략) ...
* @implSpec
* 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.
* ... (생략) ...
* @see java.lang.Cloneable
*/
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
위 주석중 First, if the class of this object does not implement the interface {@code Cloneable}, then a {@code CloneNotSupportedException} is thrown 에 따라서 implements로 Cloneable을 정의하지 않은 객체에서는 CloneNotSupportedException을 낸다고 한다.
Object.clone()의 명세 설명
이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해서 다음 식은 참이다.
x.clone() != x
또한, 다음 식도 참이다.
x.clone().getClass() == x.getClass()
하지만 이상의 요구를 전부 만족해야 하는 것은 아니다.
다음 식도 일반적으로는 참이지만, 역시 필수는 아니다.
x.clone.equals(x)
관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와 (Object 클래스를 제외한) 모든 상위 클래스가 이 관례를 따른다면 이 식은 참이다.
x.clone().getClass() == x.getClass()
Cloneable의 명세에 따른 실전 코드 테스트
public class CloneTest {
public static void main(String[] args) {
Data d = new Data(1,4);
Data d1 = new Data(0,0);
try {
d1 = (Data) d.clone();
System.out.println(d1.x + " " + d1.y);
System.out.println(d.clone().equals(d));
System.out.println(d.clone().getClass() == d.getClass() ? true : false);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
Test t = new Test(0, 0);
Test t1 = new Test(1, -1);
// try {
// t1 = (Test) t.clone();
// System.out.println(d1.x + " " + d1.y);
// } catch (CloneNotSupportedException e) {
// e.printStackTrace();
// }
}
static class Data implements Cloneable{
int x;
int y;
public Data(int x, int y){
this.x = x;
this.y = y;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
static class Test{
int x;
int y;
public Test(int x, int y){
this.x = x;
this.y = y;
}
}
}
clone() 생성시 주의사항 - 하위 클래스 예외
만일, 상위 클래스에서 clone 메서드가 super.clone이 아닌 생성자를 호출해 받은 인스턴스로 반환하여도 동작 할 것이다.
@Override
public PhoneNumber clone(){
try{
return (PhoneNumber) super.clone(); //이 와 같이 하위 클래스에서도
//연쇄적으로 clone 구현시 타입 형 변환을 해주어야 한다.
catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
자바에서는 부모 클래스의 객체가 자식 클래스의 타입으로 변환이 가능하니 위와 같이 만들어도 작동을 할 수 있다.
가변 객체 참조 예외
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();
}
}
결론 : 가변 객체인 Object[] elements 는 Stack 객체가 clone이 될 시 복사된 객체의 Stack에서 원본 객체의 elements 변수를 접근 가능하다.
이전에 사용했던 Stack 객체인데 이때 stack 객체를 clone하게 되면 내부 배열인 elements도 원본 객체의 elements를 참조하게된다.
Stack a -> clone() 하여 Stack b로 받아도 b.elements와 a.elements가 같기 때문에 NPE 오류가 날 수도 있음
가변 객체 참조 예외 해결법 - 재귀적 호출
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
스택 내부 정보를 다시 재귀적으로 불러서 해결한다.
위와 같은 코드가 되는 이유는 배열의 clone()은 완벽히 컴파일 타입, 런타임 타입과 상관 없이 완벽히 복사한다. (Object[] 타입 캐스팅을 해줄 필요가 없다는 뜻)
하지만, 위 코드는 일반 규칙인 'Cloneable 아키텍쳐는 가변 객체를 참조하는 필드는 final로 선언하라' 와 충돌한다.
그래서, clone이 필요하다면 final을 삭제하고 구현 할 수도 있다.
재귀 호출이 능사는 아니다.
위 처럼 재귀 호출을 하여도 해쉬 테이블과 같은 경우에는 재귀적 호출을 하여도 가변 참조 객체 오류가 난다.
실제 해시 테이블의 구조
해시테이블 내부는 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결 리스트의 첫 번째 엔트리를 참조한다.
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
// ...(생략)...
private transient Entry<?,?>[] table;
// ...(생략)...
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// ...(생략)...
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// ...(생략)...
}
*내부 클래스는 바깥쪽 클래스의 상속도 받을 수 있다. (Entry가 clone을 바로 쓸 수 있었던 이유)
위 클래스를 단순히 clone으로 재귀적으로 호출한다면 복사된 객체에서 가변 객체를 연결리스트의 형태로 가지는 Entry 객체가 원본 객체의 Entry를 참조하여 예기치 않은 상황이 발생 할 수 있다.
public class HashTable //...(생략)... {
//...(생략)...
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
//Object 객체의 clone 이용
}
}
/**
* Creates a shallow copy of this hashtable. All the structure of the
* hashtable itself is copied, but the keys and values are not cloned.
* This is a relatively expensive operation.
*
* @return a clone of the hashtable
*/
public synchronized Object clone() {
Hashtable<?,?> t = cloneHashtable();
t.table = new Entry<?,?>[table.length];
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
? (Entry<?,?>) table[i].clone() : null;
}
t.keySet = null;
t.entrySet = null;
t.values = null;
t.modCount = 0;
return t;
}
코드 해석 : 하나 하나 복사하는 이유는 일반적인 배열과 다르게 Entry 객체의 경우 가변 객체로써 하나 하나의 객체를 모두 clone 해주어야 한다.
*책에서는 깊은 복사를 예로 들었지만 실제 해시 테이블에서는 Object 클래스의 clone까지 가져와서 재귀적으로 호출한다.
사실 위와 같은 코드는 아이템 19의 생성자와 객체를 생성할 수 있는 모든 메서드는 재정의 될 수 있는 메서드를 호출하지 않아야 한다와 반하는 예제이다.
그래서 상속용 객체에서는 하위 클래스에서 Cloneable을 지원하지 못하게 아래와 같이 막을 수 있다.
@Override
protected final Object clone() throwns CloneNotSupportedException{
throw new CloneNotSupportedException;
}
Cloneable 사용하여야 할까?
그래서 굳이 clone을 사용하여 재정의하고 깊은 구조 내부에 숨어 있는 가변객체들을 끄집어 내어서 clone을 재귀적으로 호출해서 구현하는 것을 하여야 할까?
Cloneable을 이미 구현한 클래스를 확장하게 된다면 어쩔 수 없이 사용 해야하는 것은 맞다.
하지만, 복사 생성자와 복사 펙터리라는 객체 복사 생성방식을 사용할 수 있다.
예시) 복사 생성자
public Test(Test test){ ... };
예시) 복사 펙토리
public static Test newInstance(Test test) { ... };
그리고 위와 같은 방식들은 인수로 인터페이스 타입의 인스턴스를 받을 수 있으니 HashSet 객체 s를 TreeSet으로 바꿔서 쓸 수도 있다.
결론 : Cloneable이 몰고온 문제점을 보았을때, 새로운 인터페이스,클래스를 제작 할 때는 Cloneable을 확장해서는 안된다. 단, 기본 객체의 배열 그 자체만은 clone 메서드 방식이 가장 깔끔하게 맞기 때문에 이것만은 예외라고 할 수 있다.