이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 13. clone 재정의는 주의해서 진행하라
- Cloneable Interface
- 메서드가 하나도 없는 특이한 인터페이스이며, 이런식으로 인터페이스를 구현하면 안된다.
- clone() 을 사용하기 위해서 구현해야하는 인터페이스.
- clone() 메서드는 Cloneable 인터페이스 안에 없고 Object에 있으며, protected로 정의되어있다.
- Cloneable을 구현한 클래스의 인스턴스에서 clone() 메서드를 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며,
구현하지 않은 클래스의 인스턴스에서 clone() 메서드를 호출 시 CloneNotSupportedException을 예외를 발생시킨다. - clone 메서드는 사실상 생성자와 같은 효과를 낸다.
즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. - 참조 타입의 경우, 값을 복사하는 것이 아닌 해당 값을 가지고 있는 메모리의 주소를 복사한다. 이것을 얕은 복사라고 함
- 값 타입의 경우, 값 자체를 복사한다. 이것을 깊은 복사라고 하며, 얕은 복사보다는 시간이 많이 소요된다.
- Cloneable 을 구현하는 모든 클래스는 clone을 재정의해야 한다.
- 접근 제한자는 public 으로, 반환 타입은 클래스 자신으로 변경한다.
- 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없다.
- 일련번호나 고유 ID는 비록 기본 타입이나 불변일지라도 수정해줘야 한다.
- Clone 메서드의 일반 규약
(Object 명세에서 가져온 규약이며 허술하지만, 생성자를 호출하지 않고 객체 생성이 가능)
추가로, clone() 메서드에서 super.clone() 메서드가 아닌 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러에서 문제가 없음. 하지만, 하위 클래스에서 super.clone() 메서드를 호출하면 잘못된 클래스의 인스턴스가 생성되니 조심해야한다.
- x.clone() != x
이 식은 참이다. - x.clone().getClass() == x.getClass()
이 식 역시 참이다. 하지만 반드시 만족해야 하는 것은 아니다. - x.clone().getClass().equals(x)
이 식은 참이지만 필수는 아니다. - x.clone().getClass() == x.getClass()
만약, super.clone() 을 호출해 얻은 객체를 clone()이 반환한다면 이 식은 참이다. 또한, 관례상 반환된 객체와 원본객체는 독립적이어야 한다. 이를 만족하려면 super.clone()으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다
- 가변 상태를 참조하지 않는 클래스용 clone 메서드
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throws new AssertionError(); // Cloneable 인터페이스를 상속한 경우, 일어날 수 없는 일
}
}
- 가변 상태를 참조하지 않는 클래스용 clone 메서드
(아이템 7에서 소개한 Stack Class)
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];
}
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;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
1) super.clone()을 그대로 하는 경우 → 얕은 복사(Shadow Copy)
복사한 값을 가지지만 실제로는 값이 아닌 주소를 복사하여 원본 Stack 인스턴스와 같은 배열을 참조한다.
2) elements 배열의 주소가 아닌 값을 복사하기 위해서는 배열.clone() 을 사용 → 깊은 복사(Deep Copy)
배열의 clone 기능이 clone 기능을 제대로 사용하는 유일한 예이다.
(만약 elements가 final 이었다면, 위의 방식은 사용 X)
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- clone을 재귀적으로 호출하는 것만으로는 충분하지 않은 경우
(HashTable Class)
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Obejct key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
...
}
1) 잘못된 clone 메서드(가변 상태를 공유함)
복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 있다.
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
2) 일반적인 해법
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Obejct key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// buckets의 사이즈가 크다면 스택오버플로우가 발생할 수 있다.
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();
}
}
...
}
3) 2번의 스택오버플로우를 방지하는 방법
재귀호출 대신 반복하는 구조로 수정
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;
}
- Cloneable 인터페이스 대신 복사 생성자와 복사 팩터리를 사용
* 복사 생성자 : 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자
public Yum(Yum yum) {
...
}
* 복사 팩터리 : 복사 생성자를 모방한 정적 팩터리이다.
public static Yum newInstance(Yum yum) {
...
}
- 복사 생성자와 복사 팩터리는 Cloneable/clone 방식처럼 정상적인 final 필드 용법과 충돌하지 않으며, 엉성하게 문서화된 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않으며, 불필요한 검사 예외를 던지지 않고, 형변환도 필요하지 않다.
- 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
→ 이를 이용하면 복제본 타입을 선택하는데 있어 유연성이 향상될 수 있다.
- 정리
- 새로운 인터페이스를 만들때 절대 Cloneable을 확장하지 말자.
- final 클래스라면 성능 최적화 관점에서 검토한 이후 문제가 없을 때에만 Cloneable 인터페이스를 구현하자.
- 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고' 이다.
- 단, 배열같은 경우는 clone방식을 가장 적합하므로 예외로 친다.
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.09.16 |
---|---|
[Effective Java] 아이템 14. Comparable을 구현할지 고려하라 (0) | 2022.09.15 |
[Effective Java] 아이템 12. toString을 항상 재정의하라. (0) | 2022.09.13 |
[Effective Java] 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.09.12 |
[Effective Java] 아이템 10. equals는 일반 규약을 지켜 재정의하라 (2) | 2022.09.11 |