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

신명철·2022년 2월 14일
0

Effective Java

목록 보기
11/80

clone 메소드 규약

  1. x.clone() != x
  2. x.clone().getclass() == x.getclass()
    -> 참이지만, 필수는 아니다.
  3. x.clone().equals(x)

cloneable 를 구현한 클래스에서 clone 을 호출하면 그 객체의 필드를 하나하나 복사한 객체를 반환하고, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException 을 던진다.

clone 메서드에서 생성자를 통해 인스턴스를 반환하면 컴파일러는 오류를 내지 않을 것이다. 하지만 그 클래스의 하위클래스에서 super.clone()을 호출한다면 잘못된 클래스 객체가 만들어져 하위 클래스의 clone 메서드가 제대로 작동하지 않을 것이다.

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

@Override
public PhoneNumber clone() {
	try {
		return (PhoneNumber) super.clone();
	} catch (ConleNotSupportedException e) {
		throw new AssertionError();	// 일어날 수 없는 일이다.
	}
}
  • 클래스의 모든 필드가 기본 타입 이거나, 불변 객체인 경우의 clone 메서드 이다.

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

가변 상태를 참조하는 클래스는 깊은 복사를 진행해줘야 한다. 단순히 super.clone() 을 호출한다면 원본의 참조 필드와 동일한 필드를 참조할 것이고, 원본이나 복제본 중 하나를 수정하면 다른 하나에도 영향을 끼치게 된다.

아래와 같은 방법으로 깊은 복사를 진행한다.

@Override
public Stack clone() {
	try {
		Stack result = (Stack) super.clone();
		result.elements = elements.clone();
		return result;
	} catch (CloneNotSupportedException e) {
		throw new AssertionError();
	}
}
  • 다만, elements 필드를 final 로 선언한다면 위 코드는 작동하지 않는다. (final 필드에는 새로운 값을 할당할 수 없기 때문)
  • Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final 로 선언하라' 라는 일반 용법과 충돌하게 된다.
  • 복제할 수 있는 필드를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.

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

@Override
public HashTable clone() {
	try {
		HashTable result = (HashTable) super.clone();
		result.buckets = buckets.clone();
		return result;
	} catch (CloneNotSupportedException e) {
		throw new AssertionError();
	}
}
  • 위는 HashTable의 clone 메서드로 복제본은 자신만의 버킷 배열을 갖는다. 하지만, 원본과 같은 연결리스트를 참조한다는 문제가 있다.
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();
	}
}
  • 위 방식은 보기에는 문제가 없어 보이지만, 리스트의 원소 수만큼 스택 프레임을 소비하기 때문에 스택 오버플로를 일으킬 수도 있다.
  • 이는 반복자를 활용해 해결할 수 있다.
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;
}

객체 복사의 다른 방법

복사 생성자와 복사 팩터리를 이용한 방법도 있다.

public Yum(Yum yum) { ... }; // 복사 생성자
public static Yun newInstance(Yun yun) { ... }; // 복사 팩터리

위 방법들은 생성자를 쓰지 않는 방식을 사용하지 않고, 정상적인 final 필드 용법과 충돌하지도 않고, 불필요한 검사 예외를 던지지도 않고 형변환도 필요없다. 그리고 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수도 있다.

이를 통하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다.

profile
내 머릿속 지우개

0개의 댓글