코드 13-3 잘못된 clone 메서드 - 가변 상태를 공유한다!
// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드 (79쪽)
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
- 복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여
- 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다
- 이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다
코드 13-4 복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드
public class HashTable implements Cloneable{
private Map.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(){
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 AssertionError();
}
}
... // 나머지 코드 생략
}
- private 클래스인 HashTable.Entry는 깊은 복사(deep copy)를 지원하도록 보강되었다
- HashTable의 clone 메서드는 먼저 적절한 크기의 새로운 버킷 배열을 할당한 다음
- 원래 버킷 배열을 순회하며 비지 않은 각 버킷에 대해 깊은 복사를 수행한다
- 이때 Entry의 deepCopy 메서드는 자신이 가리키는 연결리스트 전체를 복사하기 위해서 자신을 재귀적으로 호출
- 하지만 연결리스트로 복제하는 방법은 그다지 좋지 않다
- 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하여,
- 리스트가 길면 스택 오버플로를 일으킬 위험이 있다
코드 13-5 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다.
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;
}
복잡한 가변 객체를 복제하는 마지막 방법을 살펴보자
- super.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한 다음, 원복 객체의 상태를 다시 생성하는 고수준 메서드를 호출
- HashTable 예라면, buckets 필드를 새로운 버킷 배열로 초기화한 다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해
- 복제본 테이블의 put 메서드를 호출해 둘의 내용을 같이 해준다
생성자에서는 재정의 될 수 있는 메서드를 호출하지 않아야 하는데 clone 메서드도 마찬가지다
- 만약 clone이 하위 클래스에서 재정의한 메서드를 호출하면 , 하위 클래스는 복제 과정에서 자신의 상태를 교정할 기회를 잃게 되어
- 원본과 복제본의 상태가 달라질 가능성이 크다.
- put 메서드는 final 이거나 priave 이어야 한다
Object의 clone 메서드는 CloneNotSupportedException을 던진다고 선언했지만 재정의한 메서드는 그렇지 않다
- public인 clone 메서드에서는 throws 절을 없애야 한다
- 검사 예외를 던지지 않아야 그 메서드를 사용하기 편하기 때문이다
코드 13-6 하위 클래스에서 Cloneable을 지원하지 못하게 하는 clone 메서드
@Override
protected final Object clone() throws CloneNotSupportedException(){
throw new CloneNotSupportedException();
}
- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다
- Object의 clone 메서드는 동기화를 신경 쓰지 않았다
- 그러니 super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다
요약 : Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다
- 접근자 : public
- 반환 타입 : 클래스 자신
- 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다
- 일반적으로 이 객체의 내부 '깊은 구조'에 숨어 있는 모든 가변 객체를 복사하고,
- 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻한다
이 모든 작업이 꼭 필요한 걸까?
- Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다
- 그렇지 않은 상황에서는 복사 생성자와 복사 팩토리라는 더 나은 객체 복사 방식을 제공할 수 있다
코드 13-7 복사 생성자
public Yum(Yum) {...};
코드 13-8 복사 팩터리
public static Yum newInstance(Yum yum){...};
- 복사 생성자와 그 변형인 복사 팩토리는 Cloneable/clone 방식보다 나은 면이 많다
- 언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식) 사용하지 않으며,
- 엉성하게 문서화된 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않으며,
- 불필요한 검사 예외를 던지지 않고, 형 변환도 필요치 않다
여기서 끝이 아니다
- 복사 생성자와 복사 팩토리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다
- 관례상 모든 범용 컬렉션 구현체는 Collection이나 Map 타입을 받는 생성자를 제공한다
- 인터페이스 기반 복사 생성자와 복사 팩토리의 더 정확한 이름은 '변환 생성자'와 '변환 팩토리' 다
- 이들을 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다
Cloneable이 몰고 온 모든 문제를 뒤집어봤을 때, 새로운 인터페이스를 만들 때는 절대
Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.
final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서는 검토한 후
별다른 문제가 없을 때만 드물게 허용해야 한다. 기본 원칙은 '복제 기능은 생성자와 팩토리를 이용하는 게 최고'
단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다
'Books > Effective-Java 3판' 카테고리의 다른 글
15. 클래스와 맴버의 접근 권한을 최소화하라 (0) | 2021.10.11 |
---|---|
14. Comparable을 구현할지 고려하라 (0) | 2021.10.10 |
13. clone 재정의는 주의해서 진행하라(1) (0) | 2021.10.05 |
12. toString을 항상 재정의하라 (0) | 2021.10.03 |
11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.10.02 |