-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Chapter : 3. 모든 객체의 공통 메서드
Item : 13. clone 재정의는 주의해서 진행하라.
Assignee : Lainlnya
🍑 서론
객체의 복제를 위한 Object.clone() 메서드를 제대로 재정의하기 위해서는 어떻게 해야 하는가
🍑 본론
Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이며, protected이다.
즉, 빈 인터페이스인 Cloneable을 구현해야만 제대로 작동하도록 설계되어 있다.
또한, Object.clone()은 protected로 선언되어 하위 클래스에서 재정의해주지 않으면 클라이언트에서 호출할 수 없게 되어있다.
해당 객체가 접근이 허용된 clone 메서드를 제공한다는 보장이 없다.
Cloneable 인터페이스가 하는 일
Object의 protected 메서드인 clone의 동작 방식을 결정한다.
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환, 그렇지 않을 경우 CloneNotSupportedException을 던진다.
올바른 clone() 구현 방법
- 가변 객체를 참조하지 않는 객체
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone(); // 원본의 완벽한 복제본
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}✅ super.clone()을 통해 만들어진 객체가 원본과 똑같은 값을 갖는 복제본이 되고, 모든 필드가 기본 타입이거나 불변 객체이므로 만들어진 복제본에서 수정할 것이 없다.
✅ PhoneNumber 클래스 선언에 Cloneable을 구현한다고 추가해야 동작이 가능하다.
✅ 해당 방식의 경우 클래스가 가변 객체를 참조하는 순간 불가능하다.
✅ 공변반환타입이 가능해지며 클라이언트에서 일일이 형변환을 해줄 필요가 없어진다.
✅ Number 클래스가 cloneable을 구현하고 다른 클래스를 상속받지 않으니 CloneNotSupportedException이 터질 일이 없으므로 불필요한 checked exception을 try-catch로 감싸서 메서드의 throws 절을 없애고 클라이언트에서 더 편하게 사용할 수 있도록 해주면 좋다고 한다.
- 가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(재귀 호출)
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}✅ Stack의 하나뿐인 생성자를 호출한다면 불변식을 해치지 않겠지만,원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게 된다.
✅ clone은 원본 객체에 아무런 해를 끼치지 않으면서 복제된 객체의 불변식을 보장해야 한다. (생성자와 같은 효과)
✅ Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. (복제해야 할 때는 제거해야 할 수도 있다)
- 가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(해시테이블용)
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;
}
// 이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사 (권장x)
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();
}
}
}-
가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(고수준 API 사용)
super.clone을 호출하여 모든 필드를 초기 상태로 설정- 원본 객체의 상태를 다시 생성하는 고수준 메서드(예- hashtable에서의 put)를 호출
✨ 단, 해당 메서드는 final이나 private로 정의하여 하위에서 재정의를 막아야한다. 만약 public이라면 throws절을 없애야 편하게 사용할 수 있다.
주의 사항
✅ 상속용 클래스는 Cloneable을 구현해서는 안된다.
✅ clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 한다.
✅ 만약 clone()에서 재정의 가능한 메서드를 호출하게 되면 하위 클래스에서 super.clone()을 호출했을 때 상위 클래스의 clone()에서 하위 클래스의 재정의된 메서드를 호출하게 되고 예측할 수 없는 복제본이 만들어질 가능성이 생긴다.
public class Parent implements Cloneable {
protected int value = 0;
@Override
public Parent clone() {
super.clone();
// 재정의 가능한 메서드 호출
overrideableMethod();
...
}
public void overrideableMethod() {
value += 1;
}
}
public class Child extends Parent {
@Override
public Parent clone() {
super.clone();
...
}
@Override
public void overrideableMethod() {
value += 2;
}
}=> child를 clone했을 때 parent레벨에서 조정되어야 할 값들이 child에 재정의된 메서드의 동작 방식대로 조정되고 의도치 않은 방향으로 값이 복제되는 문제가 발생할 수 있다. \
✅ Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다.
=> Object.clone() 은 멀티 쓰레드 환경을 고려하지 않았으므로 쓰레드 안전한 클래스를 만들기 위해서는 clone()메서드가 아무런 작업을 하지 않더라도 재정의하고 동기화 해주어야 한다.
🍑 결론
✅ 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다.
✅ 복제 기능은 생성자와 팩터리를 사용하는 것이 가장 좋다.
✅ 어쩔 수 없이 확장하려는 클래스가 Cloneable을 구현한 경우 clone()을 재정의해줘야 하지만, 아닐 경우 아래와 같이 복사 생성자와 복사 팩터리를 사용하는 것이 좋다.
public class Car {
// 복사 생성자
public Car(Car car) {
...
return newCar;
}
// 복사 팩터리
public static Car newInstance(Car car) {
...
return newCar;
}
}** 공변 반환 타입(Convariant Return Type)
JDK 1.5부터 추가된 개념으로 부모 클래스의 메소드를 오버라이딩하는 경우, 부모 클래스의 반환 타입은 자식 클래스의 타입으로 변경이 가능하다.
공변 반환 타입이 없는 경우, 자식 클래스를 사용하는 클라이언트 코드에서 명시적 형변환 처리를 해주어야 한다.
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
Child child = new Child();
Parent pc = new Child();
System.out.println(parent.createNewOne().getClass());
System.out.println(child.createNewOne().getClass());
System.out.println(pc.createNewOne().getClass());
}
}
class Parent {
protected Parent createNewOne() {
return new Parent();
}
}
class Child extends Parent {
// 부모 클래스로부터 재정의하였으나 반환형을 자식 클래스로 변경할 수 있다.
@Override public Child createNewOne() {
return new Child();
}
}- 인터페이스에 적용했을 때
public class Main {
public static void main(String[] args) {
Testable testable = new TestableImpl();
System.out.println(testable.tester().getClass());
}
}
interface Testable {
Testable tester();
}
class TestableImpl implements Testable {
@Override public TestableImpl tester() {
return new TestableImpl();
}
}