작성자: 박성민
작성일: 2022년 4월 23일
-
복제해도 되는 클래스임을 나타내는 믹스인 인터페이스이다.
-
Cloneable 인터페이스가 Object의 protected 메서드인 clone의 동작 방식을 결정 한다.
-
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드 모두를 복사한 객체를 반환하며,
구현하지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
-
Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다. (일반적인 인터페이스 사용 방식이 아니다)
-
clone 메서드의 일반 규약은 허술하다. (강제성이 없다)
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person("hong", 30);
Person clonePerson = (Person) person.clone();
System.out.println(person.equals(clonePerson)); // true
System.out.println(person==clonePerson); // false
}- clone() 구현 순서
- super.clone() 호출한다.
- 형변환한다.
- CloneNotSupportedException은 클래스가 Cloneable을 구현하지 않을 경우에 터지는데 예제에서는 무조건 구현했으니 그냥 던졌다.
가변객체 : 인스턴스 생성 이후에도 내부 상태 변경이 가능한 객체
- primitive 값은 올바르게 복제되지만, 배열 같은 경우는 원본 인스턴스와 똑같은 배열을 참조할 것이다.
- 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다.
- clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
- 해결하려면 가변 객체인 배열에 clone() 메서드를 이용해 따로 복사해 주자.
@Override
public Stack clone() throws CloneNotSupportedException {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result
}
public static void main(String[] args) throws CloneNotSupportedException {
Stack stack = new Stack();
stack.push("사과");
stack.push("포도");
Stack cloneStack = stack.clone();
cloneStack.push("수박");
System.out.println(Arrays.toString(stack.elements)); // 사과, 포도
System.out.println(Arrays.toString(cloneStack.elements)); // 사과, 포도, 수박
}- 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다. 배열은 clone 기능을 제대로 사용하는 유일한 예라 할 수 있다.
- elements 필드가 final이었다면 앞서의 방식은 작동하지 않는다.
- Cloneable 아키텍쳐는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다.
public class HashTable implements Cloneable {
private Entry[] buckets;
private static class Entry {
final Object key;
Object value;
Entry next;
public 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() throws CloneNotSupportedException {
HashTable result = (HashTable) 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;
}
}- deepCopy() 메서드는 자신이 가리키는 연결리스트(버킷)을 복사한다.
- 재정의한 public인 clone 메서드에서는 throws 절을 없애야 한다.
- 사용하기 편하기 때문.
- 상속용 클래스는 Cloneable을 구현해서는 안 된다.
- clone() 을 재정의해 CloneNotSupportedException()을 던지게하자.
- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다.
- Cloneable을 구현하는 모든 클래스는 public으로, 반환타입은 자신으로 clone을 재정의해야 한다.
- 재정의한 clone은 가장 먼저 super.clone을 호출하고 필요한 필드(내부의 가변객체)를 적절히 수정한다.
- 복사 생성자는 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자이다.
- 복사 팩토리는 복사 생성자를 모방한 정적 팩토리이다.
- Clonealbe/clone 방식보다 나은 면이 많다.
- 생성자를 쓰지 않는 방식을 사용하지 않는다.
- 정상적인 final 필드 용법과 충돌하지 않는다.
- 불필요한 검사 예외를 던지지 않는다.
- 형변환도 필요없다.
- '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다.
- 더 정확한 이름은 '변환 생성자' '변환 팩토리' 다.
- 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.
- final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
- 기본 원칙은 '복제 기능은 생성자와 팩토리를 이용하는 게 최고' 라는 것이다.
- 단, 배열은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.