ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 자바 Item 13 - clone 재정의는 주의해서 진행하라
    언어/Effective Java 2024. 5. 29. 15:34

    clonable의 역할

    • 복제해도 되는 클래스임을 나타내는 믹스인 인터페이스이다.
    • Object 클래스에 protected clone()라는 메서드가 있다.
    • Cloneable 인터페이스는 clone() 메서드의 동작 방식을 결정한다.
    • Cloneable을 구현하지 않는 인스턴스에서 clone()를 호출하면 CloneNotSupportedException을 던진다.

    clone() 사용해보기

    static class Entry implements Cloneable {
            String key;
            String value;
    
            public Entry(String key, String value) {
                this.key = key;
                this.value = value;
            }
    
            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }
        }
    • 메서드의 내용은 상위 클래스의 clone()을 가져다 써도 된다.
    • clone으로 객체를 복제하는 경우 원본 객체와 복제된 객체가 같은 객체를 공유한다 (얕은 복사)
    • 새로 인스턴스를 생성하지 않기 때문에 깊은 복사보다 상대적으로 빠르다. 

     

    clone 메서드의 일반 규약 

     

    • 일반적인 인터페이스의 동작방식과 다르게 상위 Object 클래스에 protected 접근자로 된 clone() 메서드가 존재하고 이를 오버라이딩 해야한다. 
    • clone 메서드가 super.clone가 아닌 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일시 문제 x 하지만 이 클래스의 하위 클래스에서 super.clone메서드를 호출허면, 잘못된 객체가 만들어져 하위 clone가 작동하지 않는다.
      • clone메서드를 재정의한 클래스가 final이라면 하위 클래스가 없으니 괜찮다. 
    • clone 메서드 일반 규약 사항
      • x.clone() != x //참 
      • x.clone().getClass == x.getClass() // 참, 하지만 반드시 만족할 필요는 없다. 
      • x.clone().getClass().equals(x) // 참이지만 필수는 아님
      • x.cloen().getClass() == x.getClass() 
        • 만약 super.clone()을 호출해 얻은 객체를 clone()이 반환한다면, 이 식은 참이다. 
        • 관례상 반환된 객체와 원본객체는 독립적이어야한다. 이를 만족하려면 super.clone에서 얻은 필드 중 하나 이상을 반환전에 수정해야할 수도 있다. 

     


    가변 상태를 참조하지 않는 clone 재정의                                                                                                                                                                                                                                                     

    public class Person implements Cloneable {
        String name;
    
        public Person(final String name) {
            this.name = name;
        }
    
        @Override
        public Person clone() throws CloneNotSupportedException {
            try {
                return (Person) super.clone();
            } catch (CloneNotSupportedException cloneNotSupportedException) {
                throw new AssertionError();
            }
        }
    }
    • Person의 clone 메서드는 Person을 반환하게 했는데, 공변 반환 타입으로 인해 재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다.  (공변 반환 타입 -> 부모 = 자식만 되는 것)

    가변 객체를 참조하는 clone 메서드 재정의 

    public class Stack {
        private Object[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
        public Stack(final Object[] elements) {
            this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
        }
    
        public void push(Object obj) {
            ensureCapacity();
            elements[size++] = obj;
        }
    
        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);
            }
        }
    
        @Override
        protected Stack clone() {
            try {
                return (Stack) super.clone();
            } catch (CloneNotSupportedException cloneNotSupportedException) {
                throw new AssertionError();
            }
        }
    }
    • 단순하게 clone 메서드가 super.clone 결과를 반환한다면 반환된 Stack인스턴스의 size는 올바른 값을 갖겠지만, elements는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
    • Stack같은 것의 clone를 제대로 동작하려면, 내부 정보를 복사해야 하는데 가장 쉬운 방법은 elements배열의 clone을 재귀적으로 호출하는 것이다.
    @Override
    protected Stack clone() {
            try {
                Stack result = (Stack) super.clone();
                result.elements = elements.clone();
                return result;
            } catch (CloneNotSupportedException cloneNotSupportedException) {
                throw new AssertionError();
            }
    }
    • 이 방식은 elemnts 필드가 final이었다면 사용할 수 없다. 
    • Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라는 일반 용법과 충돌한다.
    • 따라서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final을 제거할 수도 있다.

     


    복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드 재정의

     

    때로는 재귀적으로 호출하는 것만으로 충분하지 않을 때도 있다. 

     

    가변 상태를 공유하는 잘못된 clone 메서드 

     @Override
        protected HashTable clone() {
            try {
                HashTable result = (HashTable) super.clone();
                result.buckets = new Entry[buckets.length];
    
                return result;
            }catch (CloneNotSupportedException cloneNotSupportedException){
                throw new AssertionError();
            }
        }
    • 가변객체를 참조하는 clone 메서드의 예로 든 Stack처럼 단순히 버킷 배열의 clone을 재귀적으로 복제본을 자신만의 버킷 배열을 갖는 경우도 있지만,
    • 원본과 동일한 연결리스트를 참조하여 원본과 복제본이 예기치 않게 작동할 가능성이 있다. 
    • 이를 해결하려면, 각 버킷을 구성하는 연결리스트를 복사해야한다.
     @Override
        protected 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 cloneNotSupportedException){
                throw new AssertionError();
            }
        }
    • HashTable.Enrty는 깊은복사를 지원하도록 deepCopy에서 값만 복사해 만들어주고있다. 
    • 하지만 연결리스트를 복제하는 방법으로 재귀 호출을 선택하는 것은 좋은 방법이 아니다. (스택 오버 플로우 가능성)
    • 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;
    }
    • 복사 대신 새로 new로 같은 값을 가지게 해서 넣기 

    clone 메서드 주의사항

     

    상속용 클래스에서는 Cloneable을 구혀해서는 안된다. -> clone 메서드를 재정의해 CloneNotSupportedException()을 던지자

    Object의 clone 메서드는 동기화를 신경쓰지 않았다. -> 동시성 문제가 발생할 수 있다.

    재정의한 clone메서드는 throws절을 없애야 한다. -> 사용의 편의성 때문 

     


    객체 복사를 위해 이 모든 작업이 꼭 필요할까?

     

    • 이미 Cloneable을 구현한 클래스를 확장했다면, 어쩔 수없이 clone을 잘 작동하도록 구현해야한다.
    • 하지만 그렇지 않은 상항이라면, 복사 생성자와 복사 팩터리로 더 나은 객체 복사 방식을 제공받을 수 있다.

    복사 생성자와 복사 팩터리 

     

    복사 생성자(변환 생성자)

    자신과 같은 클래스의 인스턴스를 받는 생성자

    public Yum(Yum yum){
    	...
    }

     

    복사 팩터리(변환 팩토리)

    복사 생성자를 정적 팩터리 형석으로 정의

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

     

     

    정리

    • 인터페이스를 만들때 Cloneable를 확장하는 것은 다시 생각해보자. Cloneable은 믹스인 용도로 만들어진 것이다
    • final 클래스라면 성능 최적화 관점에서 검토 후 문제가 없을 때만 Cloneable을 구현하자
    • 객체 복제 기능은 Cloneable/clone방식보다 복사 팩터리와 복사 생성자를 이용하는 것이 좋다.
      • 단 배열의 경우 clone방식이 가장 적합하므로 예외로 친다. 

     


    https://velog.io/@alkwen0996/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C13-clone-%EC%9E%AC%EC%A0%95%EC%9D%98%EB%8A%94-%ED%95%AD%EC%83%81-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%98%EB%9D%BC

     

    [이펙티브 자바] 아이템13 | clone 재정의는 항상 주의해서 진행하라

    clone 메서드 정의 > clone 메서드는 객체의 모든 필드를 복사하여 새로운 객체에 넣어 반환하는 동작을 수행한다. 즉, 필드의 값이 같은 객체를 새로 만드는 것이다. [객체를 복제하여 필드값 비교

    velog.io

     

Designed by Tistory.