-
[이펙티브자바]clone 재정의는 주의해서 진행하라_아이템13이펙티브 자바 2024. 1. 15. 06:17
Cloneable 인터페이스는 Object의 protected메서드인 clone의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 복사한 객체을 반환하여, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다. Cloneable의 경우 상위 클래스에 정의된 protected메서드의 동작방식을 변경할 것이다.
실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다.
[Object 명세]
이 객체의 복사본을 생성해 반환한다. 객체 x에 대해 다음 식은 참이다.x.clone() != x //참이다. x.clone().getClass() == x.getClass() //참이다. x.clone().equals(x) //일반적으로 참이지만, 필수는 아니다. x.clone().getClass() == x.getClass() //메서드가 반환하는 객체는 super.clone을 호출해 얻어야 참이다.
반환된 객체와 원본 객체는 독립적이어야 한다.
super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
[clone메서드를 가진 상위 클래스를 상속해 Cloneable 구현]
super.clone 호출 => 얻은 객체는 원본의 복제본이다. 불필요한 복제는 지양한다는 관점에서 보면 불변 클래스는 굳이 clone 메서드를 제공하지 않는 게 좋다.
[가변 상태를 참조하지 않는 클래스용 clone 메서드]@Override public PhoneNumber clone() { try { return (PhoneNumber) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); //일어날 수 없는 일이다. } }
구현이 클래스가 가변 객체를 참조하는 순간 문제가 생긴다.
[아이템7의 Stack클래스 예제]public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_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); } }
원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다. => 이상하게 동작하거나 NullPointerException을 던질 것이다. clone메서드는 사실상 생성자와 같은 효과를 낸다.
즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다. => 해결방법, elements 배열의 clone을 재귀적으로 호출해 주는 것.
[가변 상태를 참조하는 클래스용 clone 메서드]@Override public Stack clone() { try { Stack result = (Stack)super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
elements.clone의 결과는 원본 배열과 똑같이 반환되어 Object[]로 형변환하지 않아도 된다. elements 필드가 final이었다면 위의 방식은 작동하지 않는다. => final필드에는 새로운 값을 할당할 수 없다. 직렬화와 같은 Cloneable 아키텍쳐는 ’가변 객체를 참조하는 필드는 final로 선언하라‘는 일반 용법과 충돌한다.
[해시테이블용 clone 메서드]
키-값 쌍을 담는 연결 리스트의 첫 번째 엔트리를 참조.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; } } ... }
Stack처럼 버킷 배열의 clone을 재귀적 호출
[잘못된 clone메서드 - 가변 상태를 공유]@Override public HashTable clone() { try { HashTable result = (HashTable) super.clone(); result.buckets = buckets.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다. => 각 버킷을 구성하는 연결 리스트를 복사.
[복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드]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; } //이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사 Entry deepCopy() { return new Entry(key, value, next == null ? null : next.deepCopy()); } } @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(); } } }
private 클래스인 HashTable.Entry는 깊은 복사를 지원하도록 보강. 하지만 연결 리스트를 복제하는 방법은 재귀 호출 때문에 리스틍 원소 수 만큼 스택 프레임을 소비하여, 리스트가 길면 스택 오버플로를 일으킬 위험이 있기 때문에 좋은 방법이 아니다. => 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; }
super.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정 => 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출한다.
HashTable은 buckets필드를 새로운 버킷 배열로 초기화한 다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해 복제본 테이블의 put(key, value)메서드 호출해 둘의 내용이 똑같게 해주면 된다. => 간단하고 우아한 코드를 얻지만, 저수준보다 처리 속도는 느리다. Cloneable 아키텍처와 어울리지 않는 방식이다.
put(key, value)메서드는 final이거나 private이어야 한다. 그렇지 않다면 원본과 복제본의 상태가 달라질 수 있다. public인 clone메서드는 throw 절을 없애야 한다. => 검사 예외를 던지지 않아야 그 메서드를 사용하기 편하다. 상속 클래스는 Cloneable을 구현하면 안된다.
작동하는 clone메서드를 구현해 protected로 두고 CloneNotSupportedException도 던질 수 있다고 선언하는 것이다. => 상속처럼 Cloneable 구현 여부를 하위 클래스에서 선택하도록 해준다.
clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 할 수도 있다. => clone 퇴화시키자.
[하위클래스에서 Cloneable을 지원하지 못하게 하는 clone메서드]@Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
Cloneable을 구현란 스레드 안전 클래스를 작성할 때 clone메서드도 적절히 동기화해줘야 한다. Object의 clone 메서드는 동기화를 하지않았다. 그렇기 때문에 super.clone호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다.
public => 반환 타입은 클래스 자신으로 변경.
일련변호나 고유 ID는 기본타입이나 불변일지라도 수정해줘야 한다. 모든 컬렉션 구현체는 Collection이나 Map타입을 받는 생성자를 제공한다. 변환 생성자와 변환 팩터리를 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다. HashSet객체 s를 TreeSet타입으로 복제할 수 있다. clone으로는 불가능한 이 기능을 변환 생성자로는 간든히 new TreeSet<>(s)로 처리할 수 있다.
마무리,
1. 새로운 인터페이스를 만들 때는 절대 Cloneable을 확잔해서는 안되고, 새로운 클래스도 구현하면 안된다.
2. final 클래스는 Cloneable의 성능 최적화 관점에서 검토한 후 문제가 없을 때만 드물게 허용해야 한다.
3. 기본 원칙은 복제 기능은 생성자와 팩터리를 이용하는게 최고다. 단, 배열만른 clone 메서드 방식이 가장 맞는 예외라 할 수 있다.'이펙티브 자바' 카테고리의 다른 글
[이펙티브자바]클래스와 멤버의 접근 권한을 최소화하라_아이템15 (0) 2024.05.31 [이펙티브자바]Comparable을 구현할지 고려하라_아이템14 (0) 2024.01.18 [이펙티브자바]toString을 항상 재정의하라_아이템12 (0) 2024.01.10 [이펙티브자바]equals를 재정의하려거든 hashCode도 재정의하라_아이템11 (0) 2024.01.09 [이펙티브자바]equals는 일반 규약을 지켜 재정의하라_아이템10 (1) 2023.12.06