Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 mixin interface지만, 의도한 목적을 제대로 이루지 못했다. 가장 큰 문제는 clone 메서드는 선언된 곳이 Cloneable이 아닌 Object이고, protected라는데 있다.
Cloneable 인터페이스는 protected 메서드인 clone의 동작 방식을 결정한다. clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환한다.
clone 메서드의 일반 규약으로 Object 명세에서 가져온 설명은 다음과 같다.
이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.
x.clone() != x
또한 다음 식도 참이다.
x.clone().getClass() == x.getClass()
하지만, 이상의 요구를 반드시 만족해야 하는 것은 아니다.
한편, 다음 식도 일반적으로 참이지만 필수는 아니다.
x.clone().equals(x)
관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와(Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.
x.clone().getClass() == x.getClass()
관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
강제성이 없다는 점만 빼면 생성자 연쇄와 비슷한 매커니즘이다.
쓸데없는 복사를 지양한다는 관점에서 보면 불변 클래스는 굳이 clone메서드를 제공하지 않는게 좋다. 이 점을 고려햐 PhoneNumber의 clone 메서드는 다음처럼 구현할 수 있다.
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError();
}
}
이 메서드가 동작하려면 PhoneNumber 클래스 선언에 Cloneable을 구현한다고 추가해야 한다. 자바가 공변 반환 타이핑을 지원하므로 권장하는 방식이다.
[ 가변 객체를 참조하는 클래스의 clone 메서드 ]
간단했던 앞서의 구현이 클래스가 가변 객체를 참조하는 순간 재앙으로 돌변한다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULR_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULR_INITIAL_CAPACITY];
}
// ...
}
이 클래스에 단순히 clone 메서드가 super.clone의 결과를 그대로 반환한다면, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하게 된다. 이러면 원본이나 복제본 중 하나만 변경해도 다른 하나도 수정되어 불변식을 해치게 된다.
clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch(CloneNotSupportedException e) {
throw new AssertionError();
}
}
배열의 clone은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다.
[ HashTable용 clone 메서드 ]
이번에는 HashTable용 clone 메서드를 생각해보자.
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;
}
}
// ...
}
여기에 단순히 버킷 배열의 clone을 재귀적으로 호출해보면 문제가 생긴다. 복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조한다. 이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다. 다음은 일반적인 해법이다.
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];
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 AssertinError();
}
}
private 클래스인 HashTable.Entry는 깊은 복사를 지원하도록 보강되었다.
HashTable의 clone 메서드는 먼저 적절한 크기의 새로운 버킷 배열을 할당한 다음, 원래의 버킷 배열을 순회하며, 비어있지 않은 각 버킷에 대해 깊은 복사를 수행한다.
잘 작동하지만, 리스트가 길면 스택 오버플로를 일으킬 위험이 있다. 이 문제를 피하려면 재귀 호출 대신 반복자를 써서 순회하는 방향으로 수정해야 한다.
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;
}
[ 변환 생성자와 변환 팩터리 ]
이렇게 복잡한 작업보다 훨씬 나은 방법인 변환 생성자와 변환 팩터리가 있다. 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.
// 변환 생성자
public Yum(Yum yum) {...};
// 변환 팩터리
public static Yum newInstance(Yum yum) {...};
이는 객체 생성 메커니즘(생성자를 쓰지 않는 방식)을 사용하지 않으며, 정상적인 final 필드 용법과도 충돌하지 않으며, 불필요한 검사 예외를 던지지 않고, 형변환도 필요하지 않다.
또한, 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다. 이들을 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고, 복제본의 타입을 직접 선택할 수 있다.
예를 들어 HastSet 객체 s를 TreeSet 타입으로 복제할 수 있다. clone으로는 불가능한 이 기능을 변환 생성자로는 간단히 new TreeSet<>(s)로 처리할 수 있다.
'Developer's_til > Effective Java' 카테고리의 다른 글
[item 15] 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2022.10.28 |
---|---|
[item 14] Comparable을 구현할지 고려하라 (0) | 2022.10.25 |
[item 11] equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.10.19 |
[item 10] equals는 일반 규약을 지켜 재정의하라 (0) | 2022.10.13 |
[item 6] 불필요한 객체 생성을 피하라 (2) | 2022.10.03 |