...
clone 메소드
Object.clone() 메소드는 인스턴스 객체의 복제를 위한 메소드로, 해당 인스턴스를 복제하여 새로운 인스턴스를 생성해 그 참조값을 반환한다.
이러한 clone() 메소드를 사용하기 위해서는 오버라이딩을 해야 되는데, 이때 데이터의 보호를 이유로 Cloneable 인터페이스를 구현한 클래스의 인스턴스만이 사용할 수 있다.
Object 클래스의 clone() 메소드는 기본으로 protected 접근 권한을 갖고 있기 때문에, 상속하여 메소드를 public 접근제어자로 재정의하여 어디서나 복제가 가능하도록 해야 한다.
import java.lang.Cloneable;
// 객체 복사 메소드를 사용하기 위해서는 Cloneable 인터페이스를 구현해서 clone을 재정의 해야함
class Person implements Cloneable {
// ...
// clone 메서드를 오버라이딩
public Object clone() throws CloneNotSupportedException { // CloneNotSupportedException는 checked exception 이라 반드시 예외처리
return super.clone(); // 기본적으로 부모의 clone을 그대로 불러와 반환
}
}
public class Main {
public static void main(String[] args) {
try {
Person p = new Person("홍길동", 11);
Person p_copy = (Person) p.clone();
} catch(Exception e) {}
}
}
얕은 복사 vs 깊은 복사
자바의 객체와 같은 참조 변수는 직접 값을 저장하는게 아닌 힙(heap) 영역에 데이터를 저장하고 그의 주소값을 저장하는 식으로 구성되어 있다.
만일 int나 double 같은 기본형(primitive) 타입이 아닌, 객체와 같은 참조형(reference) 타입의 변수를 그대로 복제를 한다면 값이 복사되는 것이 아닌 주소값이 복사되어 결국 같은 힙의 데이터를 바라보는 꼴이 되어 버린다.
이러한 복제(복사)를 '얕은 복사(shallow copy)'라고 하며 얕은 복사에서는 원본을 변경하려면 복사본도 영향을 받는다
반면에 원본이 참조하고 있는 힙의 데이터까지 복제하는 것을 '깊은 복사(deep copy)'라고 하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.
// 객체 복사 메소드를 사용하기 위해서는 Cloneable 인터페이스를 구현해서 clone을 재정의 해야함
class User implements Cloneable {
private String name;
public void setName(String name) {
this.name = name;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) {
try {
// 얕은 복사(shallow copy)
User user = new User();
user.setName("Edward");
User copy = user;
System.out.println(user.hashCode()); // 622488023
System.out.println(copy.hashCode()); // 622488023
System.out.println(user.equals(copy)); // true - 둘이 동인할 힙데이터를 바라보고 있기 때문에
// 깊은 복사(deep copy)
User user2 = new User();
user2.setName("Edward");
User copy2 = (User) user2.clone();
System.out.println(user2.hashCode()); // 1933863327
System.out.println(copy2.hashCode()); // 112810359
System.out.println(user2.equals(copy2)); // false - 둘은 복사되어 생김새만 같지 다른 힙데이터를 바라보고 있기 때문에
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
객체 형태에 따른 깊은 복사 주의사항
위의 예제로 완벽히 객체가 복사되었다고 해서 맹신하면 된다.
간단한 단일 클래스 타입 정도는 위의 방식대로 그대로 복제가 가능하지만, 클래스 타입을 여러개 담고 있는 배열을 복제할 때는 주의 사항이 따른다.
객체 배열은 그 자체로도 참조 타입이며, 가지고 있는 요소의 값 역시 참조 타입이다. 이처럼 참조 타입의 데이터를 가지고 있는 참조 타입의 클래스 자료형을 clone 했을 경우 어떤 일이 일어나는지 보자.
import java.util.Arrays;
class MyObject{
int id;
String description;
MyObject(int id, String description) {
this.id = id;
this.description = description;
}
}
public class Main {
public static void main(String[] args) {
MyObject[] arrayObj = {
new MyObject(101, "first"),
new MyObject(101, "second"),
new MyObject(101, "third")
};
System.out.println(Arrays.toString(arrayObj)); // [main$1MyObject@251a69d7, main$1MyObject@7344699f, main$1MyObject@6b95977]
MyObject[] arrayObj2; // 복사할 배열
arrayObj2 = arrayObj.clone(); // 배열을 복사해도 내용물 객체의 주소는 똑같다.
System.out.println(Arrays.toString(arrayObj2)); // [main$1MyObject@251a69d7, main$1MyObject@7344699f, main$1MyObject@6b95977]
System.out.println(arrayObj[0].id); // 101
arrayObj2[0].id = 999; // 복사한 arrayObj2의 첫째 객체의 멤버를 변경
// 원본과 복사본의 값이 같다.
System.out.println(arrayObj2[0].id); // 999
System.out.println(arrayObj[0].id); // 999 : arrayObj1 의 첫째 겍체의 멤버도 변경됨
}
}
코드의 출력을 보면 알 수 있듯이, 참조 객체인 배열 자체는 완벽히 복사가 되었지만, 배열 내용물 객체는 얕은 복사가 되어 버려 원본과 배열본의 배열 요소가 담고있는 주소값이 같아 바라보고 있는 힙 데이터가 같게 되어버렸다.
그래서 마지막에 배열 요소의 필드값을 변경했더니 원본의 요소도 변경됨을 볼 수 있다.
즉, 배열 내용물은 여전히 같은 객체 주소를 가리키기 때문에 객체도 복사 되었는 줄 알고 복사한 객체의 멤버를 변경하면 복사된 멤버에 객체도 변경되는 꼴이 된다.
따라서 이러한 경우에는 복제되는 대상이며 배열의 요소 데이터인 MyObject 클래스에 Object.clone() 을 재정의 해줌으로서, 직접 for문을 돌며 객체 복사를 해주어야 한다.
즉, 데이터를 clone 하는 것의 핵심은, 데이터를 복사할때 int형이나 double형 같은 primitive 타입이 아닌, 클래스를 복사할 일이 있을 경우 clone() 을 오버라이딩 하여야 완벽히 복사가 행해진다는 점이다.
만일 다루는 데이터의 형태가 객체와 같은 reference 타입일 경우 clone 메서드를 재정의해야 된다라고 암기하자!
package house;
import java.util.Arrays;
class MyObject implements Cloneable{
int id;
String description;
MyObject(int id, String description) {
this.id = id;
this.description = description;
}
@Override // 공변 반환 타입을 이용한 오버라이딩
public MyObject clone() throws CloneNotSupportedException {
return (MyObject) super.clone();
}
}
public class Test1 {
public static void main(String[] args) {
try {
MyObject[] arrayObj = {
new MyObject(101, "first"),
new MyObject(102, "second"),
new MyObject(103, "third")
};
System.out.println(Arrays.toString(arrayObj)); // [MyObject@251a69d7, MyObject@7344699f, MyObject@6b95977]
MyObject[] arrayObj2 = new MyObject[3];
for (int i = 0; i < arrayObj.length; i++) {
arrayObj2[i] = arrayObj[i].clone();
}
// 배열 내용물 객체의 @주소가 달라짐을 볼 수 있다.
System.out.println(Arrays.toString(arrayObj2)); // [MyObject@1540e19d, MyObject@677327b6, MyObject@14ae5a5]
System.out.println(arrayObj[0].id); // 101
arrayObj2[0].id = 999; // 복사한 arrayObj2의 첫째 객체의 멤버를 변경
// 원본과 복사본의 값이 다르게 된다.
System.out.println(arrayObj2[0].id); // 999
System.out.println(arrayObj[0].id); // 101
} catch (Exception e) {}
}
}
[ 공변 반환 타입 ]
본래 오버라이딩은 부모의 메서드와 시그니처가 같아야 가능하다. 즉, 아규먼트 갯수나 메서드 반환 타입이 완벽히 일치해야 된다는 말이다.
JDK 1.5부터 '공변 반환 타입(convariant return type)'이 추가되었는데, 이 기능은 오버라이딩 할 때 부모 메서드의 반환 타입을 자식 클래스의 타입으로 변경을 허용하는 것이다.
공변 반환 타입을 사용하면 부모의 타입이 아닌, 실제 반환되는 자식 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어든다는 장점이 있다.
# 참고자료
https://www.youtube.com/watch?v=Mc6OaicCZVA
https://www.youtube.com/watch?v=NI6QZy6juc8
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.