
자바에서 객체 복사를 지원하기 위해 Cloneable 인터페이스와 clone() 메서드가 존재한다.
public class PhoneNumber implements Cloneable {
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone(); // ①
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // ② 발생할 수 없음
}
}
}
① super.clone()은 Object의 protected Object clone() 메서드를 호출한다.
② AssertionError()를 던진다.
처음에 읽을 때 왜 catch문 내부에서 AssertionError()를 던지는지 이해가 안됐었다.
이 클래스는 이미 Cloneable을 구현했기 때문에 예외가 발생할 일이 없기 때문이다.
catch 블록은 사실상 절대 실행되지 않는 코드다. 그래서 AssertionError를 던져 "일어날 수 없는 일이다."라는 것을 명시해주기 위해서 작성된 것이였다.
| 구분 | 얕은 복사(Shallow Copy) | 깊은 복사(Deep Copy) |
|---|---|---|
| 정의 | 필드의 참조값만 복사 | 참조 대상 객체 자체도 복사 |
| 특징 | 원본과 복사본이 같은 객체를 가리킴 | 독립적인 새로운 객체 생성 |
| 예시 | 배열 필드 복사 시, 배열 객체 자체는 공유 | 배열 필드 복사 시, 새로운 배열도 생성해서 복사 |
| 위험성 | 한 쪽을 수정하면 다른 쪽에도 영향 | 서로 완전히 독립적 |
clone() 메서드는 기본적으로 얕은 복사를 한다.
만약 깊은 복사를 해야된다면 clone()메서드 안에 별도로 복사 로직을 추가해줘야한다.
public class Stack {
private Object[] elements;
// 생성자 생략
@Override
public Stack clone() {
try {
return (Stack) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// 메서드 생략
}
위의 코드는 잘못된 clone() 메서드 재정의 방식이다.
원본 객체에 영향을 끼치지 않도록 하려면 아래와 같이 elements 배열의 clone을 재귀적으로 호출해줘야한다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.cloneO;
result.elements = elements.clone(); // 배열까지 복제(깊은 복사)
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
위의 예제에서 만약 elements 필드가 final이였다면 위의 코드는 작동하지 않을 것이다. final 키워드로 작성된 필드에는 새로운 값을 할당할 수 없기 때문이다.
자바의 HashTable처럼 내부에 가변 객체(예: 배열, 리스트, 키-값 쌍 등)를 가지고 있는 객체는 clone()을 재정의할 때 깊은 복사를 해주어야 한다.
public class MyHashTable implements Cloneable {
private Entry[] buckets;
static class Entry {
final String key;
String value;
Entry next;
Entry(String key, String 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 MyHashTable clone() {
try {
MyHashTable result = (MyHashTable) 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();
}
}
}
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
}
public class PhoneNumber {
private final int areaCode, prefix, lineNumber;
public PhoneNumber(PhoneNumber original) { // 복사 생성자
this.areaCode = original.areaCode;
this.prefix = original.prefix;
this.lineNumber = original.lineNumber;
}
public static PhoneNumber copyOf(PhoneNumber original) { // 복사 팩터리
return new PhoneNumber(original);
}
}