본문 바로가기

Language/Java Plus

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

 

 

 

1. 배경

// Cloneable는 복제해도 되는 클래스임을 명시하는 인터페이스
// clone메서드는 해당 인터페이스가 아닌 Object에 protected로 선언되어 있음.
// 따라서, Cloneable을 구현하는 것만으로는 clone메서드를 호출할 수 없다.

 

 

 

2. Cloneable 인터페이스의 역할

// - 기본적으로 해당 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다.
// - 해당 인터페이스를 구현하고 object의 clone메서드를 재정의해 호출시 해당 클래스의 필드를 복사한 객체 반환
// - 해당 인터페으스를 구현안하고 clone 메서드 사용시 CloneNotSupportedException 발생

 

 

 

3. Clone메서드의 기본적인 구현 방법

// - Cloneable 인터페이스 구현
// - Override로 clone 메서드 구현
// -> super.clone() : object의 clone메서드로 해당 클래스 복사 후 casting 실행
// - try-catch로 오류 처리
@Override
public CloneClass clone(){
    try{
        return (CloneClass) super.clone();
    }
    catch (CloneNotSupportedException e){
        // Cloneable을 구현했다면 일어나지 않음
        throw new AssertionError();
    }
}

 

 

 

4. 클래스가 가변 객체를 참조하는 경우

// - 예시 : Stack
// - elements 필드는 위의 방법으로 복사시 기존 인스턴스와 똑같은 인스턴스를 참조하게 되어
// 하나 수정시 둘다 수정되어 복사의 개념에 위배된다.

 

 

해결책

// - clone메서드는 사실상 생성자와 같은 효과를 낸다.
// - 즉, clone은 원본 객체의 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
// -> elements 배열의 clone을 재귀적으로 호출한다.
// - 해당 필드가 final로 선언되어있다면 동작하지 않는다.
@Override
public Stack clone(){
    try{
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    }
    catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}

- 해당 코드에서 elements 는 Object[] 임.

 

 

 

5. Clone 메서드를 재귀적으로 호출하는 것만으로 충분하지 않은 경우

// - 예시 : HashTable
// 해시테이블 내부는 버킷들의 배열이고 각 버킷들은 키-값 쌍을 담는 연결리스트의 첫번째 엔트리를 참조한다.
// 복제본은 자신만의 버킷배열을 갖지만 이 배열은 원본과 같은 연결리스트를 참조하여 문제 야기
// -> 즉, Entry 각각이 원본과 같은 인스턴스를 참조하게 된다.
@Override
public HashTable clone(){
    try{
        HashTable result = (HashTable) super.clone();
        // 복제본은 자신만의 버킷 배열을 갖지만 이때, 배열은 원본과 같은 연결리스트를 참조하여 문제 야기
        result.buckets = buckets.clone();
        return result;
    }
    catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}

- 해당 HashTable은 연결리스트로 구성 됨.

 

 

 

 

해결책

// - 각 버킷을 구성하는 연결리스트를 복사한다.
// -> 즉, Entry 클래스에 deepCopy를 선언해 Entry 자체를 새로 생성해 복사하는 메서드를 만들고
// 이 후, 하나하나 넣는 방식으로 복사를 진행한다.

// 하지만 deepcopy시에 자신이 가리키는 연결리스트에 대해서 재귀적으로 deepcopy를 호출하므로
// 스택 오버플로를 일으킬 문제가 발생한다.
// - deepCopy를 재귀 호출 대신 반복자를 사용하여 순회하는 방향으로 수정해야 한다.
// 기존
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();
    }
}

 

 

 

6. Clone의 문제점

// - 생성자를 쓰지 않아 인스턴스를 생성하는 과정에서 오류가 발생할 수 있다.
// - clone의 규약이 정확하지 않다.
// - final 필드를 제대로 사용할 수 없다.
// - 불필요한 예외, 형변환이 많다.

 

 

 

7. 결론

// -> 복사 생성자(변환 생성자)와 복사 팩터리(변환 팩터리)라는 더 나은 객체 복사 방식을 사용하자.
// 복사 생성자 : 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자

 

- 복사 생성자 : 객체 자체를 복사

- 복사 팩터리 : 객체의 필드 값을 복사하여 새로운 객체 생성

 

장점

- 생성자 사용

- 엉성화된 문서화된 규약에 기대지 않음

- final 필드 용법과도 충돌하지 않는다.

- 불필요한 예외도 던지지 않는다. 

- 형변환 필요도 없다.

- 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다. (HashSet 인스턴스를 TreeSet 타입으로 복제 가능)

// 1. 복사 생성자 사용
public class Person {
    private String name;
    private int age;

    // 복사 생성자
    public Person(Person other) {
        this.name = other.name;
        this.age = other.age;
    }

    // 기본 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}


// 2. 복사 팩터리 사용
public class Person {
    private String name;
    private int age;

    // 기본 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 복사 팩토리 메서드
    public static Person createCopy(Person other) {
        return new Person(other.getName(), other.getAge());
    }
}

 

 

 

 

https://github.com/mokjaemin/EffectiveJAVA

 

GitHub - mokjaemin/EffectiveJAVA: Study Files Of Effective JAVA

Study Files Of Effective JAVA. Contribute to mokjaemin/EffectiveJAVA development by creating an account on GitHub.

github.com