peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 13. clone 재정의는 주의해서 진행하라. #10

Open Lainlnya opened 7 months ago

Lainlnya commented 7 months ago

Chapter : 3. 모든 객체의 공통 메서드

Item : 13. clone 재정의는 주의해서 진행하라.

Assignee : Lainlnya


🍑 서론

객체의 복제를 위한 Object.clone() 메서드를 제대로 재정의하기 위해서는 어떻게 해야 하는가

🍑 본론

Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.

clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이며, protected이다. \ 즉, 빈 인터페이스인 Cloneable을 구현해야만 제대로 작동하도록 설계되어 있다. \ 또한, Object.clone()은 protected로 선언되어 하위 클래스에서 재정의해주지 않으면 클라이언트에서 호출할 수 없게 되어있다. \ 해당 객체가 접근이 허용된 clone 메서드를 제공한다는 보장이 없다.

Cloneable 인터페이스가 하는 일

Object의 protected 메서드인 clone의 동작 방식을 결정한다. \ Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환, 그렇지 않을 경우 CloneNotSupportedException을 던진다.

올바른 clone() 구현 방법

  1. 가변 객체를 참조하지 않는 객체
@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone(); // 원본의 완벽한 복제본
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // 일어날 수 없는 일이다.
    }
}

✅ super.clone()을 통해 만들어진 객체가 원본과 똑같은 값을 갖는 복제본이 되고, 모든 필드가 기본 타입이거나 불변 객체이므로 만들어진 복제본에서 수정할 것이 없다. ✅ PhoneNumber 클래스 선언에 Cloneable을 구현한다고 추가해야 동작이 가능하다. ✅ 해당 방식의 경우 클래스가 가변 객체를 참조하는 순간 불가능하다. ✅ 공변반환타입이 가능해지며 클라이언트에서 일일이 형변환을 해줄 필요가 없어진다. ✅ Number 클래스가 cloneable을 구현하고 다른 클래스를 상속받지 않으니 CloneNotSupportedException이 터질 일이 없으므로 불필요한 checked exception을 try-catch로 감싸서 메서드의 throws 절을 없애고 클라이언트에서 더 편하게 사용할 수 있도록 해주면 좋다고 한다.

  1. 가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(재귀 호출)
@Override
public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

✅ Stack의 하나뿐인 생성자를 호출한다면 불변식을 해치지 않겠지만,원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게 된다. \ ✅ clone은 원본 객체에 아무런 해를 끼치지 않으면서 복제된 객체의 불변식을 보장해야 한다. (생성자와 같은 효과) ✅ Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. (복제해야 할 때는 제거해야 할 수도 있다)

  1. 가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(해시테이블용)
public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

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

        // 이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사 (권장x)
        Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }
        // 권장 (엔트리 자신이 가리키는 연결리스트를 반복적으로 복사)
        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() {
        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();
        }
    }
}
  1. 가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(고수준 API 사용)

    1. super.clone 을 호출하여 모든 필드를 초기 상태로 설정
    2. 원본 객체의 상태를 다시 생성하는 고수준 메서드(예- hashtable에서의 put)를 호출

    ✨ 단, 해당 메서드는 final이나 private로 정의하여 하위에서 재정의를 막아야한다. 만약 public이라면 throws절을 없애야 편하게 사용할 수 있다.

주의 사항

✅ 상속용 클래스는 Cloneable을 구현해서는 안된다. \ ✅ clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 한다. \ ✅ 만약 clone()에서 재정의 가능한 메서드를 호출하게 되면 하위 클래스에서 super.clone()을 호출했을 때 상위 클래스의 clone()에서 하위 클래스의 재정의된 메서드를 호출하게 되고 예측할 수 없는 복제본이 만들어질 가능성이 생긴다.

public class Parent implements Cloneable {

    protected int value = 0;

    @Override
    public Parent clone() {
        super.clone();
        // 재정의 가능한 메서드 호출
        overrideableMethod();
        ...
    }

    public void overrideableMethod() {
        value += 1;
    }
}

public class Child extends Parent {
    @Override
    public Parent clone() {
        super.clone();
        ...
    }

    @Override
    public void overrideableMethod() {
        value += 2;
    }
}

=> child를 clone했을 때 parent레벨에서 조정되어야 할 값들이 child에 재정의된 메서드의 동작 방식대로 조정되고 의도치 않은 방향으로 값이 복제되는 문제가 발생할 수 있다. \

✅ Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다. => Object.clone() 은 멀티 쓰레드 환경을 고려하지 않았으므로 쓰레드 안전한 클래스를 만들기 위해서는 clone()메서드가 아무런 작업을 하지 않더라도 재정의하고 동기화 해주어야 한다.

🍑 결론

✅ 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. ✅ 복제 기능은 생성자와 팩터리를 사용하는 것이 가장 좋다. \ ✅ 어쩔 수 없이 확장하려는 클래스가 Cloneable을 구현한 경우 clone()을 재정의해줘야 하지만, 아닐 경우 아래와 같이 복사 생성자와 복사 팩터리를 사용하는 것이 좋다.

public class Car {

    // 복사 생성자
    public Car(Car car) {
        ...
        return newCar;
    }

    // 복사 팩터리
    public static Car newInstance(Car car) {
        ...
        return newCar;
    }
}

** 공변 반환 타입(Convariant Return Type) JDK 1.5부터 추가된 개념으로 부모 클래스의 메소드를 오버라이딩하는 경우, 부모 클래스의 반환 타입은 자식 클래스의 타입으로 변경이 가능하다. \ 공변 반환 타입이 없는 경우, 자식 클래스를 사용하는 클라이언트 코드에서 명시적 형변환 처리를 해주어야 한다.

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();
        Parent pc = new Child();

        System.out.println(parent.createNewOne().getClass());
        System.out.println(child.createNewOne().getClass());
        System.out.println(pc.createNewOne().getClass());
    }
}

class Parent {
    protected Parent createNewOne() {
        return new Parent();
    }
}

class Child extends Parent {
    // 부모 클래스로부터 재정의하였으나 반환형을 자식 클래스로 변경할 수 있다.
    @Override public Child createNewOne() {
        return new Child();
    }
}

interface Testable { Testable tester(); } class TestableImpl implements Testable { @Override public TestableImpl tester() { return new TestableImpl(); } }



--- ---
## Referenced by
- [공변반환타입](https://ingnoh.tistory.com/153)