원본 객체의 필드 값과 동일한 값을 가지는 새로운 객체를 생성해주는 메서드
clone
메서드는 Cloneable
이라는 인터페이스를 implements 해야 쓸 수 있다.
하지만 아이러니하게도 clone
메서드는 Cloneable
에 선언되어 있지 않고 Object
에 선언되어 있다 (?)
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
// 필수는 아님!
Object
에 선언되어 있는 clone
메서드는 접근제한자가 protected
이고, Object
객체를 리턴한다. 이 메소드를 PhoneNumber
에서 구현해보자.
// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
다음과 같이 public
으로 메서드 접근을 열어두고(다른 곳에서도 객체를 복제할 수 있도록 하기 위해), PhoneNumber
객체를 반환하게 했다. (공변 반환 타이핑)
super.clone()
를 통해 부모의 clone
메서드를 이용해 객체를 복제한다.
(아마 결국 Object
의 clone
메서드를 쓰게 될 것이다.)
근데 super.clone 으로 안하고 PhoneNumber의 생성자로 새로운 객체 생성해서 반환하는 방법도 있을텐데...?
문제 발생 : 하위클래스에서 super.clone을 호출하면 상위타입 객체가 반환된다. -> 제대로 동작X
// 생성자를 이용해 객체 반환
@Override
public PhoneNumber clone() {
try {
return new PhoneNumber();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
PhoneNumber를 상속 받는 SubPhoneNumber가 있다고 가정하면
public class SubPhoneNumber extends PhoneNumber implements Cloneable {
private String name;
@Override
public SubItem clone() {
return (SubPhoneNumber)super.clone(); //에러 발생 (업캐스팅 불가)
}
}
SubItem clone
에서 super.clone()
은 PhoneNumber
객체를 반환한다.
근데 상위타입이 하위타입으로 바뀔 수 없기 때문에 에러가 발생한다.
지금까지 설명한 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];
}
}
만약 이 Stack
클래스를 clone
하면 동일한 elements 가지게 되는 것이다.
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
배열.clone()
을 해주면 될 것 같지만, 이렇게 하면 "같은 배열"을 참조하지는 않지만 배열 안에 있는 인스턴스은 같은 인스턴스를 참조하게 된다.
예를 들어 HashTable
용 clone
메서드를 생각해보자. HashTable
은 버킷 배열을 가진다. 그리고 버킷은 LinkedList
를 가진다. 그럼 버킷도 복제를 하고, 버킷 안에 있는 LinkedList
복제를 해야 한다. -> 버킷을 깊은복사해야 한다.
public class HashTable implements Cloneable {
private Entry[] buckets = new Entry[10];
private static class Entry {
final Object key;
Object value;
Entry next;
//재귀적으로 복사
public Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
// 반복적으로 복사
public 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() {
HashTable result = null;
try {
result = (HashTable)super.clone();
result.buckets = new Entry[this.buckets.length];
for (int i = 0 ; i < this.buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = this.buckets[i].deepCopy(); // p83, deep copy
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
deepCopy()
재귀적 호출로 인해 스택오버플로우에러가 발생 할 수도 있다.
이 대안으로는 반복자 사용해서 반복적으로 복사하는 방법이 있다.
clone()
쓰는 메서드들은, 재정의하게 나두면 안된다.clone()
이 쓰는 메서드를 재정의할 수 있게 하면 clone
이 오동작할 수 있으므로 그런 메서드들은 private
이나 final
이어야 한다.Cloneable
을 구현해서는 안된다. -> clone()
을 protected
로 두고, 예외를 던진다. 복사 생성자나 복사 팩터리 이용
public PhoneNumber(PhoneNumber phoneNumber) {
this(phoneNumber.areaCode, phoneNumber.prefix, phoneNumber.lineNum);
}
이렇게 생성자를 써서 객체를 반환하자.
복사 생성자를 쓰면 생성할 때 값을 설정해주므로 final
필드여도 괜찮다.
(clone
을 사용하면 새로운 객체를 할당해야 해서 final
불가)
그리고 복사 생성자에서 복사할 객체의 상위 타입을 받는다고 지정해놓으면,
하위타입 객체라면 모두 받을 수 있기 때문에 보다 유연해진다.
복사 팩터리로도 바꿀 수 있다.
public static PhoneNumber newInstance(PhoneNumber phoneNumber){...}
결론 : clone 쓰지 말고 복사생성자/복사팩터리를 사용하자!