[Java] - Object 클래스 (clone)

janjanee·2021년 6월 11일
0

Java

목록 보기
6/17
post-thumbnail

clone()

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

공변 반환타입(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);
    }
}

반환타입을 자손 클래스 타입으로 변경했기 때문에 사용하는 코드에서 불필요한 형변환이 줄어드는 장점이 있다.


얕은복사(shallow copy) / 깊은복사(deep copy)

clone()은 단순히 객체에 저장된 값을 그대로 복사할 뿐, 객체가 참조하고 있는 객체까지
복사하지는 않는다. 이러한 복사를 '얕은복사(shallow copy)'라고 한다.

  • 얕은 복사(shallow copy)
    • 원본과 복사본이 같은 객체를 공유
    • 원본을 변경하면 복사본도 영향을 받는다.
  • 깊은 복사(deep 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의 이름을 변경해도 깊은 복사이기 때문에 서로 영향을 주지 않는다.

References

  • 남궁성, 『자바의 정석』, 도우출판(2016)
profile
얍얍 개발 펀치

0개의 댓글