1. 배경
// Cloneable는 복제해도 되는 클래스임을 명시하는 인터페이스
// clone메서드는 해당 인터페이스가 아닌 Object에 protected로 선언되어 있음.
// 따라서, Cloneable을 구현하는 것만으로는 clone메서드를 호출할 수 없다.
2. Cloneable 인터페이스의 역할
// - 기본적으로 해당 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다.
// - 해당 인터페이스를 구현하고 object의 clone메서드를 재정의해 호출시 해당 클래스의 필드를 복사한 객체 반환
// - 해당 인터페으스를 구현안하고 clone 메서드 사용시 CloneNotSupportedException 발생
3. Clone메서드의 기본적인 구현 방법
// - Cloneable 인터페이스 구현
// - Override로 clone 메서드 구현
// -> super.clone() : object의 clone메서드로 해당 클래스 복사 후 casting 실행
// - try-catch로 오류 처리
@Override
public CloneClass clone(){
try{
return (CloneClass) super.clone();
}
catch (CloneNotSupportedException e){
// Cloneable을 구현했다면 일어나지 않음
throw new AssertionError();
}
}
4. 클래스가 가변 객체를 참조하는 경우
// - 예시 : Stack
// - elements 필드는 위의 방법으로 복사시 기존 인스턴스와 똑같은 인스턴스를 참조하게 되어
// 하나 수정시 둘다 수정되어 복사의 개념에 위배된다.
해결책
// - clone메서드는 사실상 생성자와 같은 효과를 낸다.
// - 즉, clone은 원본 객체의 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
// -> elements 배열의 clone을 재귀적으로 호출한다.
// - 해당 필드가 final로 선언되어있다면 동작하지 않는다.
@Override
public Stack clone(){
try{
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
}
catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
- 해당 코드에서 elements 는 Object[] 임.
5. Clone 메서드를 재귀적으로 호출하는 것만으로 충분하지 않은 경우
// - 예시 : HashTable
// 해시테이블 내부는 버킷들의 배열이고 각 버킷들은 키-값 쌍을 담는 연결리스트의 첫번째 엔트리를 참조한다.
// 복제본은 자신만의 버킷배열을 갖지만 이 배열은 원본과 같은 연결리스트를 참조하여 문제 야기
// -> 즉, Entry 각각이 원본과 같은 인스턴스를 참조하게 된다.
@Override
public HashTable clone(){
try{
HashTable result = (HashTable) super.clone();
// 복제본은 자신만의 버킷 배열을 갖지만 이때, 배열은 원본과 같은 연결리스트를 참조하여 문제 야기
result.buckets = buckets.clone();
return result;
}
catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
- 해당 HashTable은 연결리스트로 구성 됨.
해결책
// - 각 버킷을 구성하는 연결리스트를 복사한다.
// -> 즉, Entry 클래스에 deepCopy를 선언해 Entry 자체를 새로 생성해 복사하는 메서드를 만들고
// 이 후, 하나하나 넣는 방식으로 복사를 진행한다.
// 하지만 deepcopy시에 자신이 가리키는 연결리스트에 대해서 재귀적으로 deepcopy를 호출하므로
// 스택 오버플로를 일으킬 문제가 발생한다.
// - deepCopy를 재귀 호출 대신 반복자를 사용하여 순회하는 방향으로 수정해야 한다.
// 기존
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();
}
}
6. Clone의 문제점
// - 생성자를 쓰지 않아 인스턴스를 생성하는 과정에서 오류가 발생할 수 있다.
// - clone의 규약이 정확하지 않다.
// - final 필드를 제대로 사용할 수 없다.
// - 불필요한 예외, 형변환이 많다.
7. 결론
// -> 복사 생성자(변환 생성자)와 복사 팩터리(변환 팩터리)라는 더 나은 객체 복사 방식을 사용하자.
// 복사 생성자 : 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자
- 복사 생성자 : 객체 자체를 복사
- 복사 팩터리 : 객체의 필드 값을 복사하여 새로운 객체 생성
장점
- 생성자 사용
- 엉성화된 문서화된 규약에 기대지 않음
- final 필드 용법과도 충돌하지 않는다.
- 불필요한 예외도 던지지 않는다.
- 형변환 필요도 없다.
- 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다. (HashSet 인스턴스를 TreeSet 타입으로 복제 가능)
// 1. 복사 생성자 사용
public class Person {
private String name;
private int age;
// 복사 생성자
public Person(Person other) {
this.name = other.name;
this.age = other.age;
}
// 기본 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// 2. 복사 팩터리 사용
public class Person {
private String name;
private int age;
// 기본 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 복사 팩토리 메서드
public static Person createCopy(Person other) {
return new Person(other.getName(), other.getAge());
}
}
https://github.com/mokjaemin/EffectiveJAVA
'Language > Java Plus' 카테고리의 다른 글
Effective JAVA - Item15 : 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2023.10.05 |
---|---|
Effective JAVA - Item14 : Comparable을 구현할지 고려하라. (0) | 2023.09.26 |
Effective JAVA - Item12 : toString을 항상 재정의하라. (0) | 2023.09.19 |
Effective JAVA - Item11 : equals를 재정의하려거든 hashcode도 재정의하라. (0) | 2023.09.18 |
Effective JAVA - Item10 : equals는 일반 규약을 지켜 재정의하라. (1) | 2023.09.18 |