아이템13. clone 재정의는 주의해서 진행하라

2021. 7. 20. 19:15책/이펙티브자바

다룰 내용

  • clone 메서드를 잘 동작하게끔 해주는 구현 방법
  • 언제 그렇게 해야 하는지, 가능한 다른 선택지에 관해

Cloneable Interface 가 하는 일

  • 메서드 하나 없는 인터페이스
  • Object.clone()의 동작 방식을 결정한다.
  • Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출시 CloneNotSupportedException을 던진다.

clone 규약

// 이 객체의 복사본을 생성해 반환한다. 
// 복사의 정확한 뜻은 그 개체를 구현한 클래스에 따라 다를 수 있다.

// true
x.clone() != x 

// true
x.clone().getClass() == x.getClass()

// true, 필수는 아님
x.clone().eqauls(x)

// true
x.clone().getClass() == x.getClass()

// 관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 
// 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

가변 상태를 참조하지 않는 클래스의 clone()

// PhoneNumber.class

@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

가변 객체를 참조하는 클래스의 clone()

// 아이템7 에서 소개한 Stack.class

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];
  }

  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public Object pop() {
    if (size == 0)
      throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; 
    return result;
  }

  private void ensureCapacity() {
    if (elements.length == size)
      elements = Arrays.copyOf(elements, 2 * size + 1);
  }

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

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

 

clone 메서드는 사실상 생성자와 같은 효과를 낸다.

즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.

재귀호출만으로는 충분하지 않은 클래스의 clone()

public class HashTable implements Cloneable {
  private Entry[] buckets = ...;

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

    Entry(Obejct key, Object value, Entry next) {
      this.key = key;
      this.value = value;
      this.next = next;
    }
  }

  // 잘못된 clone 메서드: 가변 상태를 공유함.
  @Override 
    public HashTable clone() {
    try {
      HashTable result = (HashTable) super.clone();
      result.buckets = buckets.clone();
      return result;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError();
    }
  }
}
public class HashTable implements Cloneable {
  private Entry[] buckets = ...;

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

    Entry(Obejct key, Object value, Entry next) {
      this.key = key;
      this.value = value;
      this.next = next;
    }

    // 1. bucket의 크기가 크지 않다면 괜찮지만 너무 크다면 콜 스택 오버플로가 발생한다. 
    Entry deepCopy() {
      return new Entry(key, value, next == null ? null : next.deepCopy());
    }
  }

  // 1. 
  @Override 
  public HashTable clone() {
    try {
      HashTable result = (HashTable) super.clone();
      result.buckets = new Entry[buckets.length];
      for (int i = 0; i < buckets.length; i++) {
        if (buckets[i] != null) {
          result.buckets[i] = buckets[i].deepCopy();
        }
      }
      return result;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError();
    }
  }
}
// 2. 재귀호출 -> 반복자를 써서 순회하는 방향으로 수정 
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;
}

참조

  • Cloneable 을 구현하는 모든 클래스는 clone을 재정의해야 한다.
  • 접근 제한자는 public 으로, 반환 타입은 클래스 자신으로 변경한다.
  • 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없다.
  • 단, 일련번호나 고유 ID는 비록 기본 타입이나 불변일지라도 수정해줘야 한다.

근데 이미 구현한 클래스를 확장하는게 아니라면 복사 생성자와 복사 팩토리라는 더 나은 객체 복사 방식을 제공할 수 있다.

결론

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안된다.

기본 원칙은 복제 기능은 생성자와 팩토리를 이용하는게 최고 라는 것이다.

단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.