객체의 복제를 위한 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();
}
}
--- ---
## Referenced by
- [공변반환타입](https://ingnoh.tistory.com/153)
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() 구현 방법
✅ super.clone()을 통해 만들어진 객체가 원본과 똑같은 값을 갖는 복제본이 되고, 모든 필드가 기본 타입이거나 불변 객체이므로 만들어진 복제본에서 수정할 것이 없다. ✅ PhoneNumber 클래스 선언에 Cloneable을 구현한다고 추가해야 동작이 가능하다. ✅ 해당 방식의 경우 클래스가 가변 객체를 참조하는 순간 불가능하다. ✅ 공변반환타입이 가능해지며 클라이언트에서 일일이 형변환을 해줄 필요가 없어진다. ✅ Number 클래스가 cloneable을 구현하고 다른 클래스를 상속받지 않으니
CloneNotSupportedException
이 터질 일이 없으므로 불필요한 checked exception을 try-catch로 감싸서 메서드의 throws 절을 없애고 클라이언트에서 더 편하게 사용할 수 있도록 해주면 좋다고 한다.✅ Stack의 하나뿐인 생성자를 호출한다면 불변식을 해치지 않겠지만,원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게 된다. \ ✅ clone은 원본 객체에 아무런 해를 끼치지 않으면서 복제된 객체의 불변식을 보장해야 한다. (생성자와 같은 효과) ✅ Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. (복제해야 할 때는 제거해야 할 수도 있다)
가변 객체가 있는 클래스의 Cloneable을 구현하는 방법(고수준 API 사용)
super.clone
을 호출하여 모든 필드를 초기 상태로 설정✨ 단, 해당 메서드는 final이나 private로 정의하여 하위에서 재정의를 막아야한다. 만약 public이라면 throws절을 없애야 편하게 사용할 수 있다.
주의 사항
✅ 상속용 클래스는 Cloneable을 구현해서는 안된다. \ ✅ clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 한다. \ ✅ 만약 clone()에서 재정의 가능한 메서드를 호출하게 되면 하위 클래스에서
super.clone()
을 호출했을 때 상위 클래스의clone()
에서 하위 클래스의 재정의된 메서드를 호출하게 되고 예측할 수 없는 복제본이 만들어질 가능성이 생긴다.=> child를 clone했을 때 parent레벨에서 조정되어야 할 값들이 child에 재정의된 메서드의 동작 방식대로 조정되고 의도치 않은 방향으로 값이 복제되는 문제가 발생할 수 있다. \
✅ Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다. => Object.clone() 은 멀티 쓰레드 환경을 고려하지 않았으므로 쓰레드 안전한 클래스를 만들기 위해서는 clone()메서드가 아무런 작업을 하지 않더라도 재정의하고 동기화 해주어야 한다.
🍑 결론
✅ 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. ✅ 복제 기능은 생성자와 팩터리를 사용하는 것이 가장 좋다. \ ✅ 어쩔 수 없이 확장하려는 클래스가 Cloneable을 구현한 경우 clone()을 재정의해줘야 하지만, 아닐 경우 아래와 같이 복사 생성자와 복사 팩터리를 사용하는 것이 좋다.
** 공변 반환 타입(Convariant Return Type) JDK 1.5부터 추가된 개념으로 부모 클래스의 메소드를 오버라이딩하는 경우, 부모 클래스의 반환 타입은 자식 클래스의 타입으로 변경이 가능하다. \ 공변 반환 타입이 없는 경우, 자식 클래스를 사용하는 클라이언트 코드에서 명시적 형변환 처리를 해주어야 한다.
interface Testable { Testable tester(); } class TestableImpl implements Testable { @Override public TestableImpl tester() { return new TestableImpl(); } }