메서드 하나 없는 Cloneable 인터페이스는 대체 무슨 일을 할까?
- Object의 protected 메서드인 clone의 동작 방식을 결정한다
- Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며,
- 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다
- 인터페이스를 상당히 이례적으로 사용한 예이니 따라하지는 말자
인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위
- 그런데 Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다.
실무에서는 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며,
사용자는 당연히 복제가 제대로 이뤄지리라 기대한다
- 이 기대를 만족시키려면 그 클래스와 모든 상위 클래스는 복잡하고, 강제할 수 없고
- 허술하게 기술된 프로토콜을 지켜야만 하는데,
- 그결과로 깨지기 쉽고, 위험하고, 모순적인 메커니즘이 탄생한다
- 생성자를 호출하지 않고도 객체를 생성할 수 있게 되는 것이다
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 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않을 것이다
- 하지만 이 클래스의 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져,
- 결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다
- 'clone'을 재정의한 클래스가 final이라면 걱정해야 할 하위 클래스가 없으니 이 관례는 무시해도 안전하다
- 하지만 final 클래스의 clone 메서드가 super.clone을 호출하지 않는다면 Cloneable을 구현할 이유도 없다
- Object의 clone 구현의 동작 방식에 기댈 필요가 없기 때문이다
제대로 동작하는 clone 메서드를 가진 상위 클래스를 상속해 Cloneable을 구현하고 싶다고 해보자!
- 먼저 super.clone을 호출한다
- 이렇게 얻은 객체는 원본의 완벽한 복제본이다
- 클래스에 정의된 모든 필드는 원본 필드와 똑같은 값을 갖는다
- 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 이 객체는 완벽히 우리가 원하는 상태다
코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드 (79쪽)
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
- 메서드가 동작하게 하려면 PhoneNumber의 클래스 선언에 Cloneable을 구현한다고 추가
- Object의 clone 메서드는 Object를 반환하지만 PhoneNumber의 clone 메서드는 PhoneNumber를 반환하게 했다.
- 자바가 공변 반환 타이핑을 지원하니 이렇게 하는 것이 가능하고 권장하는 방식이다
- 재정의한 메서드의 반환 타입은 상위 클래스 메서드가 반환하기 전에 PhoneNUmber로 형 변환하였다
super.clone 호출을 try-catch 블록으로 감싼 이유는?
- Object의 clone 메서드가 검사 예외인 CloneNotSupportedException을 던지도록 선언되었기 때문이다
- PhoneNumber가 Cloneable을 구현하니, 우리는 super.clone이 성공할 것임을 안다
- 이 거추장스러운 코드는 CloneNotSupportedException이 사실은 비검사 예외였어야 했다는 신호다
클래스가 가변 객체를 참조하는 순간 재앙으로 돌변한다
// Stack의 복제 가능 버전 (80-81쪽)
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);
}
}
- clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면 어떻게 될까?
- 반환된 Stack 인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조
- 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다
- 결국에는 NullPointerException을 던질 것이다
Clone 메서드는 사실상 생성자와 같은 효과를 낸다.
즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다
코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
// 코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- Stack의 clone 메서드는 제대로 동작하려면 스택 내부 정보를 복사해야 하는데,
- 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해주는 것이다
- elements.clone의 결과를 Object []로 형 변환할 필요는 없다
- 배열의 clone은 런타임 타입과 컴파일 타임 타입 모두가 원본 배열과 똑같은 배열을 반환한다
- 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다
elements 필드가 final이었다면 앞서의 방식은 작동하지 않는다 -> final 필드에는 새로운 값을 할당할 수 없기 때문이다
Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 출동한다
- 단, 원본과 복제된 객체가 그 가변 객체를 공유해도 안전하다면 괜찮다.
- 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다
clone을 재귀적으로 호출하는 것 만으로는 충분하지 않을 때도 있다. 이번에는 해시 테이블용 clone 메서드를 생각해보자
- 해시테이블 내부는 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결 리스트의 첫 번째 엔 트릴 참조한다
- 그리고 성능을 위해 java.util.LinkedList 대신 직접 구현한 경량 연결 리스트를 사용하겠다
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;
}
}
...// 나머지 생략
}
'Books > Effective-Java 3판' 카테고리의 다른 글
14. Comparable을 구현할지 고려하라 (0) | 2021.10.10 |
---|---|
13. clone 재정의는 주의해서 진행하라(2) (0) | 2021.10.06 |
12. toString을 항상 재정의하라 (0) | 2021.10.03 |
11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.10.02 |
10.equals는 일반 규약을 지켜 재정의하라(3) (0) | 2021.10.01 |