[이펙티브 자바] 아이템13

hyng·2022년 12월 15일
0

이펙티브 자바

목록 보기
13/13

clone 재정의는 주의해서 진행하라

참고

clone 호출 시 다음의 과정이 필요하다

  1. Object.clone()은 기본적으로 protected이기 때문에 하위 클래스에서 public으로 오버라이드 해주어야 한다. 이때 반환 타입은 Object가 아닌 해당 클래스 타입으로 하는 것이 좋다. (클라이언트가 형 변환해 주지 않아도 됨, 재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입 일수 있다.)
  2. Cloneable을 구현해야 한다. Object.clone()에서는 실제 객체의 크기를 알아내고 복사하는데 이때 실제 객체가 Cloneable을 구현했는지 확인하고 구현하지 않았다면 CloneNotSupportedException을 던진다.
  3. 오버라이드 한 메서드의 내부 구현은 super.clone() 호출부터 시작되어야 한다.
  • 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 더 이상 필요한 작업은 없다.
  • 생성자 연쇄와 비슷하지만 강제성이 없다는 점에서 차이가 있다.

clone 호출 시 문제가 발생할 수 있는 부분

  • 상위 클래스의 clone 메 서드에서 super.clone()을 호출하지 않는 경우
    • 이 경우 하위 클래스에서 clone 메서드를 호출하더라도 제대로 동작하지 않게 된다
  • 최종적으로 실행되는 Object.clone 메서드는 얕은 복사를 제공하기 때문에 참조 타입을 가지는 경우에는 반환하기 전에 필드를 수정해야 한다.
    • Stack 클래스가 있을 때 이를 복사한다고 가정해 보자.
    • 단순하게 super.clone()만 호출하면 복사 객체와 원본 객체의 elements는 동일한 메모리를 공유하기 때문에 하나를 수정하면 다른 하나도 수정되게 된다.
class Stack {
  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);
    }
  }
}
  • 그래서 다음처럼 clone 메서드를 구현해야 한다.
@Override
  public Stack clone() {
    try {
      Stack result = (Stack) super.clone();
      result.elements = elements.clone();
      return result;
    } catch (CloneNotSupportedException e) {
      throw new RuntimeException(e);
    }
  }
  • elements.clone()의 결과를 Object[]로 형 변환할 필요는 없다. 배열의 clone은 런타임 타입과 컴파일 타입 모두가 원본 배열과 똑같은 배열을 반환한다. (배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다.)
  • 하지만 이것만으로는 부족한 경우도 있다. 해시테이블 내부는 버킷들의 배열이고 각 버킷은 key-value를 담는 연결 리스트로 이루어져 있는데 각 버킷은 연결 리스트의 첫 번째 엔트리를 참조한다.
class HashTable implements Cloneable {
  private Entry[] buckets = ...;
  
  @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 RuntimeException(e);
    }
  }
  
  private static class Entry {
    final Object key;
    Object value;
    Entry next;

    public Entry(Object key, Object value, Entry next) {
      this.key = key;
      this.value = value;
      this.next = next;
    }
    
     // 이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사
    Entry deepCopy() {
      return new Entry(key, value, next == null ? null : next.deepCopy());
    }
  }
  ... // 나머지 코드 생략
}
  • 이때 버킷 배열은 복사본을 가지게 되지만 각 버킷을 구성하는 연결 리스트는 원본 버킷 배열과 동일하기 때문에 각 버킷을 구성하는 연결 리스트를 복사해야 한다.

복제 기능은 생성자와 팩토리를 이용하자

  • clone() 을 통한 복제는 여러 문제점을 가지고 온다.
  1. 인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위다. 그런데 Cloneable의 경우에는 상위 클래스에 정의된 protected 메 서드의 동작 방식을 변경하는 것이다.
  2. clone 메서드의 규약이 허술하다. 강제성이 없기 때문에 clone 메서드를 잘못 구현해 문제가 발생할 수 있다. (예를 들어 상위 클래스가 super.clone() 을 호출하지 않을 수도 있음)
  3. Stack의 경우 elements 필드에 새로운 값을 할당했었다. 이는 '가변 객체를 참조하는 필드는 final로 선언하라'라는 용법과 충돌한다.
  4. 필요하다면 객체 내부 참조 객체들에 대해 깊은 복사를 하는 처리가 필요하다.

Cloneable을 이미 구현한 클래스를 확장하는 경우가 아니라면 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 사용하자.

// 복사 생성자
public Yum(Yum yum) {}

// 복사 팩터리
public static Yum newInstance(Yum yum) {}
profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글