728x90
Cloneable
- 복제해도 되는 클래스임을 명시하는 용도의 mix-in interface.
clone()
- 한마디로 말하면 복제하는 메소드 이다.
- 그러나, 모든 클래스가 clone() 메서드를 사용할 수 있는 것이 아니다.
- 왜? -> clone() 메서드가 선언된 곳은 Object고 접근 제한자는 protected이지만, clone()은 Clonealbe 인터페이스의 추상 메소드이기 때문.
- 따라서, Cloneable을 구헌하는 것 만으로는 clone()을 호출할 수 없다.
//clone() 자동생성method
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
Cloneable Interface
[ 용도 ]
- Object protected method인 clone()의 동작 방식을 결정한다.
- cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면, 객체의 필드를 하나하나 복사한 객체를 반환한다.
- cloneable을 구현하지 않은 클래스의 인스턴스에서 clone()을 호출하면, CloneNotSupportedException을 던진다.
인스턴스를 구현하는 것은 인터페이스에서 정의한 기능을 제공한다고 선언하는 의미인데, Cloneable의 경우 상위 클래스에서 정의된 protected clone()의 동작 방식을 변경한 것이다. => 상당히 이례적!
[ clone() 일반 규약 ]
일반적인 의도 : 객체의 복사본을 생성해 반환한다. ('복사'의 의미는 클래스마다 다름)
x.clone() != x // 참
x.clone().getClass() == x.getClass() // 참
x.clone().equals(x) // 일반적으로는 참. 그러나 반드시 만족해야 하는 것은 아니다.
clone()이 반환하는 객체는 super.clone()을 호출해 얻었을 때
x.clone().getClass() == s.getClass() // 참
[ 주의할 점 ]
- clone() 메서드가 super.clone()이 아닌 생성자를 호출해 인스턴스를 반환한다면?
- 컴파일러는 동일한 인스턴스로 취급 할 것이다.
- 그러나 클래스의 하위 클래스에서 super.clone()을 호출한다면, 잘못된 클래스의 객체가 만들어져 하위 클래스의 clone()은 제대로 동작하지 않는다.
- 상위 클래스의 clone()인 super.clone()이 아니라, 하위 클래스 타입으로 변환해 반환하자.
- (clone 재정의 클래스가 final이라면 상관 없지만, 이런 경우 애초에 cloneable을 구현 할 필요가 없다.)
- 모든 필드가 기본 타입이거나 불변 필드라면 super.clone() 호출해 자바의 공변 변환 타이핑을 적용한다.
- 즉, 재정의한 메서드의 반환 타입을 상위 클래스의 메서드가 반환하는 타입의 하위 타입으로 만들어준다.
- 모든 필드가 기본 타입이거나 불변 객체를 참조하는 경우, 또는 불변 클래스는 굳이 clone()을 제공하지 않는 것이 좋다.
public class Beverage implements Cloneable{
@Override
public synchronized Beverage clone(){
try{ // 재정의한 메서드의 반환 타입은 상위 클래스 메서드가 반환하는 타입의 하위 타입이다.
// 클라이언트는 형변환을 하지 않아도 된다.
return (Beverage) super.clone();
}catch (CloneNotSupportedException e){
// Object clone메서드가 검사 예외를 던지도록 선언되었기 때문에 try-catch로 감쌈.
throw new AssertionError();
}
}
}
-> 위 코드는 성공적으로 동작하는 코드이지만, 클래스가 가변 구조를 참조하는 순간 오류를 발생시킨다.
- 클래스가 가변 객체를 참조하는 경우
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) {
this.ensureCapacity();
this.elements[size++] = e;
}
/*
-------------- Bad ------------------
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return this.elements[--size];
}
*/
/*-------------- Good ------------------*/
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object value = this.elements[--size];
this.elements[size] = null;
return value;
}
/***
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 배열의 크기가 대략 2배씩 늘어난다.
*/
private void ensureCapacity() {
if (this.elements.length == size) {
this.elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public static void main(String[] args) {
System.gc();
System.runFinalization();
}
}
- clone()이 단순히 super.clone()의 결과를 그대로 반환한다면?
- 가변 필드를 super.clone()으로 복제한다면, 값이 아닌 주소값을 복제하게 된다.
- 즉 반환된 stack 인스턴스의 사이즈 필드는 올바른 값을 갖지만, element 필드는 원본 stack와 똑같은 배열을 참조할 것이다.
- 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되기 때문에 불변식을 해칠 수 있다.
- -> 프로그램의 오동작 또는 NullPointerException이 발생한다.
- clone()은 원본 객체에 아무런 해를 끼치지 않는 동시에, 복제된 객체의 불변식을 보장해야 한다.
- 재귀적으로 가변 객체 참조 필드를 복사하면 해결할 수 있다.
- 가변 객체를 갖는 클래스를 복사하기 위해서는 내부 정보를 복사해야 한다.
- 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출하는 것이다.
@Override
public Stack clone(){
try{
Stack result = (Stack) super.clone();
// 배열의 clone은 런타임, 컴파일 타입이 모두 원본 배열과 같은 배열을 반환한다.
// 배열을 복제할 때는 배열의 clone()을 사용하라 권장한다.
// 배열은 clone()을 제대로 사용하는 유일한 예이다.
result.elements = elements.clone();
return result;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
- 만약 가변 필드가 final인 경우에는 위 방식이 작동하지 않는다.
- 새로운 값을 참조할 수 없기 때문이다.
- 가변 객체를 참조하는 필드는 final로 선언하라는 용법과도 충돌한다.
- -> CLoneable을 구현하려면 일부 필드에서 final을 떼버려야 하는 일이 생길 수 있다.
[ clone()의 재귀적 호출로는 충분하지 않을 때 ]
- 배열 안에 가변 참조 객체가 있는 경우에 해당한다.
- [해결 방법]
- Deep Copy를 한다. ex) HashTable의 clone()
- 단순히 버킷 배열의 clone()을 재귀적으로 호출한다면?
- 복사본은 자신만의 버킷 배열을 갖지만, 원본과 같은 연결리스트를 참조하기 때문에 원본과 복제본 모두 동작할 가능성이 생긴다.
- 따라서 각 버킷을 구성하는 연결 리스트를 복사해야 한다.
- Deep copy를 통한 해결
- 적절한 크기의 새로운 버킷 배열을 생성해 깊은 복사를 진행한다.
- deepCopy()는 자신이 가리키는 연결 리스트 전체를 복사하기 위해 자기를 재귀적으로 호출한다.
- -> 여기서 리스트 원소 수만큼 스택 프레임을 소비하여, 리스트가 긴 경우 스택오버플로우가 생길 수 있다.
- -> deepCopy()는 재귀 호출 대신 반복자를 사용해 순회해라.
[ 복잡한 가변 객체를 복제하는 방법 ]
- super.clone()을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한다.
- 원본 객체 상태를 다시 생성하는 고수준 메서드를 호출한다.
- -> 저수준 처리보다는 느리다.
- -> Cloneable 아키텍처와 어울리지 않기도 하다.
[ clone() 메서드 재정의 시 주의사항 ]
- public인 clone() 메서드에서는 throws절을 없애야 한다. 검사 예외를 던지지 않아야 한다.
- 상속용 클래스는 Cloneable을 구현해서는 안된다.
- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone() 역시 적절히 동기화 해줘야 한다.
- super.clone() 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다.
[ 요 약 ]
1. Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.
2. 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다.
3. clone()은 가장 먼저 super.clone()을 호출하고 필요한 필드를 적절히 수정한다.
- 즉, 객체 내부에 숨은 모든 가변 객체를 복사하고 복제본이 가진 객체 참조 모두가 복사된 객체를 가리키게 한다.
- 주로 clone()을 재귀적으로 호출해 구현하지만 언제나 최선은 아니다.
4. 기본 타입 필드나 불변 객체 참조만 갖는 클래스라면 수정할 필요가 없다.
- 일련번호, 고유 ID는 수정해야 한다.
728x90
'JAVA > Effective Java' 카테고리의 다른 글
item 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.04.23 |
---|---|
item 14. Comparable을 구현할지 고려하라 (0) | 2022.04.23 |
item 12. toString을 항상 재정의하라 (0) | 2022.04.19 |
item 11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.04.19 |
item 10. equals는 일반 규약을 지켜 재정의하라 (0) | 2022.04.19 |
댓글