Cloneable
- 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스
- 아쉽지만 의도한 목적 제대로 이루지 못함
- 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고 protected라는 것
- 그래서 Cloneable을 구현하는 것만으로 clone 메서드를 호출할 수 없고 오버라이딩을 해야 호출이 가능함
- 이 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정함
- Cloneable을 구현한 클래스의 인스턴스에스 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환
- 그렇지 않은 클래스라면 CloneNotSupportedException을 던짐
Clone 메서드의 일반 규약

- 강제성이 없다는 점만 빼면 생성자 연쇄와 살짝 비슷한 메커니즘임 ↔ clone메서드가 super.clone( ) 이 아닌 생성자 호출해 얻은 인스턴스 반환해도 컴파일러는 불평 x
- super.clone( ) 이 아닌 생성자 호출을 하게 되면 object의 clone( ) 부분이 호출이 안되게 되는것
Clone 메서드 구현 방식
모든 필드가 기본타입, 불변객체 참조 시 → clone 후, 형변환
@Override public PhoneNumber clone() { try { return (PhoneNumber) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // 일어날 수 없는 일이다. } }
- covarinat return typing을 지원하니 Object반환이 아닌 PhoneNumber반환으로 바꾸기
- 다 기본타입이고 불변객체이니, super.clone() 호출 후 타입만 변형해주면 됨
클래스가 가변객체를 참조할 시 → 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; } public boolean isEmpty() { return size ==0; } // 코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드 @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
- 그냥 super.clone( )만 호출 시, elements는 똑같은 참조값을 가지게 됨. 즉 두개의 Stack 객체가 elements를 공유하게 됨
- 배열의 clone은 런타임 타입과 컴파일타임 타입 모두 원본 배열과 똑같은 배열을 반환함. 따라서 배열 복제할 때는 배열의 clone 메서드를 사용하라고 권장함
- 배열이 clone 기능을 제대로 사용하는 유일한 예
clone 재귀 호출 만으로 충분하지 않은 경우 → deepCopy같은 메서드 따로 정의
public class HashTable implements Cloneable { private Entry[] buckets = ...; private static class Entry{ final Object key; Object value; Entry next; Entry(Object key, Object value, Entry next) { this.key= key; this.value = value; this.next = next; } 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; @Override public HashTable clone(){ try{ HashTable result = (HashTble) super.clone(); result.buckets = new Entry[buckets.length]; for(int i=0; i< buckets.length; i++) if(buckets[i] != null) result.buckets[i] = buckets[i].deepCopy(); return result; } catch (CloneNotSupportedException e){ throw new AssertionError(); } }
- deepCopy 메서드를 따로 정의
- deepCopy를 재귀적으로 정의하면 연결 리스트가 너무 길면 stack overflow error 발생할 수 있으니, 위와 같이 정의
super.clone() 호출 후, 고수준 메서드(ex. put(key, value) ) 활용
- 고수준 API를 활용해 복제하면 보통은 간단하고 제법 우아한 코드를 얻게 됨
- 그러나 저수준에서 바로 처리할 때보다는 느림
- Cloneable 아키텍처의 기초가되는 필드 단위 객체 복사를 우회하기 때문에 Cloneable 아키텍처와는 어울리지 않는 방식이기도 함
- 생성자와 clone 메서드에서는 재정의될 수 있는 메서드를 호출하지 않아야 함
- 만약 재정의한 메서드를 호출하면 예상치 못한 상황이 생길 수 있음
Cloneable 사용 요약
- Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 함
- 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경
- 이 메서드는 가장 먼저 super.clone() 호출 후 필요한 필드를 전부 적절히 수정함
복사 생성자(conversion constructor)와 복사 팩터리(conversion factory) 라는 더 나은 객체 복사 방식..
//복사 생성자 public Yum(Yum yum){ ... }; // 복사 팩터리 public static Yum newInstance(Yum yum) { ... } ;
- Cloneable/clone 방식보다 나은 면이 많음
- 언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식)을 사용하지 않음
- 엉성하게 문서화된 규약에 기대지 않음
- 정상적인 final 필드 용법과도 충돌하지 않음
- Cloneable architecture에서는 필드를 새로 할당해야 하기 때문에 final 필드에 대해서는 재할당이 불가능하게 됨
- 불필요한 검사 예외 x
- 형변환 필요 x
- 인터페이스 타입의 인스턴스를 인수로 받을 수 있음
Cloneable이 몰고 온 모든 문제를 되짚어 봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 함. 기본 원칙은 ‘복제 기능은 생성자와 팩터리를 이용하는 게 최고' 라는 것임. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있음