super.clone()을 사용하는 것을 보면 결국 Object의 clone()을 사용하는 것인데, 메모리 차원에서 복사하지 않을까 추측
기존의 clone() : protected인데, 상속한 클래스에서는 접근 지시자가 동일하거나 더 넓어야하기도 하기에 보통은 public으로 선언하고 사용한다.
애초에 클라이언트 코드에서 사용하기 위해 넣는 경우가 많기 때문이다.
반환 타입이 Object인데, 메소드를 override할 때, 반환 값 또한 선언된 반환값의 상속된 타입이라면 override이다.
Checked exception을 사용하는게 크게 의미 없는 상황으로, unchecekd exception을 사용한다.
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");
}
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
public static void main(String[] args) {
PhoneNumber pn = new PhoneNumber(707, 867, 5309);
Map<PhoneNumber, String> m = new HashMap<>();
m.put(pn, "제니");
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가 아닐 수도 있다.
}
예시 코드
하위 클래스의 clone이 깨질 수 있는 경우
SubItem -> Item (하위 타입 -> 상위 타입) : 가능하다.
Item -> SubItem (상위 타입 -> 하위 타입) : 불가능하다.
public SubItem clone()을 없애도, clone() 이 호출될 때 super.clone()이 호출되기 때문에 Item에서 Item이 반환된다.
super.clone() 의 반환 타입이 비 결정적이다.
public class Item implements Cloneable {
private String name;
@Override
public Item clone() {
Item item = new Item();
item.name = this.name;
return item;
}
}
``` java
public class SubItem extends Item implements Cloneable {
private String name;
@Override
public SubItem clone() {
return (SubItem)super.clone();
}
public static void main(String[] args) {
SubItem item = new SubItem();
SubItem clone = item.clone();
System.out.println(clone != item);
System.out.println(clone.getClass() == item.getClass());
System.out.println(clone.equals(item));
}
}
올바르게 구현한 경우 (super.clone() 사용하기)
생성자 호출하지 않는 방법
new Item();을 사용하면 안된다.
super.clone()을 호출 했을 때 어떤 타입이 나올지 정해준다. 현재 Item 클래스에서는 Item으로 나오지만,
Item을 상속 받은 SubItem이 호출하면 SubItem 타입으로 받을 수 있다.
CloneNotSupportedException은 Cloneable 인터페이스를 상속했는지 확인하는 기능을 한다.
public class Item implements Cloneable {
private String name;
@Override
public Item clone() {
Item result = null;
try {
result = (Item) super.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
핵심 정리: 가변 객체의 clone 구현하는 방법
접근 제한자는 public, 반환 타입은 자신의 클래스로 변경한다.
super.clone을 호출한 뒤 필요한 필드를 적절히 수정한다.
배열을 복제할 때는 배열의 clone 메서드를 사용하라.
shallow copy이다.
경우에 따라 final을 사용할 수 없을지도 모른다.
필요한 경우 deep copy를 해야한다.
super.clone으로 객체를 만든 뒤, 고수준 메서드를 호출하는 방법도 있다.
외부 API를 통해 객체에 데이터를 주입하는 방법
오버라이딩 할 수 있는 메서드는 참조하지 않도록 조심해야 한다.
하위 클래스에서 오버라이딩하여 동작을 바꿀 수 있기 때문이다.
객체를 만드는 과정에 끼어드는 것이기 때문이다.
상속용 클래스는 Cloneable을 구현하지 않는 것이 좋다.
Cloneable을 상속하는 순간, 상속용 클래스를 사용하는 개발자에게 큰 부담을 주기 때문이다.
상속용 클래스에서 Cloneable을 직접 구현해두고 상속하게 하는 방법
하위 클래스에서 Cloneable 구현을 막는 방법
Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 동기화(synchronized)를 해야 한다.
예시 코드
가변 상태를 참조하는 클래스용 clone 메서드
stack과 copy는 같은 element를 보고 있다.
주어진 배열을 같이 참조하고 있기 때문에, 한 쪽의 변화가 일어나고, 그 변화를 다른 쪽에서 인지하지 못하면 문제가 생기게 된다.
배열 복제해도 shallow copy이다.
elementS[0,1], elementC[0,1] 으로 두어도,
elementsS[0] == elementsC[0] 이다.
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;
}
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
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);
}
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]);
}
}
deep 복제
주석 처리된 코드(Linked List)와 같이 재귀적인 구현을 하면, stack이 쌓여 StackOverFlow가 날 수도 있기 때문에
Iterative하게 구현하는 것을 권장한다.
public class HashTable implements Cloneable {
private Entry[] buckets = new Entry[10];
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;
}
public void add(Object key, Object value) {
this.next = new Entry(key, value, null);
}
// public Entry deepCopy() {
// return new Entry(key, value, next == null ? null : next.deepCopy());
// }
public 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() {
HashTable result = null;
try {
result = (HashTable)super.clone();
result.buckets = new Entry[this.buckets.length];
for (int i = 0 ; i < this.buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = this.buckets[i].deepCopy(); // p83, deep copy
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) {
HashTable hashTable = new HashTable();
Entry entry = new Entry(new Object(), new Object(), null);
hashTable.buckets[0] = entry;
HashTable clone = hashTable.clone();
System.out.println(hashTable.buckets[0] == entry);
System.out.println(hashTable.buckets[0] == clone.buckets[0]);
}
}
<br><br>
## 핵심 정리: clone 대신 권장하는 방법
규약을 모두 지키며 clone을 재정의하기에는 많은 노력이 필요하기 때문에...
- “복사 생성자” 또는 변환 생성자, “복사 팩터리” 또는 변환 팩터리.
* 복사 생성자를 사용했을 때의 장점
* 생성자를 통해 복제하면 명확하다.
* 기존의 생성자에 검증 작업을 생략할 수 있다.
* final을 쓸 수 있다.
* 복사 생성자를 사용하지 않는 경우, 굳이 clone()을 구현하기 위해 final을 떼어내야 하기 때문에 고민해볼 필요가 있다.
- 또 다른 큰 장점 중 하나로 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
* 클라이언트가 복제본의 타입을 결정할 수 있다.
- 읽어볼 것) Josh Bloch on Design, “Copy Constructor versus Cloning”
<br>
## 예시 코드
### 복사 생성자
``` 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");
}
// Copy Constructor
public PhoneNumber(PhoneNumber phoneNumber) {
this(phoneNumber.areaCode, phoneNumber.prefix, phoneNumber.lineNum);
}
}
완벽 공략
p80, 비검사 예외(UnChecked Exception)였어야 했다는 신호다.
p81. HashTable과 LinkedList
아이템 11.에서 다루었던 내용, 참고
p83, 깊은 복사 (deep copy)
사용하는 것이 까다롭다.
예시 코드에서 deepCopy()를 생각해보더라도,
결국 그 key, value는 그대로 가져다 쓰기 때문에,
어디까지 deep copy가 deep copy인지 기준이 오묘하기 때문이다.
p83, 리스트가 길면 스택 오버플로를 일으킬 위험이 있기 때문이다.
p85, clone 메서드 역시 적절히 동기화해줘야 한다.
Thread Safe한 방법 중 하나
p86, TreeSet ("HashSet 객체를 TreeSet으로 복사해서 사용할 수 있다.")
완벽 공략 29. UncheckedException
왜 우리는 unchecked exception를 선호하는가?
컴파일 에러를 신경쓰지 않아도 되며,
try-catch로 감싸거나
메서드 선언부에 선언하지 않아도 된다.
그렇다면 우리는 비검사 예외만 쓰면 되는걸까? 검사 예외는 왜 있는것일까?
그렇다면 우리는 비검사 예외만 쓰면 되는걸까?
왜 잡지 않은 예외를 메서드에 선언해야 하는가?
메서드에 선언한 예외는 프로그래밍 인터페이스의 일부다. 즉, 해당 메서드를 사용하는 코드가 반드시 알아야 하는 정보다. 그래야 해당 예외가 발생했을 상황에 대처하는 코드를 작성할 수 있기 때문이다.
비검사 예외는 그럼 왜 메서드에 선언하지 않아도 되는가?
비검사 예외는 어떤 식으로든 처리하거나 복구할 수 없는 경우에 사용하는 예외다. 가령, 숫자를 0으로 나누거나, null 레퍼런스에 메서드를 호출하는 등.
이런 예외는 프로그램 전반에 걸쳐 어디서든 발생할 수 있기 때문에 이 모든 비 검사 예외를 메서드에 선언하도록 강제한다면 프로그램의 명확도가 떨어진다.
추가
throws는 자체가 API라고 볼 수 있다.
클라이언트가 사용할 때, 이를 인지하고 대응(복구)할 수 있도록 할 수 있다.
API를 사용하는 쪽에 명시적으로 정보를 건내주는 역할을 한다.
Runtime 계열도 던져서 잡을 수 있기는 하다.
그러나 어차피 대응(복구)을 할 수 없기 때문에 던지는게 의미가 없다. (비효율적이고 명확도를 떨어뜨린다.)
즉, 클라이언트 쪽에서 예외 상황이, 아래와 같은 경우에 사용하도록 한다.
복구가 가능한 경우
명시적으로 예외 상황의 정보를 전달해야하는 경우
언제 어떤 예외를 써야 하는가?
단순히 처리하기 쉽고 편하다는 이유만으로 RuntimeException을 선택하지는 말자.
사용의 이유가 "편리하다"는 아무래도 우선순위가 뒤로 밀리게 된다.
가이드라인: 클라이언트가 해당 예외 상황을 복구할 수 있다면 검사 예외를 사용하고, 해당 예외가 발생했을 때 아무것도 할 수 없다면, 비검사 예외로 만든다.
아이템 13. clone 재정의는 주의해서 진행하라.
핵심 정리 : 애매모호한 clone 규약
예시 코드
예시 코드
하위 클래스의 clone이 깨질 수 있는 경우
Item -> SubItem (상위 타입 -> 하위 타입) : 불가능하다.
private String name;
@Override public Item clone() { Item item = new Item(); item.name = this.name; return item; } }
올바르게 구현한 경우 (super.clone() 사용하기)
생성자 호출하지 않는 방법
new Item();
을 사용하면 안된다.CloneNotSupportedException
은 Cloneable 인터페이스를 상속했는지 확인하는 기능을 한다.핵심 정리: 가변 객체의 clone 구현하는 방법
예시 코드
가변 상태를 참조하는 클래스용 clone 메서드
stack과 copy는 같은 element를 보고 있다.
deep 복제
Iterative하게 구현하는 것을 권장한다.
// public Entry deepCopy() { // return new Entry(key, value, next == null ? null : next.deepCopy()); // }
}
완벽 공략
완벽 공략 29. UncheckedException
왜 우리는 unchecked exception를 선호하는가?
그렇다면 우리는 비검사 예외만 쓰면 되는걸까?
추가
언제 어떤 예외를 써야 하는가?
완벽 공략 30. TreeSet
AbstractSet을 확장한 정렬된 컬렉션
예시 코드
natural order가 존재하지 않는 경우
ClassCastException이 발생하게 된다.
Comparator만 넘겨준다.
다른 방법으로는 PhoneNumber에서 comparable을 정의하는 방법이 있다.
동기화가 적용된 TreeSet을 사용하고 싶다면
Collections.synchronizedSet();
를 사용하면 된다.public static void main(String[] args) { TreeSet numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));
Set phoneNumbers = Collections.synchronizedSet(numbers);
phoneNumbers.add(new PhoneNumber(123, 456, 780));
phoneNumbers.add(new PhoneNumber(123, 456, 7890));
phoneNumbers.add(new PhoneNumber(123, 456, 789));
} }