이펙티브자바 아이템13

한주영·2023년 10월 19일
0

이펙티브자바

목록 보기
12/33

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

Cloneable인터페이스
1)복제해도 되는 클래스임을 명시하는 용도
2)clone메서드가 선언된 곳이 Clonable이 아닌Object객체,protected
3.clone의 동작 방식을 결정한다

1.실무에서 Clonable을 구현한 클래스는 clone메서드를 public으로 제공, 사용자는 당연히 복제가 제대로 이뤄진다고 기대한다.

clone메서드의 일반규약

-->객체의 복사본을 생성해 반환.
-->복사는 그 객체를 구현한 클래스에따라 다를수있지만 일반적인 의도는 다음과같다.
-->어떤객체x에 대해 다음 식은 모두 참이다.

1)x.clone()!=x

2)x.clone().getClass()==x.getClass()

3)x.clone.equals(x)

4)x.clone().getClass()==x.getClass()

2.제대로 동작하는 clone메서드를 가진 상위클래스를 상속해 Cloneable을 구현하고싶다면?
1)super.clone을 호출
-->이렇게 호출한 객체는 완벽한 복제본
-->클래스에 정의된 모든 필드는 원본 필드와 똑같은 값을 갖는다.

해당특징을 고려해 PhoneNumber의 clone메서드를 구현

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

@Override
public PhoneNumber clone(){
    try{
       return (PhoneNumber)super.clone();
       }catch(CloneNotSupportedException e){
          throw new AssertionError(); //일어날수없는 일이다.
       }
    
}

해당메서드가 동작하게하려면 Cloneable을 구현해야함

public class Stack{
    private Object[] elements;
    private int size=0;
    private static int DEFAUT_INITAL_CAPACITY=16;
    
    public Stack(){
       this.elements= new Object[DEFAUT_INITAL_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메서드가 단순히 super.clone의 결과를 그대로 반환한다면
size필드는 올바른 값을 갖지만 , elements필드는 원본 인스턴스와 똑같은 배열을 참조하게됨

-->원본이나 복제본중 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게됨.

3.clone메서드는 사실상 생성자와 같은 효과를 나타냄
-->원본 객체에 아무런 해를 끼치지않은 동시에 객체의 불변식을 보장해야한다.

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

@Override
public Stack clone(){
   try{
     Stack result= (Stack)super.clone();
     result.elements= elements. clone();
     return result;
   }catch(CloneNotSupportedException e){
         throw new AssertionError();
   }
}

1)배열의 clone은 런타임타입,컴파일타입 모두가 원본과똑같은 배열을 반환
2)배열을 복제할때는 clone메서드를 사용하는것이좋음!

-->단 final필드라면 새로운값을 할당할수밖에없기때문에 앞서 말한 방식이 동작하지않음

-->Clonalbe아키텍챠는 가변객체를 참조하는 필드는 final필드로 선언하라 라는 일반용법과 충돌함.
복제할수있는 클래스를 만들기위해 일부클래스에서 final을 제거해야할때도있음

4.단순한 버킷배열의 메서드보다 각 버킷을 구성하는 연결리스트를 복사하는 것이 좋다

import java.util.Objects;

public class HashTable implements Cloneable {
    
    private static class Entry{
        
        final Objects key;
        Object value;
        Entry next;

        Entry(Objects key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        
        //이 엔트리가 가리키는 연결리스트를 재귀적으로 복사
        Entry deepCopy(){
            
            return Entry(key,value,next==null?null:next.deepCopy());
        }
    }

    @Override
    protected HashTable clone() {
        
        try {
            HashTable result= (HashTable) super.clone();
            result.buckets= new Entry[buckets.length];
            for(int i=0; i<buckets.length; i++){
                result.buckets[i]= buckets[i].deepCopy();
                return result;
            }
        }catch (CloneNotSupportedException e){
            throw  new AssertionError();
            
        }
        //return super.clone();
    }
    //나머지 코드는 생략
}

1)private 클래스인 Entry는 깊은 복사를 지원하도록 보강
2.clone메서드는 적절한 크기의 새로운 버킷배열을 할당한 다음 원래 버킷배열을 순회 ->비지않은 각 버킷에 대해 깊은 복사를 수향
3.deepCopy()메서드는 자신이 가리키는 연결리스트 전체를 복사하기위해 자신을 재귀적으로 호출

-->재귀호출때문에 원소수만큼 스택 프레임을 소비하여 리스트가길면 스택오버플로우의 위험이있음

해당 문제를 피하려면 재귀호출 보다 반복순회 방향으로 순회해야함

  1. super.clone을 호출하여 얻은 객체의 모든 필드를 초기로 설정후에 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출

-->HashTable의 경우 buckets필드를 새로운 배열로 초기화후에
put메서드를 통해 둘의내용을 동일하게 만듬.

6.public인 clone메서드에서는 throw절을 없애야한다.

-->검사 예외를 던지지않아야 그 메서드를 사용하기 편하기 때문

7.Clonable을 구현한 스레드 안전 클래스를 작성할때에는 clone메서드 역시 적절히 동기화 해야함

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

9.복사 생성자와 복사 팩터리라는 더 나은 객체복사 방식을 제공할수있다.
-->복사생성자란?
-->단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 의미.

복사생성자

public Yum(Yum yum){,...}

복사팩터리

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

복사생성자의 특징
1)언어 모순적이고 위험천만한 객체생성을 사용하지 않는다
2)엉성하게 문서화된 규약에 기대지않는다
3)정상적인 final 필드용법과도 충돌하지않는다
4)불필요한 검사예외를 던지지않고 형변환도 필요치않다.
5)해당클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을수있다.
-->Collection이나 Map타입을 받는생성자를 제공
-->이들을 이용면 클라이언트는 원본의 구현타입에 얽매이지않고 복제본의 타입을 직접 선택가능

ex)
1)HashSet객체 s를 TreeSet타입으로 복제가능
2)clone으로 불가능한 이 기능을 생성자로는 간단히 new TreeSet<>(s)로 처리 가능

profile
백엔드개발자가 되고싶은 코린이:)

0개의 댓글