Cloneable
복제해도 되는 클래스임을 명시하는 용도의 인터페이스다.
Cloneable.java
public interface Cloneable { // 실제로 이렇게 비어있음
}
하지만 신기한 점이 있는데, clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이다.
Object.java
protected native Object clone() throws CloneNotSupportedException;
위 코드를 보면 clone() 메서드는 protected 접근 제한자를 가진다. 이렇게 되면 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
위 방법은 clone() 메서드를 오버라이드하여 public 접근제한자로 변경하여 외부 객체에서 접근 가능하도록 할 수 있다. 자식클래스는 부모클래스의 접근제한자보다 동일하거나 더 넓어야한다.
public clone() 메서드
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
Cloneable 인터페이스
public interface Cloneable { // 실제로 이렇게 비어있음
}
이 인터페이스는 메서드가 1개도 없다. 대신 Object의 protected 메서드인 clone() 메서드의 동작방식을 결정한다. Cloneabl을 구현한 클래스의 인스턴스에서 clone() 메서드를 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 호출한다.
인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위지만, Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다.
clone() 메서드의 일반 규약
1) 다음은 참이다.
x.clone() != x
2) 다음은 참이다.
x.clone().getClass == x.getClass()
3) 일반적으로 참이지만 필수는 아니다.
x.clone().equals(x)
clone() 메서드의 잘못된 구현
만약 clone() 메서드가 super.clone()이 아닌, 생성자를 호출해 얻은 인스턴스를 반환하면 이 클래스의 하위 클래스에서 super.clone()을 호출했을때 잘못된 클래스의 객체가 만들어진다. 결국 하위 클래스의 clone() 메서드가 제대로 동작하지 않게된다.
예제코드
Item.java
public class Item implements Cloneable {
private String name;
/**
* 이렇게 구현하면 하위 클래스의 clone()이 깨질 수 있다. p78
* @return
*/
@Override
public Item clone() {
// super.clone()을 쓰지 않고 생성자를 썼음
// 이렇게하면 규약이 깨짐
Item item = new Item();
item.name = this.name;
return item;
}
}
SubItem.java
public class SubItem extends Item implements Cloneable {
private String name;
@Override
public SubItem clone() {
// 생성자로 생성한 Item을 리턴하는 clone() 메서드 결과를 SubItem 으로 변환을 못함
// 상위타입(Item)을 하위타입(SubItem)으로 타입변환이 불가능
return (SubItem) super.clone();
}
...
}
Item.java에서 clone()을 올바르게 구현
public class Item implements Cloneable {
private String name;
/**
* 올바르게 구현한 clone()
* @return
*/
@Override
public Item clone() {
Item result = null;
try {
// 여기선 Item 이라고 생각하지만, SubItem 에서 호출하면 SubItem 타입이다.
result = (Item) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
return result;
}
}
clone() 메서드 사용예제
PhoneNumber.java
public final class PhoneNumber implements Cloneable {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
System.out.println("constructor is called");
}
public PhoneNumber(PhoneNumber phoneNumber) {
this(phoneNumber.areaCode, phoneNumber.prefix, phoneNumber.lineNum);
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드 (79쪽)
// 접근제한자는 부모클래스보다 같거나 넓은 범위의 접근제한자를 가져야한다.
// Object의 하위타입 PhoneNumber 리턴할 수 있다.(구체적잍 타입 리턴하면 클라이언트에서 타입캐스팅 안해도됨)
// protected 로 하면 하위클래스만 접근 가능한데, 거의 외부에서 접근하므로 public이 맞다.
@Override
public PhoneNumber clone() {
try {
// super.clone()을 사용해야한다.
// 여기서 생성자를 사용하면 안된다.
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) { // checkedException (Cloneable 구현하지 않았을 경우)
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
// 상위클래스의 clone() 모습
// @Override
// protected Object clone() throws CloneNotSupportedException {
// return super.clone();
// }
public static void main(String[] args) {
PhoneNumber pn = new PhoneNumber(707, 867, 5309);
Map<PhoneNumber, String> m = new HashMap<>();
m.put(pn, "제니");
/* 생성자로 만들진 않음, 결국엔 Object의 clone() 호출 */
PhoneNumber clone = pn.clone();
System.out.println(m.get(clone));
System.out.println(clone != pn); // 반드시 true
System.out.println(clone.getClass() == pn.getClass()); // 반드시 true
System.out.println(clone.equals(pn)); // true가 아닐 수도 있다.
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
* XXX는 지역 코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override public String toString() {
return String.format("%03d-%03d-%04d",
areaCode, prefix, lineNum);
}
}
이렇게 얻은 객체는 원본의 완벽한 복제본일 것이다. 클래스에 정의된 모든 필드는 원본 필드와 똑같은 값을 갖는다.
1) Cloneable 구현
public final class PhoneNumber implements Cloneable {
...
}
위 PhoneNumber 클래스 선언에 Cloneable을 구현해야한다. Object의 clone() 메서드는 Object를 반환하지만 PhoneNumber의 clone 메서드는 PhoneNumber를 반환하게 했다.
2) 형변환
@Override
public PhoneNumber clone() {
try {
// super.clone()을 사용해야한다.
// 여기서 생성자를 사용하면 안된다.
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) { // checkedException (Cloneable 구현하지 않았을 경우)
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
결국, 재정의한 메서드의 반환 타입은 사위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다. 이 방식으로 클라이언트가 형변환하지 않아도 되게끔 해주자.
3) try~catch 블록으로 CloneNotSupportedException 처리
@Override
public PhoneNumber clone() {
try {
// super.clone()을 사용해야한다.
// 여기서 생성자를 사용하면 안된다.
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) { // checkedException (Cloneable 구현하지 않았을 경우)
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
가변객체를 참조하는 경우
클래스가 가변객체를 참조할때 clone() 메서드를 사용한다면 큰 문제가 발생할 수 있다.
Stack.java
public class Stack implements Cloneable {
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;
}
public boolean isEmpty() {
return size ==0;
}
// 코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
// TODO stack -> elementsS[0, 1]
// TODO copy -> elementsC[0, 1]
// TODO elementsS[0] == elementsC[0]
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
/*
호출 안하면?
다른 두 인스턴스 stack, copy -> 동일한 elements 를 참조하게된다.
다른 두 인스턴스에서 동일한 배열을 쓰게됨
elements.clone()을 해서 배열을 복사해야 복사본, 원본 인스턴스가 각각의 elements를 가지게된다.
(얕은복사)
stack -> elementsS[0, 1]
copy -> elementsC[0, 1]
// 배열만 새로 만들고. 안에 있는 인스턴스들은 동일하게 참조한다. (여전히 위험하긴한다.)
elementsS[0] == elementsC[0]
*/
// result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// clone이 동작하는 모습을 보려면 명령줄 인수를 몇 개 덧붙여서 호출해야 한다.
public static void main(String[] args) {
Object[] values = new Object[2];
values[0] = new PhoneNumber(123, 456, 7890);
values[1] = new PhoneNumber(321, 764, 2341);
// 원본
Stack stack = new Stack();
for (Object arg : values)
stack.push(arg);
// 복사본
Stack copy = stack.clone();
System.out.println("pop from stack");
while (!stack.isEmpty())
System.out.println(stack.pop() + " ");
System.out.println("pop from copy");
while (!copy.isEmpty())
System.out.println(copy.pop() + " ");
System.out.println(stack.elements[0] == copy.elements[0]);
}
}
clone() 메서드가 super.clone()의 결과를 그대로 반환한다면, 반환된 Stack 인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하게된다. 이때 원본이나 복제본 중 하나를 수정한다면 다른 하나도 수정되어 불변식을 해치게된다. 만약 Stack 클래스의 하나뿐인 생성자를 호출했다면 이러한 상황은 발생할 수 없다.
clone() 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.
위 Stack 클래스의 경우에는 스택 내부 정보를 복사해야한다. elements 배열의 clone을 재귀적으로 호출해주자.
Stack.java의 가변 상태를 참조하는 클래스용 clone 메서드
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
배열의 clone은 런타임 타입과 컴파일 타입 모두가 원본 배열과 똑같은 배열을 반환한다.
elements 필드가 final이였다면?
final 필드에는 새로운 값 할당이 불가능하므로 앞의 방식은 작동하지 않는다. 이는 Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야할 수도 있다.
▶ final로 선언한다면 위 Stack.java 예제에서는 아래 예제를 수행시킬 수 없다.
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
CloneNotSupportedException
public class CloneNotSupportedException extends Exception {
private static final long serialVersionUID = 5195511250079656443L;
/**
* Constructs a <code>CloneNotSupportedException</code> with no
* detail message.
*/
public CloneNotSupportedException() {
super();
}
/**
* Constructs a <code>CloneNotSupportedException</code> with the
* specified detail message.
*
* @param s the detail message.
*/
public CloneNotSupportedException(String s) {
super(s);
}
}
1) CheckedException
Exception을 상속하고 있다. CheckedException 이기 때문에 클라이언트에서 이 오류를 잡아서 처리해줘야한다.
public class CloneNotSupportedException extends Exception {
...
}
사실, CloneNotSupportedException가 CheckedException이여야했던 이유는 이펙티브 자바의 저자 조차도 이해할 수 없다고 한다. 클라이언트에서 해당 에러를 잡아서 처리할 행위가 없는데 CheckedException이기 때문에 호출을 하는 코드마다 해당 오류를 처리해줘야한다. UncheckedException이면 더 좋았을거라는 생각이 든다.
상속용 클래스는 Cloneable 구현하지않기
부모클래스 Shape.java
public abstract class Shape implements Cloneable {
private int area;
public abstract int getArea();
}
일반적으로 상속용 클래스에 Cloneable 인터페이스 사용을 권장하지 않는다. 이 클래스를 확장하려는 프로그래머는 자식클래스가 어떻게 clone() 메서드를 구현해야할지 고민을 해야하는 부담을 주기 때문이다.
기본 clone() 구현
public abstract class Shape implements Cloneable {
private int area;
public abstract int getArea();
/**
* p84, 부담을 덜기 위해서는 기본 clone() 구현체를 제공하여,
* Cloenable 구현 여부를 서브 클래스가 선택할 수 있다.
* (하위클래스가 굳이 구현을 안하게끔 해준다.)
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
부담을 덜어주기 위해서 기본 clone() 구현체를 제공한다. 이렇게하면 Cloneable 구현 여부를 자식 클래스가 선택할 수 있다.
또다른 방법
clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 할 수 있다.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
복사 생성자
Cloneable을 구현하는 것 대신, 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다. 복사 생성자란 단순히 자신과 같은 클래스의 은서튼스를 인수로 받는 생성자를 말한다.
// 복사 생성자
public PhoneNumber(PhoneNumber phoneNumber) {
this(phoneNumber.areaCode, phoneNumber.prefix, phoneNumber.lineNum);
}
이 방법은 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않는다. 불필요한 검사 예외를 던지지 않고, 형변환도 필요하지 않다. 또한 해당 클래스가 구현한 인터페이스 타입의 인스턴스로도 받을 수 있는 장점이 있다.
'Book > Effective Java' 카테고리의 다른 글
[교재 EffectiveJava] 포스팅 목록 정리 (0) | 2023.06.11 |
---|---|
[교재 EffectiveJava] 아이템 14. Comparable을 구현할지 고려하라 (0) | 2023.06.10 |
[교재 EffectiveJava] 아이템 12. toString을 항상 재정의하라 (0) | 2023.05.29 |
[교재 EffectiveJava] 아이템11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2023.05.29 |
[교재 EffectiveJava] 아이템 10. equals는 일반 규약을 지켜 재정의하라 (1) | 2023.05.23 |