clone 재정의는 주의해서 진행하라
/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* {@code Object.clone} (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the {@code clone} method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since 1.0
*/
public interface Cloneable {
}
Cloneable
인터페이스는 메서드가 하나도 없는 인터페이스다. 이 인터페이스는 Object 클래스의 protected 메서드인 clone의 동작 방식을 결정한다.- Cloneable을 구현한 인스턴스에서
clone
을 호출하면 그 객체의 필드를 모두 복사한 객체를 반환하고, 그렇지 않은 인스턴스에서 호출하면CloneNotSupportedException
예외를 던진다.
Cloneable 같은 메서드가 하나 없는 인터페이스는 해당 클래스가 본인의 기능 이외에 추가로 구현할 수 있는 자료형으로, 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다. 즉, 어떠한 구현할 추가 기능이 있음을 명시하는 믹스인(mixin) 인터페이스라고 한다.
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
- Object의 clone 메서드는 다음과 같이 선언되어 있다.
native
메서드는 C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 의미한다. Java Native Interface(JNI)라고 부른다. 즉, C나 C++의 코드를 자바에서 불러 사용하려면native
메서드를 정의해서 메서드 바디를 갖지 않는 메서드를 구현한다. (메서드 바디가 dll (Unix에선 so) 파일로 되어 있어 런타임 시에 dll 파일을 System.loadLibrary 메서드가 수행하여 classpath 경로에서 파라미터의 파일을 메모리에 로딩)@HotSpotIntrinsicCandidate
는 해당 메소드 가 HotSpot에 일련의 효율적인 구현을 포함하고 런타임에 JDK 소스 코드 구현을 대체할 수 있음을 의미한다.
인터페이스는 일반적으로 재사용을 위한 기능을 제공하는 행위를 하는데, Cloneable 인터페이스는 그렇지 않다. 해당 경우는 이례젹으로 사용한 케이스로 따라 하지는 말 것을 권하며, 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경하는 기능을 갖고 있다.
Cloneable 구현
class PhoneNumber implements Cloneable {
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
- 제대로 동작하는 clone 메서드를 구현하기 위해선 먼저
super.clone()
을 선언해야 한다.- 그 이유는 생성자로 구현해도 컴파일 오류는 발생하지 않지만 A, B, C가 있을 때 A의 인스턴스를 B, C가 동일하게 받기 때문에 최상위 부모의 인스턴스를 계속 받는 문제가 생기기 떄문에 반드시 부모의 인스턴스를 받도록 구현하기 위함이다.
- 부모의 타입을 받은 후 현재 인스턴스 타입으로 형변환 하여 반환한다. 자바는 공변 반환 타입 (covariant return typing)을 지원하기 때문에 절대 실패하지 않는다.
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack implements Cloneable {
private Object[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
@Override
public Stack clone() {
try {
Stack clone = (Stack) super.clone();
// TODO: copy mutable state here, so the clone can't change the internals of the original
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
- 다음 예시에서
Cloneable
인터페이스를 구현한 경우 clone 메서드에서 TODO를 자동 생성한다. 그 이유는 단순히 super.clone의 결과를 반환하면 반환된 Stack의 인스턴스의 size 필드는 올바른 값을 갖지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하게 된다. 즉, 원본 또는 복제본 하나라도 수정이 발생한다면, 다른 하나도 수정되어 불변식을 해치게 된다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- 다음과 같이 elements 배열의 clone을 재귀적으로 호출하는 것으로 해결가능하다. 배열의 clone은 런타임과 컴파일 시점에서 모두 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때 clone 메서드를 사용하라고 권장한다. 사실 배열은 clone 기능을 제대로 사용하는 유일한 예라고 볼 수 있다.
- 만약 elements 필드가 final이었다면 동작하지 않는다. 이는 해당 인터페이스의 근본적인 문제로 해당 아키텍쳐에서 명세하는 가변 객체를 참조하는 필드는 final로 선언하라와 충돌하게 된다. 따라서 Cloneable을 적용하기 위해 final을 제거하는 경우도 발생할 수 있다.
clone을 재귀적으로 호출함으로 가변 객체를 해결할 수 있었지만, 해결이 안되는 경우도 존재한다. 해시테이블은 내부적으로 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결리스트의 첫 번째 엔트리를 참조한다.
package effective.item13;
public class HashTable implements Cloneable{
private Entry[] bucket;
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.bucket = bucket.clone(); // 깊은 복사 보장
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
- 복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다. 이를 해결하기 위해 각 버킷을 구성하는 연결 리스트를 복사해야 한다.
- 재정의에서 버킷 배열을 깊은 복사해줬지만, 재귀 호출이 발생하므로 리스트의 원소 수만큼 스택 프레임을 소비하게 된다. 즉, 리스트가 길수록 스택 오버 플로우를 발생할 여지가 생긴다. 따라서 재귀 호출 대신 반복자를 써서 순회하는 방향이 더 효율적이다.
Entry deepCopy() { // 반복자 (for 문) 순회
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;
}
- 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
- 하위 클래스에서 Cloneable을 지원하지 못하도록 하는 메서드
정리
=
연산자를 사용하면 반드시 얕은 복사가 이루어지며, clone()
재정의는 1차원 배열만 깊은 복사 나머지는 얕은 복사가 되며, 이때 깊은 복사 설정을 위해 재정의하는 용도로 Cloneable
인터페이스를 사용하여 clone
메서드를 재정의한다. (얕은 복사는 원본과 복제 객체가 같이 변할 때, 깊은 복사는 독립적일 때를 의미)
새로운 인터페이스를 만들 때는 절대 Cloneable
인터페이스를 확장해선 안되며, 새로운 클래스에서도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 구현해도 위험 요소가 적지만, 드물게 허용하는 편이다.
복제의 기본 원칙은 생성자와 팩터리를 사용하는 것이 가장 바람직하다.
복사 생성자와 복사 팩터리 예시
public Yum(Yum yum) {}; // 복사 생성자
public static Yum newInstance(Yum yum) {}; // 복사 팩터리
REFERENCES
https://blog.daum.net/creazier/12250893
https://blog.krybot.com/a?ID=01750-c42d3d7c-ba8f-454b-b044-f9e712221c05