기록이 힘이다.

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

JAVA

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

dev22 2023. 3. 20. 16:53
728x90

 Cloneable 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다. Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다. 

 

clone규약

   x.clone() != x 반드시 true

   x.clone().getClass() == x.getClass() 반드시 true

   x.clone().equals(x) true가 아닐 수도 있다.

 

불변 객체라면 다음으로 충분하다.

   Cloneable 인터페이스를 구현하고

   clone 메서드를 재정의한다. 

 

 clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. 그래서 stack의 clone 메서드는 제대로 동작하려면 스택 내부 정보를 복사해야 하는데, 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해 주는 것이다. 

 

가변 상태를 참조하는 클래스용 clone메서드

@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 기능을 제대로 사용하는 유일한 예라 할 수 있다. 

 

Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. 그래서 복제 클래스 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다. 

 

<가변 객체의 clone 구현하는 방법>

-접근 제한자는 public, 반환 타입은 자신의 클래스로 변경한다.

-super.clone을 호출한 뒤 필요한 필드를 적절히 수정한다.

  -배열을 복제할 때는 배열의 clone 메서드를 사용하라.

  -경우에 따라 final을 사용할 수 없을지도 모른다.

  -필요한 경우 deep copy를 해야한다.

 -super.clone으로 객체를 만든 뒤, 고수준 메서드를 호출하는 방법도 있다.

  -오버라이딩 할 수 있는 메서드는 참조하지 않도록 조심해야 한다.

  -상속용 클래스는 Cloneable을 구현하지 않는 것이 좋다.

  -Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 동기화를 해야 한다.

 

해시테이블용 clone 메서드

package me.whiteship.chapter02.item13;

public class HashTable implements Cloneable {

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

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

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

        public void add(Object key, Object value) {
            this.next = new Entry(key, value, null);
        }

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

    /**
     * TODO hasTable -> entryH[],
     * TODO copy -> entryC[]
     * TODO entryH[0] == entryC[0]
     *
     * @return
     */
//    @Override
//    public HashTable clone() {
//        HashTable result = null;
//        try {
//            result = (HashTable)super.clone();
//            result.buckets = this.buckets.clone(); // p82, shallow copy 라서 위험하다.
//            return result;
//        } catch (CloneNotSupportedException e) {
//            throw  new AssertionError();
//        }
//    }

    /**
     * TODO hasTable -> entryH[],
     * TODO copy -> entryC[]
     * TODO entryH[0] != entryC[0]
     *
     * @return
     */
    @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();
        }
    }

    public static void main(String[] args) {
        HashTable hashTable = new HashTable();
        Entry entry = new Entry(new Object(), new Object(), null);
        hashTable.buckets[0] = entry;
        HashTable clone = hashTable.clone();
        System.out.println(hashTable.buckets[0] == entry);
        System.out.println(hashTable.buckets[0] == clone.buckets[0]);
    }
}

버킷이 너무 길지 않다면 잘 작동, 스택 프레임을 소비하여 리스트가 길면 스택 오버플로를 일으킬 위험. 

 

이 문제를 피하려면 deepCopy를 재귀 호출 대신 반복자를 써서 순회하는 방향으로 수정

 

엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다. 

하위 클래스에서 Cloneable을 지원하지 못하게 하는 clone 메서드

@Override
protected final Object clone() throws CloneNotSupportedException{
	throw new CloneNotSupportedException();
}

 

Cloneable을 구현하는 모든 클래스는 clone을 재정의해야한다. 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다. 

 

복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.

public Yum(Yum yum){...};
public static Yum newInstance(Yum yum){...};

Cloneable/clone 방식보다 나은 면이 많다. 

 

언어 모순적이고 위험천만한 객체 생성 매커니즘(생성자를 쓰지 않는 방식)을 사용하지 않으며, 엉성하게 문서화된 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않으며, 불필요한 검사 예외를 던지지 않고, 형변환도 필요치 않다. 

 

해당 클래스가 구현한 '인터페이스'타입의 인스턴스를 인수로 받을 수 있다. 

클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다. 

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다. 단 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.