Object 클래스의 clone() 메소드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다.
단순히 인스턴스 변수의 값만 복사하기 때문에 참조타입의 인스턴스 변수가 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다.
바로 코드를 살펴보자.
class Book implements Cloneable {
String name;
int price;
}
clone()을 사용하려면, 복제할 클래스가 Cloneable
인터페이스를 구현해야 한다.
Cloneable 인터페이스를 들어가보면 아무런 내용이 없는 빈껍데기 인터페이스인데
해당 클래스는 복제가 가능하다. 라고 알려주는 역할이라고 생각하면된다.
public class CloneEx {
public static void main(String[] args) {
Book book = new Book("JPA Book", 10000);
Book bookCopy = book.clone(); // compile error
}
}
book.clone()을 했더니 컴파일 오류가 발생한다.
Object의 메소드이기 때문에 toString(), equals() 메소드 처럼 사용가능한게 아닌가?
// Object.java
protected native Object clone() throws CloneNotSupportedException;
Object 클래스의 clone() 메소드를 살펴보면 접근 제어자가 protected
이다.
CloneEx 클래스는 동일한 패키지(java.lang)나 상속관계가 아니므로 clone()을 호출할 수 없다.
그래서 컴파일 에러가 발생한 것이다.
따라서 상속관계가 없는 다른 클래스들에서 Book.clone()을 호출할 수 있도록 변경해보자.
class Book implements Cloneable {
String name;
int price;
@Override
public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
}
return obj;
}
}
clone() 메소드를 오버라이딩하면서 접근 제어자를 public
으로 변경하면 된다.
public class CloneEx {
public static void main(String[] args) {
Book book = new Book("JPA Book", 10000);
Book bookCopy = (Book)book.clone();
System.out.println(book);
System.out.println(copy);
}
}
Book{name='JPA Book', price=5}
Book{name='JPA Book', price=5}
이제는 정상적으로 book.clone()을 하면 Book을 복사할 수 있다.
공변 반환타입(covariant return type)을 이용해서 오버라이딩할 때 조상 메소드의 반환타입을
자손 클래스 타입으로 변경을 허용할 수도 있다.
코드로 이해해보자.
class Book implements Cloneable {
String name;
int price;
@Override
public Book clone() { // 반환타입을 Object -> Book으로 변경
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
}
return (Book)obj; // Book 타입으로 형변환
}
}
public class CloneEx extends ToStringEx{
public static void main(String[] args) {
Book book = new Book("JPA Book", 5);
Book copy = book.clone(); // 형변환 할 필요 없음.
System.out.println(book);
System.out.println(copy);
}
}
반환타입을 자손 클래스 타입으로 변경했기 때문에 사용하는 코드에서 불필요한 형변환이 줄어드는 장점이 있다.
clone()은 단순히 객체에 저장된 값을 그대로 복사할 뿐, 객체가 참조하고 있는 객체까지
복사하지는 않는다. 이러한 복사를 '얕은복사(shallow copy)'라고 한다.
아래 코드를 보자.
public class BookStore implements Cloneable {
private Book book;
private String name;
@Override
public BookStore clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {}
return (BookStore) obj;
}
...
위의 Book 클래스를 참조변수로 갖고있는 BookStore 클래스를 만들었다.
public static void main(String[] args) {
BookStore bs1 = new BookStore(new Book("JPA BOOK", 10000), "교보문고");
BookStore bs2 = bs1.clone();
// clone한 BookStore의 BOOK 이름을 JPA BOOK -> SPRING BOOK 변경
bs2.getBook().setName("SPRING BOOK");
// 원본 BookStore을 조회
System.out.println(bs1);
}
BookStore 인스턴스 bs1을 생성 후 clone()을 한다.
복제된 bs2의 Book의 이름을 "SPRING BOOK" 으로 변경했고,
bs1의 참조변수 Book이 변했는지 확인해보자.
BookStore{book=Book{name='SPRING BOOK', price=10000}, name='교보문고'}
bs1의 Book 이름이 SPRING BOOK으로 바뀐것을 알 수 있다.
얕은 복사이기 때문에 복사본을 변경했더니 원본(반대도 마찬가지)에 영향이 있다.
그럼 어떻게 하면 bs1과 bs2의 인스턴스가 각각 다른 Book 인스턴스를 가리키도록 할 수 있을까?
public class BookStore implements Cloneable {
private Book book;
private String name;
public BookStore deepCopy() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {}
BookStore bs = (BookStore) obj;
bs.setBook(new Book(this.book.getName(), this.book.getPrice()));
return bs;
}
...
deepCopy() 메소드를 새로 생성했는데 clone() 까지 동일한 코드이고
아래 두 줄을 더 추가했다.
복제된 객체가 새로운 Book 인스턴스를 참조하도록 했고, 새 인스턴스를 만들 때 값을 복사해서 넘기면 된다.
public static void main(String[] args) {
BookStore bs1 = new BookStore(new Book("JPA BOOK", 10000), "교보문고");
BookStore bs2 = bs1.clone();
// clone한 BookStore의 BOOK 이름을 JPA BOOK -> SPRING BOOK 변경
bs2.getBook().setName("SPRING BOOK");
System.out.println(bs1);
System.out.println(bs2);
}
BookStore{book=Book{name='JPA BOOK', price=10000}, name='교보문고'}
BookStore{book=Book{name='SPRING BOOK', price=10000}, name='교보문고'}
이제는 Book의 이름을 변경해도 깊은 복사이기 때문에 서로 영향을 주지 않는다.