Item13. clone overriding은 주의해서 진행하라
2021-12-12 15:26:47
Object class에는 clone() method가 protected로 선언되어 있고, 객체를 복사하려면 java.langCloneable Interface를 implement하여 복사할 수 있다.
package java.lang;
public interface Cloneable {
}
(https://docs.oracle.com/javase/7/docs/api/java/lang/Cloneable.html)
Cloneable은 아무 method도 없는 marker interface지만 ,Object의 clone method의 동작 방식을 결정한다.
Cloeanble을 구현한 class에서부터 만들어진 객체에서 clone을 호출하면 객체의 필드들을 복사한 객체를 반환하도록 하고, 그렇지 않은 객체에서는 CloneNotSupportedException을 던진다.
java Object 명세에 적혀있는 clone method에서 지켜야할 규약은 다음과 같다.
어떤 객체 x에 대해서도 다음은 true이다.
1. x.clone() != x; (복사된 객체와 원복 객체의 주소가 다름)
2. x.clone().getClass() == x.getClass();
3. x.clone().equals(x) (일반적으로 참이나 필수는 아님)
관례상 이 clone() method가 반환하는 객체는 super.clone을 호출해 얻어야 하며,
반환된 객체와 원본 객체는 독립적이여야 한다.
super.clone을 통해 method를 부모쪽으로 재귀적으로 호출해서 받으므로, 강제성이 없는 생성자 연쇄와 비슷한 방식이다.
문제는 clone() method를 통해 생성된 객체가 , 가변 객체를 참조하는 경우이다.
예를 들어 다음과 같은 Hotel 객체가 있는데, 이 객체의 필드 중에 가변필드인 guests 필드가 있다고 가정하자.
@Slf4j
@Getter
public class Hotel implements Cloneable {
public String location;
public List<Person> guests; // 투숙객
// 입실
public void addGuest(Person person){
if(guests == null){
guests =new ArrayList<>();
}
guests.add(person);
}
//투숙객 정보 변경
public void updateGuest(Person oldPerson, Person newPerson){
guests.stream().filter((guest) -> guest.equals(oldPerson))
.findFirst()
.ifPresentOrElse(
(p)->{
log.info("투숙객을 찾아서 변경합니다.");
p.setAge(newPerson.getAge());
p.setName(newPerson.getName());
},
()->log.info("투숙객을 찾지 못했습니다.")
);
}
@Override
public Hotel clone() {
try {
return (Hotel) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ( !(o instanceof Hotel)) {
return false;
};
Hotel hotel = (Hotel) o;
return Objects.equals(getLocation(), hotel.getLocation()) && Objects.equals(getGuests(), hotel.getGuests());
}
@Override
public int hashCode() {
return Objects.hash(getLocation(), getGuests());
}
}
위 객체를 그대로 clone(얇은 복사) 한뒤 원본객체에서 투숙객 정보를 바꾸면 어떻게 될까?
class HotelTest {
@Test
@DisplayName("가변 객체를 얇은 복사를 사용하면 안됩니다.")
void testClone() {
Hotel hotel = new Hotel();
Person personA = new Person(10, "김이름");
Person personB = new Person(10,"박이름");
Person personC = new Person(10,"장이름");
Person newPerson = new Person(20,"김새이름");
hotel.addGuest(personA);
hotel.addGuest(personB);
hotel.addGuest(personC);
// 얇은 복사
Hotel copyedHotel = hotel.clone();
// 물리적으로 다른 주소를 가지지만, 논리적으로는 동일한 copyedHotel
assertThat(hotel != copyedHotel).isTrue();
assertThat(hotel.equals(copyedHotel)).isTrue();
List<Person> guests = hotel.getGuests();
// person A -> personB 로 변경
hotel.updateGuest(personA,newPerson);
List<Person> copyedGuest = copyedHotel.getGuests();
assertThat(copyedGuest.contains(newPerson));
}
}
복사된 객체에서 guest가 원본 객체의 guests의 메모리 주소를 참조함으로 동일하게 바뀐다.
- clone method 는 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
위 내용을 참조해서 Hotel class의 clone 메소드를 수정하면 다음과 같다.
@Override
public Hotel clone() {
try {
Hotel copyedHotel = (Hotel) super.clone();
List<Person> newGuests = new ArrayList<>();
// list의 모든 요소를 복사해준다.
Collections.copy(newGuests,guests);
copyedHotel.guests = newGuests;
return copyedHotel;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
+) 가변 필드가 final인 경우는 불가능
해시테이블과 같이 linkedList로 구현된 경우 Clone 방법
public class HashTable implements Cloneable{
private Entity[] buckets = new Entity[]{};
private static class Entry{
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
위와 같이 각 해시테이블내 각 버킷 인덱스가 entry의 linkedList로 구현되어 있다고 하면
@Override
protected HashTable clone() {
try{
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
복제본은 자신만의 copy본을 갖지만 원본과 동일한 연결 리스트를 참조한다. 즉 복제본의 entry의 next가 원본의 entry를 가르킬수도 있는 가능성이 있다.
따라서 각 버킷을 구성하는 연결리스트 자체도 복사해야 한다.
- 재귀적으로 호출하여 복사
// 연결 리스트내 엔트리도 재귀적으로 복사해준다.
public Entry deepCopy(){
return new Entry(key,value,next ==null ? null : next.deepCopy());
}
- for loop 사용하여 복사
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
protected HashTable clone() {
try{
HashTable result = (HashTable) super.clone();
result.buckets = new Entity[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();
}
}
- 객체를 생성하는 목적이 아닌 상속용 class는 cloneable을 구현해서는 안된다.
상속용 class에서 clone method를 하위 class에서 overriding 하지 못하게, final 로 구현시켜놓으면 된다.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
정리
- cloneable 을 구현하는 모든 class는 clone을 overriding 해주어야 한다.
- 반환타입은 class 자신으로 , 접근 제한자는 public으로 설정한다.
- super.clone() 을 호출한뒤, 부모에 있는 가변 객체를 복사한다.
// 복사 생성자 : 자신과 같은 class의 인스턴스를 받는 생성자
public Hotel ( Hotel hotel){
this.location = hotel.location;
this.guests = new ArrayList<>();
Collections.copy(this.guests,hotel.getGuests());
}
// 복사 팩터리
public static Hotel getNewInstance(Hotel hotel){
return new Hotel(hotel);
}
- clone() 보다는 복사 생성자 또는 복사 팩터리 방식을 고려하자 (배열 제외)
복사 생성자 , 복사 팩터리 방식을 사용하면 다음과 같은 이점을 갖기 때문이다.
-
cloneable의 단점이 없다 (문서화된 규약에 종속적이지 않고, 불필요한 검사 예외도 던지지 않고, 형변환도 필요가 없음 )
-
해당 class가 구현체라면 인터페이스 타입의 인스턴스를 인수로 받을 수 있다. (유연하게 개발이 가능하다)
댓글
이 게시글에 대한 의견을 공유해주세요!
