■ Cloneable
Cloneable은 복제해도 되는 클래스임을 알리기 위해 만들어진 믹스인 인터페이스이다. Java의 Cloneable 인터페이스를 보면 아무런 메소드가 보이지 않는다. 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected라는데 있다. 그래서 Cloneable을 구현하는 것 만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
Cloneable 인터페이스를 구현한 클래스는 Object의 메서드인 clone()을 어떤식으로 사용할 것인지를 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
일반적으로 클래스에서 인터페이스를 구현하면 해당 인터페이스에서 정의한 기능을 제공한다는 선언 행위이다. 그런데 Cloneabl의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다.
■ Clone() 메서드에 대한 Object 명세
어떤 객체 x에 대해 다음식은 참이다. 관례상 반환된 객체와 원본 객체는 독립적이어 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 숟고 있다.
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
■ Clone() 메서드 구현
다음은 가변상태를 참조하지 않는 클래스용 clone 메서드 구현 예시 코드이다. Object의 clone()은 Object를 반환하지만 이 클래스에서는 Foo 인스턴스를 반환한다.
public class Foo implements Cloneable{
private int foo;
public Foo(int foo){
this.foo = foo;
}
public int getFoo() {
return foo;
}
@Override
protected Foo clone() throws AssertionError {
try {
return (Foo)super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) {
Foo f1 = new Foo(10);
Foo f2 = f1.clone();
}
}
가변 객체를 참조하는 클래스의 clone 메서드를 구현할 때는 조심해야한다. elements필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하기 때문에 원본이나 복제본을 수정하면 다른 하나도 같이 수정된다. 따라서 이상하게 동작하거나 NullpointerException을 던질 것이다.
clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다. 따라서 Stack의 clone 메서드가 제대로 동작하려면 스택 내부 정보를 복사해야하는데, 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출하는 것 이다.
배열의 clone()은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환하므로 형변환이 필요 없다.
import java.util.Arrays;
import java.util.EmptyStackException;
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);
}
@Override
protected Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); //Object의 주소는 다르지만 Object안에 담겨있는 객체들의 레퍼런스는 같음
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
■ 복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드
다음 예제는 HashTable에 Entry 배열에 있는 Entry 인스턴스들을 복사할 때 깊은 복사를 할 수 있도록 구현한 예제 코드이다. HashTable.Entry는 깊은 복사를 지원하도록 deepCopy메서드를 만들어서 값만 복사해주고 있다. 이때 new를 이용하여 새로운 인스턴스를 만드는 것을 볼 수 있다.
연결 리스트를 복제하는 방법으로 재귀적으로 호출하는 것은 좋은 방법이 아니다. 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하여 리스트가 길면 스택 오버 플로우 에러가 날 수 있다. 이 문제를 피하려면 deepCopy 메서드를 재귀 호출 대신에 반복자를 사용하여 순회하는 방향으로 구현할 수 있다.
import java.util.ArrayList;
public class HashTable implements Cloneable{
private Entry[] buckets = new Entry[100];
private int size = 0;
public void printAll(){
for (int i=0;i<size;i++){
System.out.println(buckets[i].toString());
}
}
public void put(Entry entry){
buckets[size++] = entry;
}
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());
}
/*
* 리스트가 길 경우 stack over flow가 날 수 있으므로 반복문으로도 구현 가능
*/
/*
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;
}
*/
}
@Override
protected 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 static void main(String[] args) {
HashTable hashTable1 = new HashTable();
hashTable1.put(new HashTable.Entry("hoon1", 10, null));
HashTable.Entry entry1 = new HashTable.Entry("hoon2", 20, null);
HashTable.Entry entry2 = new HashTable.Entry("hoon3", 30, entry1);
hashTable1.put(entry2);
HashTable hashTable2 = hashTable1.clone();
//실행 결과 Entry까지 복사된걸 볼 수 있음
System.out.println("============hashTable1===============");
hashTable1.printAll();
System.out.println("============hashTable2===============");
hashTable2.printAll();
}
}
■ 복사 생성자와 복사 팩토리
객체를 복제하는 다른 방법으로 복사 생성자와 복사 팩토리 방식이 있다.
복사 생성자는 자신과 같은 클래스의 인스턴스를 매개 변수로 받는 생성자를 말한다.
public Yum(Yum yum) {...}
복사 팩토리는 복사 생성자를 정적 팩토리 형식으로 정의한 것이다.
public static Yum newInstance(Yum yum) {...}
복사 생성자와 복사 팩토리를 사용하면 Cloneable 방식처럼 불필요한 check exception 처리가 필요 없고, 형변환도 필요 없다. 또한, 직접적인 인스턴스가 아닌 인터페이스 타입의 인스턴스를 매개 변수로 받을 수 있어 유연성 또한 향상될 수 있는 장점이 있다.
결과적으로 객체의 복제 기능은 Cloneable보다 복사 생성자와 복사 팩토리를 이용하는 것이 가장 좋다라는 것이다. 하지만 배열 같은 경우는 clone() 메소드를 제대로 사용한 것이니 배열의 경우는 예외이다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템15 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2021.02.17 |
---|---|
[Effective Java] 아이템14 Comparable을 구현할지 고려하라 (0) | 2021.02.05 |
[Effective Java] 아이템12 toString을 항상 재정의하라 (0) | 2021.01.24 |
[Effective Java] 아이템11 equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.01.24 |
[Effective Java] 아이템9 try-finally 보다는 try-with-resources를 사용하라 (0) | 2021.01.24 |