아이템 13 : clone 재정의는 주의해서 진행하라
Cloneable은 메서드가 없는 인터페이스입니다
clone 메서드는 원본 객체의 필드 값과 동일한 값을 가지는 새로운 객체를 생성해줍니다.
clone 메서드를 사용하기 위해서는 해당 클래스에서 Cloneable 인터페이스를 구현해주어야 합니다.
이러한 스펙이라면, 아래와 같은 생각이 들 수 있습니다. 저도 그랬구요.
Cloneable 인터페이스를 구현해야 clone 메서드를 사용할 수 있다면, Cloneable 인터페이스에 clone 메서드가 선언되어있겠군!
하지만, 아닙니다.
clone 메서드가 선언되어있는 곳은 Object이고, protected이며, Override 해주어야 하면 사용할 수 있습니다.
@Override
public PhoneNumber clone() { // public으로 선언
try { // 예외 처리를 메서드 내부에서 try-catch 구문으로 처리해 사용처에서 이 메서드를 사용하기 편하게 해줍니다.
return (PhoneNumber) super.clone(); // Object의 clone 메서드를 호출해줍니다.
} catch(CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일
}
}
Cloneable 인터페이스는 Object의 protected 메서드 clone의 동작 방식을 결정해주는 역할을 합니다. 객체를 복사할지, CloneNotSupportedException을 던질지를요.
확실히 통상적인 인터페이스 사용 방법은 아닌 것 같습니다.
제대로 사용하기 힘듭니다
쉽게 납득이 되는 방법은 아니지만, Cloneable 인터페이스를 구현하고 clone 메서드를 오버라이드 했다고 칩시다.
그럼 끝일까요? 불행히도 아닙니다.
Cloneable 인터페이스를 구현한 클래스가 불변 객체만을 참조하는 경우에는 아무런 문제가 없습니다만, 가변 객체를 참조하게 된다면 원본 인스턴스와 복제된 인스턴스 모두 동일한 가변객체를 참조하게 되기 때문에 복제 이후 문제가 발생할 수 있습니다.
public class Stack {
private Object[] element;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
...
}
위 클래스를 복제하게 된다면, 복제된 인스턴스가 생성자를 통해 생성된 것이 아니기 때문에 elements 필드는 원본 인스턴스와 동일한 배열을 참조하게 됩니다.
그래서 한쪽에서 elements의 변경이 발생하면 다른 한쪽에도 영향을 주게 되고, 버그로 이어질 수 있습니다.
물론 해결 방법은 있습니다. elements 배열의 clone을 재귀적으로 호출해주면 됩니다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
다만, 이러한 방법은 elements 필드가 final일 경우에는 통하지 않으며, 이는 final 필드에 새로운 값을 할당할 수 없기 때문입니다.
때문에 복제할 수 있는 클래스를 만들기 위해 final을 제거해야 합니다.
그리고 clone을 재귀적으로 호출하더라도 충분하지 않을 수 있습니다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
@AllArgsConstructor
private static class Entry {
final Object key;
Object value;
Entry next;
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
위 케이스에서 복제된 인스턴스는 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 동일한 연결 리스트를 참조하게 됩니다.
그래서 이러한 경우에는 아래와 같이 연결 리스트까지 복제해야 합니다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
@AllArgsConstructor
private static class Entry {
final Object key;
Object value;
Entry 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();
}
}
}
이 방법은 간단하긴 하지만, 재귀 호출로 인해 배열의 길이가 길어지면 스택오버플로우를 일으킬 수 있습니다.
복사 생성자와 복사 팩터리를 사용합시다
복사 생성자
자신과 같은 클래스의 인스턴스를 인수로 받습니다.
public Yum(Yum yum) { ... }
복사 팩터리
복사 생성자를 모방한 정적 팩터리 메서드입니다.
public static Yum newInstance(Yum yum) { ... }
이 두 가지 패턴에서는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 매개변수로 받을 수 있습니다.
배열은 clone을 사용하세요
배열만은 clone 메서드 방식이 가장 깔끔한 방식입니다.
'☕️ JAVA > Effective JAVA' 카테고리의 다른 글
[Effective Java] 아이템 68 : 일반적으로 통용되는 명명 규칙을 따르라 (0) | 2021.12.18 |
---|---|
[Effective Java] 아이템 49 : 매개변수가 유효한지 검사하라 (0) | 2021.12.04 |
[Effective Java] 아이템 33 : 타입 안전 이종 컨테이너를 고려하라 (0) | 2021.11.28 |
[Effective Java] 아이템 23 : 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2021.11.21 |
[Effective Java] 아이템 10 : equals는 일반 규약을 지켜 재정의하라 (0) | 2021.11.02 |