2023. 11. 24. 16:01ㆍBook/이펙티브 자바
- 접근 제한자는 public, 반환 타입은 자신의 클래스로 변경한다.
- super.clone을 호출한 뒤 필요한 필드를 적절히 수정한다.
1. 배열을 복제할 때는 배열의 clone 메서드를 사용
2. 경우에 따라 final을 사용할 수 없을 수도 있다.
3. 필요한 경우 deep copy를 해야한다.
4. super.clone으로 객체를 만든 뒤, 고수준 메서드를 호출하는 방법도 있다.
5. 오버라이딩 할 수 있는 메서드는 호출하지 않도록 조심해야 한다.
6. 상속용 클래스는 Cloneable을 구현하지 않는 것이 좋다.
7. Cloneable을 구현한 안전 클래스를 작성할 때는 동기화를 해야 한다.
package effective.study.chapter03.Item13.inheritance;
import effective.study.chapter02.item07.stack.EmptyStackException;
import effective.study.chapter03.Item13.PhoneNumber;
import java.util.Arrays;
// Stack의 복제 가능 버전 (80-81쪽)
public class Stack {
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;
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
위 클래스의 clone 메서드를 앞서 했던 것 처럼 재정의 해 보자
1. Cloneable인터페이스를 구현하고(안에 아무것도 없는 인터페이스)
2. clone 메서드를 재정의한다. super.clone()을 사용한다.
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
super.clone 의 결과를 그대로 반환한다면 원시 타입인 size는 올바른 값을 갖겠지만, 참조 타입인 elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
=> 원본 or 복제본의 elements 필드를 수정시 참조하고 있는 두 객체 모두 수정됨
1.Stack의 clone이 제대로 동작하려면 배열 같은 경우는 간단하게 elements 배열의 clone을 호출하면 된다.
@Override public Stack clone() {
//TODO 배열을 clone하지 않으면 원본과 복사본이 동일한 배열을 참조하게 된다.
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
2. 한편 elemnts 필드가 final이었다면 위 방식은 작동하지 않는다.
super.clone()으로 객체를 할당 받고 그 필드만 수정 할 수 없기 때문이다. 이는 근본적인 문제로 직렬화와 마찬가지로 Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다.(원본과 복사본이 필드를 공유해도 상관 없다면 괜찮다.) 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서는 final 한정자를 제거야해 할 수도 있다.
이마저도 충분하지 않을 때도 있다.
public class HashTable implements Cloneable {
private Entry[] buchkets = ...;
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 메서드 - 가변 상태 공유
@Override public HashTable clone() {
//TODO 배열을 clone하지 않으면 원본과 복사본이 동일한 배열을 참조하게 된다.
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
복제본은 자신만의 버킷 배열을 갖지만, 이 배열 안에 있는 next는 원본과 공유하는 참조 객체다.(얕은 복사 때문)
3.이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다.
다음은 깊은 복사를 이용한 일반적인 해법이다.
public ㅊlass HashTable implements Cloneable {
private Entry[] buchkets = ...;
private 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() {
return new Entry(key, value,
next = null? null : next.deepCopy();
}
}
... 생략
//잘못된 clone 메서드 - 가변 상태 공유
@Override public HashTable clone() {
//TODO 배열을 clone하지 않으면 원본과 복사본이 동일한 배열을 참조하게 된다.
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for(int i = 0; i < buckets.length; i++) {
result.buckets[i] = buckets[i].deepCopy();
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
하지만 연결 리스트를 복제하는 방법은 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하여, 리스트가 길면 스택 오버플로를 일으킬 수 있다.
* 스레드는 각각 스택을 가지고 있고 그 안에 스택 프레임을 쌓고 스택 프레임은 메서드 호출시 쌓인다.
그러니 좋은 방법은 재귀 호출이 아닌 반복자를 써서 순회하는 방향으로 순회해야 한다.
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;
}
4. 마지막 방법으로는 super.clone을 통하여 객체를 만든 뒤 객체를 초기화 하고 고수준 메서드를 통하여 clone 객체 안에 똑같은 값을 채워주면 된다.
=> HashTable에서는 buckets 필드를 새로운 버킷 배열로 초기화 한 후 원본 테이블에 담긴 모든 키-값 쌍 각각에 복제본 테이블의 put(key,value) 메서드를 호출
5. 하지만 오버라이딩 할 수 있는 메서드는 호출하지 않도록 조심해야 한다.
만약 clone이 하위 클래스에서 재정의한 메서드에 의지해 호출하면 하위 클래스는 자신의 상태를 교정시킬 수 있는 기회를 일게 되어 원본과 복제본의 상태가 달라질 가능성이 크다.(하위 타입에서 super.clone 호출시 자식이 재정의한 메서드를 호출하게 됨 그러니 재정의 하지 못하도록 final 을 붙여줘야 한다.)
6. 상속에서 쓰기 위한 클래스 설게 방식 두 가지가 있다. 그리고 둘 다 Cloneable을 구현해서는 안 된다.
1. Object의 방식을 모방해 클론을 지원하지 않을 수 있다는 에러를 던져 클라이언트가 clone을 할지 말지 정하게 둔다.
2. clone을 동작하지 않게 구현한다.
*7. 멀티 스레드 환경에서 Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 역시 Syncronized 하게 하자
결론!
이처럼 clone 재정의는 굉장히 머리가 아프다. 그러니 Cloneable을 구현하지말고 복사 생성자와 복사 팩터리를 사용하자
public class PhoneNumber {
private final short areaCode, prefix, lineNum
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
//복사 생성자
public PhoneNumber(PhoneNumber p) {
this(p.areaCode, p.prefix, p.lineNum);
}
public static PhoneNumber newInstance(PhoneNumber p) {
return new PhoneNumber(p.areaCode, p.prefix, p.lineNum);
}
}
'Book > 이펙티브 자바' 카테고리의 다른 글
Item15 - 클래스와 멤버의 접근 권한을 최소화하라 (2) | 2023.11.27 |
---|---|
Item14 - Comparable을 구현할지 고민하라 (1) | 2023.11.25 |
Item13 - clone 재정의는 주의해서 진행하라 (2) | 2023.11.24 |
Item12 - toString을 항상 재정의하라 (1) | 2023.11.23 |
Item11 - equals를 재정의하려거든 hashCode도 재정의하라(2) (3) | 2023.11.22 |