[이펙티브 자바] 13. clone 재정의는 주의해서 진행하라

노을·2023년 2월 1일
0

이펙티브 자바

목록 보기
10/14
post-thumbnail

⭐ clone 메서드란?

원본 객체의 필드 값과 동일한 값을 가지는 새로운 객체를 생성해주는 메서드

clone 메서드는 Cloneable이라는 인터페이스를 implements 해야 쓸 수 있다.
하지만 아이러니하게도 clone 메서드는 Cloneable에 선언되어 있지 않고 Object에 선언되어 있다 (?)

clone 메서드의 일반 규약

  1. x.clone() != x
  2. x.clone().getClass() == x.getClass()
  3. x.clone().equals(x) // 필수는 아님!

⭐ clone를 구현하는 방법


Object에 선언되어 있는 clone 메서드는 접근제한자가 protected이고, Object 객체를 리턴한다. 이 메소드를 PhoneNumber에서 구현해보자.

// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // 일어날 수 없는 일이다.
    }
}

다음과 같이 public으로 메서드 접근을 열어두고(다른 곳에서도 객체를 복제할 수 있도록 하기 위해), PhoneNumber 객체를 반환하게 했다. (공변 반환 타이핑)

super.clone()를 통해 부모의 clone 메서드를 이용해 객체를 복제한다.
(아마 결국 Objectclone 메서드를 쓰게 될 것이다.)

근데 super.clone 으로 안하고 PhoneNumber의 생성자로 새로운 객체 생성해서 반환하는 방법도 있을텐데...?



생성자를 사용한다면 어떻게 될까?

문제 발생 : 하위클래스에서 super.clone을 호출하면 상위타입 객체가 반환된다. -> 제대로 동작X

// 생성자를 이용해 객체 반환
@Override
public PhoneNumber clone() {
    try {
        return new PhoneNumber();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // 일어날 수 없는 일이다.
    }
}

PhoneNumber를 상속 받는 SubPhoneNumber가 있다고 가정하면

public class SubPhoneNumber extends PhoneNumber implements Cloneable {

    private String name;

    @Override
    public SubItem clone() {
        return (SubPhoneNumber)super.clone(); //에러 발생 (업캐스팅 불가)
    }

}

SubItem clone에서 super.clone()PhoneNumber 객체를 반환한다.
근데 상위타입이 하위타입으로 바뀔 수 없기 때문에 에러가 발생한다.





⭐ 가변객체를 참조하는 객체 clone 할 때


지금까지 설명한 clone 구현방법는 불변객체를 참조하는 클래스에서만 구현해야 한다.
가변 객체를 clone 하면, 동일한 객체를 참조하게 되기 때문이다.

public class Stack implements Cloneable {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
}

만약 이 Stack클래스를 clone 하면 동일한 elements 가지게 되는 것이다.



@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

배열.clone()을 해주면 될 것 같지만, 이렇게 하면 "같은 배열"을 참조하지는 않지만 배열 안에 있는 인스턴스은 같은 인스턴스를 참조하게 된다.

예를 들어 HashTableclone 메서드를 생각해보자. HashTable은 버킷 배열을 가진다. 그리고 버킷은 LinkedList를 가진다. 그럼 버킷도 복제를 하고, 버킷 안에 있는 LinkedList 복제를 해야 한다. -> 버킷을 깊은복사해야 한다.

public class HashTable implements Cloneable {

    private Entry[] buckets = new Entry[10];

    private static class Entry {
        final Object key;
        Object value;
        Entry next;


		//재귀적으로 복사
        public Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }

		// 반복적으로 복사
        public 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;
        }
    }


    @Override
    public HashTable clone() {
        HashTable result = null;
        try {
            result = (HashTable)super.clone();
            result.buckets = new Entry[this.buckets.length];

            for (int i = 0 ; i < this.buckets.length; i++) {
                if (buckets[i] != null) {
                    result.buckets[i] = this.buckets[i].deepCopy(); // p83, deep copy
                }
            }
            return result;
        } catch (CloneNotSupportedException e) {
            throw  new AssertionError();
        }
    }


}

deepCopy() 재귀적 호출로 인해 스택오버플로우에러가 발생 할 수도 있다.
이 대안으로는 반복자 사용해서 반복적으로 복사하는 방법이 있다.






⭐ 주의할 점 / clone의 대안

☑️ 주의할 점

  • clone() 쓰는 메서드들은, 재정의하게 나두면 안된다.
    객체 생성 과정에 참여하는 clone()이 쓰는 메서드를 재정의할 수 있게 하면 clone이 오동작할 수 있으므로 그런 메서드들은 private이나 final이어야 한다.
  • 상속해서 쓸 클래스는 Cloneable을 구현해서는 안된다. -> clone()protected 로 두고, 예외를 던진다.

☑️ clone의 대안

복사 생성자나 복사 팩터리 이용

    public PhoneNumber(PhoneNumber phoneNumber) {
        this(phoneNumber.areaCode, phoneNumber.prefix, phoneNumber.lineNum);
    }

이렇게 생성자를 써서 객체를 반환하자.
복사 생성자를 쓰면 생성할 때 값을 설정해주므로 final 필드여도 괜찮다.
(clone을 사용하면 새로운 객체를 할당해야 해서 final 불가)

그리고 복사 생성자에서 복사할 객체의 상위 타입을 받는다고 지정해놓으면,
하위타입 객체라면 모두 받을 수 있기 때문에 보다 유연해진다.

복사 팩터리로도 바꿀 수 있다.

public static PhoneNumber newInstance(PhoneNumber phoneNumber){...}

결론 : clone 쓰지 말고 복사생성자/복사팩터리를 사용하자!

0개의 댓글