자바의 참조 변수는 heap 영역에 데이터를 저장하고 그 주소 값을 저장하는 방식으로 구성되어 있습니다.
기본형 타입이 아닌, 객체와 같은 참조형 타입의 변수를 그대로 복제한다면, 주소값을 복사하여 같은 힙의 데이터를 가르키게 됨
이러한 복사를 얕은 복사라고 하며, 얕은 복사에서는 원본을 변경하면 복사본 또한 영향을 받습니다.
반면, 원본이 참조하고 있는 힙의 데이터까지 복제하는 것을 깊은 복사라고 하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 서로 영향을 미치지 않습니다.
복사 생성자 사용
public class Person {
private String name;
private Address address;
// 복사 생성자
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address); // 깊은 복사
}
}
정적 팩토리 메서드 사용
public class Person {
private String name;
private Address address;
// 복사 생성자
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address); // 깊은 복사
}
public static Person copyOf(Person other) {
return new Person(other);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동", new Address("서울"));
// 복사 생성자 사용
Person p2 = new Person(p1);
// 정적 팩토리 메서드 사용
Person p3 = Person.copyOf(p1);
System.out.println(p1 == p2); // false
System.out.println(p1 == p3); // false
}
} Object.clone() 메서드 사용
Object.clone() 메서드는 인스턴스 객체를 복제하기 위한 메서드로, 해당 인스턴스를 복제하여 새로운 인스턴스를 생성하여 참조 값을 반환합니다.
clone() 메서드를 사용하기 위해서는 오버라이딩 해야만 하며, 이때 Cloneable 인터페이스를 구현해야만 함
또한, clone() 메서드는 기본적으로 protected 접근 권한이기 때문에 상속하여 public 접근 제어자로 오버라이딩하여 어디에서나 사용할 수 있게 해야 합니다.
class Person implements Cloneable {
...
// clone 메서드를 오버라이딩
// CloneNotSupportedException는 checked exception 이라 반드시 예외 처리 해야 함
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) {
try {
Person p = new Person("홍길동", 11);
Person p_copy = (Person) p.clone();
sout(p.hashCode()); // 12312534
sout(p_copy.hashCode()); // 34212431
sout(p.equals(p_copy))) // false
} catch(Exception e) {}
}
}
clone() 메서드를 사용할 때, 단일 클래스 타입은 괜찮지만 클래스 타입이 여러 개 있는 배열을 복제할 때는 주의 사항이 존재
객체 배열도 참조 타입이며, 가지고 있는 요소 역시 참조 타입
class User {
int id;
String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
User[] array = {
new User(1, "park"),
new User(2, "lee"),
new User(3, "jung")
};
System.out.println(Arrays.toString(array));
// [main$1MyObject@251a69d7, main$1MyObject@7344699f, main$1MyObject@6b95977]
User[] array2;
array2 = array.clone();
System.out.println(Arrays.toString(array2));
// [main$1MyObject@251a69d7, main$1MyObject@7344699f, main$1MyObject@6b95977]
// 배열을 복사해도 내용물 객체의 주소는 같음
System.out.println(array[0].id); // 1
array2[0].id = 999;
System.out.println(array2[0].id); // 999
System.out.println(array[0].id); // 999
}
}
참조 객체인 배열 자체는 깊은 복사가 되었지만, 배열 요소 객체는 얕은 복사가 되어 있음을 볼 수 있습니다.
이렇듯, 클래스를 복사할 경우 clone() 오버라이딩 해야 하며, 배열의 경우에는 for문을 돌며 요소들을 직접 clone() 해줘야 함
clone() 메서드를 사용하기 위해서는 Clonable 인터페이스를 구현하여, Object.clone()을 protected에서 public으로 오버라이드해야 하며, super.clone()은 객체의 필드만 단순 복사하기 때문에 List 또는 배열은 깊은 복사를 할 수 없습니다.
또한, clone() 메서드는 실패 시 CloneNotSupportedException를 던지도록 되어 있으므로 예외 처리를 반드시 해줘야 함
따라서, 되도록이면 복사 생성자/정적 팩터리 메서드 사용을 권장