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

2023. 11. 24. 00:16Book/이펙티브 자바

 

 

- clone 규약

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

  => clone()이 반환한 객체와 원본은 반드시 달라야 한다.(reference가 달라야 한다.)

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

  => clone()이 반환한 객체와 원본 객체의 클래스가 같아야 한다.

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

  => clone()이 반환한 객체가 새로 세팅이 필요한 경우가 있을 수도 있다.

 

// PhoneNumber에 clone 메서드 추가 (79쪽)
public final class PhoneNumber implements Cloneable {
    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "지역코드");
        this.prefix   = rangeCheck(prefix,   999, "프리픽스");
        this.lineNum  = rangeCheck(lineNum, 9999, "가입자 번호");
    }

    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    .
    .
    . 코드 생략

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

    public static void main(String[] args) {
        PhoneNumber pn = new PhoneNumber(707, 867, 5309);
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(pn, "제니");
        PhoneNumber clone = pn.clone();

        System.out.println(clone != pn);
        System.out.println(clone.getClass() == pn.getClass());
        System.out.println(clone.equals(pn));
    }
}

=> 
true
true
true

 

 

Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 의도한 목적을 제대로 이루지 못했다.

가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 접근지시자도 protected라는 데 있다.

 => (다른 패키지나 상속 받은 객체만 사용한다는 보장도 없는데 말이다.)

그럼에도 불구하고 Cloneable방식은 널리 쓰이기에 잘 알아두는 것이 좋다고 한다.

 

 

Cloneable 인터페이스는 아무것도 없는데 대체 무슨 일을 할까?

 => 이 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다.

Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 복사한 객체를 반환한다.

Cloneable을 구현하지 않은 클래스에서 clone을 호출하면 CloneNotSupportedException을 던진다.

 

    /**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * The method {@code clone} for class {@code Object} performs a
     * specific cloning operation. First, if the class of this object does
     * not implement the interface {@code Cloneable}, then a
     * {@code CloneNotSupportedException} is thrown. Note that all arrays
     * are considered to implement the interface {@code Cloneable} and that
     * the return type of the {@code clone} method of an array type {@code T[]}
     * is {@code T[]} where T is any reference or primitive type.
     * Otherwise, this method creates a new instance of the class of this
     * object and initializes all its fields with exactly the contents of
     * the corresponding fields of this object, as if by assignment; the
     * contents of the fields are not themselves cloned. Thus, this method
     * performs a "shallow copy" of this object, not a "deep copy" operation.
     * <p>
     * The class {@code Object} does not itself implement the interface
     * {@code Cloneable}, so calling the {@code clone} method on an object
     * whose class is {@code Object} will result in throwing an
     * exception at run time.
     *
     * @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    @HotSpotIntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

 

Object의 clone() 이 native이기 때문에 소스코드는 jvm.cpp에 있다.

 

 

 

clone 메서드를 보면 super.clone이 아닌 생성자(new PhoneNumber())를 호출해 인스턴스를 반환해도 정상적으로 작동할 것 처럼 보인다. 과연 그럴까?

public class Item implements Cloneable {

    private String name;

    /**
     * 이렇게 구현하면 하위 클래스의 clone()이 깨질 수 있다.
     */

    @Override
    public Item clone() {
        Item item = new Item();
        item.name = this.name;
        return item;
    }
}

=======================================================================

public class SubItem extends Item implements Cloneable{
    private String name;

    @Override
    public SubItem clone() {
        //상위 타입을 구체 타입으로 캐스팅 못 함
        return (SubItem) super.clone();
    }

    public static void main(String[] args) {

        SubItem item = new SubItem();
        SubItem clone = item.clone(); // => ClassCastException 발생
        System.out.println(clone != null);
        System.out.println(clone.getClass() == item.getClass());
        System.out.println(clone.equals(item));
    }
}

 

하위 클래스에서 상위 클래스의 super.clone을 호출한다면 ClassCastException이 발생해 규약을 지킬 수 없게 된다.

 

그러니 불변 객체라면 처음 코드처럼 다음 처럼만 하면 된다.

 1. Cloneable인터페이스를 구현하고(안에 아무것도 없는 인터페이스)

 2. clone 메서드를 재정의한다. 이대 super.clone()을 사용한다.

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

 

 

 

간단했던 위 구현이 가변객체를 참조하는 순간 심각한 에러를 발생시킬 수 있다.