꾸준한 스터디
article thumbnail

clone 메서드?

Object의 clone 메서드는 자기 자신의 인스턴스를 복사하는 기능을 가지고 있다.

생성된 인스턴스의 참조값을 다시 대입하는게 아니라 같은 인스턴스를 다시 다른 메모리공간에 생성하여 복사된 인스턴스의 참조값을 준다.

인스턴스 자체는 별개의 인스턴스로 복사를 했지만 정작 인스턴스에서 쓰이는 값들이 Reference Type 이라면 이전 원본의 참조값을 가지고 있어서 복사된 인스턴스의 상태값을 바꾸면 원본 인스턴스의 상태값도 변경이 된다.

불변 객체일 경우 한번 세팅된 상태값이 변경되지 않으니 복사본이 같은 참조값을 가져도 무방하다.

 

Object의 clone 메서드

사용하기 좀 까다로운 메서드인데

접근제어자가 protected여서 같은 패키지, 상속받은 클래스일 경우만 해당 메서드를 사용할 수 있다.

Overriding 할 때는 외부 객체에서 사용할 수 있도록 메서드 접근제어자를 public으로 변경한다.

또한, Cloneable 인터페이스를 구현해야하는데 이 인터페이스는 아무것도 정의되어 있지 않지만 이 인터페이스를 구현하지 않으면 CloneNotSupportedException 예외가 난다.

어쨋던 저쨋던 Object클래스의 clone메서드를 실행해야 사용하는 클래스의 복사본이 만들어진다. Object 클래스의 clone 메서드는 native 메서드라 내부적으로 불러온 클래스의 인스턴스를 메모리에 복사하는 로직이 별도로 구현되어 있을 것이다.

clone 규약

  • x.clone() != x 반드시 true. 원본과 복사본은 반드시 다른 인스턴스여야 한다. (참조값이 다르다)
  • x.clone().getClass() == x.getClass() 반드시 true. 원본과 복사본은 반드시 같은 클래스여야 한다.
  • x.clone().equals(x) true가 아닐 수 있다. 원본과 복사본의 상태값 중 유일한 값일 경우 ID 등.. 상태값이 다를 수 있다.

 

왜 생성자로 인스턴스를 만들지 않고 clone 메서드를 사용하는 걸까?

타입 캐스팅이 안되는 문제점을 가지고 있다.

package item13.clone_use_constructor;

public class Item implements Cloneable {

	private String name;

	// 생성자로 생성한 Item을 리턴하는 clone
	@Override
	public Object clone() {
		Item item = new Item();
		item.name = this.name;
		return item;
	}
	
}
package item13.clone_use_constructor;

public class SubItem extends Item implements Cloneable {
	
	private String name;
	
	// 일반적인 규약을 따른 clone 메서드
	@Override
	public SubItem clone() {
		// Item 클래스의 생성자를 불러오는 overriding
		// 부모클래스를 자식클래스로 형변환 할 수 없다.
		// ClassCastException
		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));
	}

}

부모클래스에서 clone 메서드 구현을 생성자로 리턴하는 방식을 썼기 때문에 자식 클래스에서 캐스팅 되지 않아 예외가 난다. 부모클래스에서도 clone 메서드를 규약에 따라 정의해야 한다.

 

package item13.clone_use_constructor;

public class Item implements Cloneable {

	private String name;

	// 제대로 구현된 clone 메서드 Object의 clone메서드를 부른다.
	// Item 클래스를 반환하지만 서브 클래스에서 메서드 사용시 불러온 클래스 타입을 리턴한다.
	@Override
	protected Item clone() {
		try {
			return (Item) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new AssertionError();
		}
	}

}
package item13.clone_use_constructor;

public class SubItem extends Item implements Cloneable {
	
	private String name;
	
	// 일반적인 규약을 따른 clone 메서드
	@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); // true
		System.out.println(clone.getClass() == item.getClass()); // true
		System.out.println(clone.equals(item)); // false
	}

}

Item의 clone 메서드에서 반환 타입이 Item 이지만 SubItem 클래스로 복사된 인스턴스가 잘 담긴다.

 

클래스가 불변객체라면 Cloneable 인터페이스 구현과 규약에 따른 clone메서드를 정의하면 큰 문제는 없다.

불변객체의 clone 메서드 구현 방법

  • Cloneable 인터페이스 구현
  • clone 메서드 public으로 재정의. 반환 타입은 자신의 클래스로 변경한다.
  • super.clone()을 사용해야 한다.
package item13;

import java.util.HashMap;
import java.util.Map;

public final class PhoneNumber implements Cloneable{
	
	private final int areaCode, prefix, lineNum;
	
	public PhoneNumber(int areaCode, int prefix, int lineNum) {
		super();
		this.areaCode = rangeCheck(areaCode, 999, "지역코드");
		this.prefix = rangeCheck(prefix, 999,"프리픽스");
		this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
	}
	
	private static int rangeCheck(int val, int max, String arg) {
		if(val < 0 || val > max) {
			throw new IllegalArgumentException(arg + " : " + val);
		}
		return val;
	}

	/**
	 * 전화번호의 문자열 형식은 "XXX-YYY-ZZZZ" 형태의 12글자
	 * XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호
	 * 각 대문자는 10진수 숫자를 나타낸다.
	 * 각 부분의 자릿수를 채울 수 없다면 앞에서부터 0으로 채운다.
	 */
	@Override
	public String toString() {
		return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
	}
	
	// PhoneNumber 클래스의 정보를 제공하는 팩터리 메서드
	public static PhoneNumber of(String phoneNumberString) {
		String[] split = phoneNumberString.split("-");
		PhoneNumber pn = new PhoneNumber(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]));
		
		return pn;				
	}

	@Override
	public boolean equals(Object obj) {
		if(obj == this) return true;
		
		if(!(obj instanceof PhoneNumber)) return false;
		
		PhoneNumber pn = (PhoneNumber) obj;
		return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
	}

	@Override
	public int hashCode() {
		int result = Integer.hashCode(areaCode);
		result = 31 * result + Integer.hashCode(prefix);
		result = 31 * result + Integer.hashCode(lineNum);
		return result;
	}

	// 메서드 overriding시 접근 제어자는 원본 메서드 보다 범위를 넓게 가질 수 있다.	
	// private, default 제어자는 protected보다 범위가 더 좁기 때문에 선언 불가
	// 외부 객체에서 사용할 수 있게 public으로 선언
	@Override
	public PhoneNumber clone() {
		try {
			return (PhoneNumber) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new AssertionError();
		}
	}

	public static void main(String[] args) {
		PhoneNumber jenny = new PhoneNumber(707, 789, 1304);
		Map<PhoneNumber, String> m = new HashMap<>();
		
		m.put(jenny, "jenny");
		PhoneNumber clone = jenny.clone();
		System.out.println(m.get(clone)); // jenny
		
		System.out.println(clone != jenny); // true
		System.out.println(clone.getClass() == jenny.getClass()); // true
		System.out.println(clone.equals(jenny)); // true
		
		
	} // end of main
	
}// end of class

 

 

가변 객체의 clone 메서드 구현 방법

  • Cloneable 인터페이스를 구현한다.
  • 접근제어자를 public, 반환 타입은 자신의 클래스로 변경한다.
  • super.clone을 호출한 뒤 필요한 필드를 적절히 수정한다.
    • 배열을 복제할 때는 배열의 clone 메서드를 사용하라
    • 경우에 따라 final을 사용할 수 없을지도 모른다.
    • 필요한 경우 deep copy를 해야한다.
    • super.clone으로 객체를 만든 뒤, 고수준 메서드를 호출하는 방법도 있다.
    • 오버라이딩 할 수 있는 메서드는 참조하지 않도록 조심해야 한다.
    • 상속용 클래스는 Cloneable을 구현하지 않는것이 좋다.
    • Cloneable을 구현한 스레드 안전 클래스를 작성할 떄는 동기화를 해야 한다.

 

가변 객체 상태 값중 배열이 있을 경우 원본, 복사본 모두 같은 배열을 참조하고 있어서 어느 한 곳에서 값을 변경하면 다른 곳에서도 영향을 받는다.

 

package item13;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack implements Cloneable {
	
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY  = 16;
	
	// 클래스 생성하면 elements 배열에 16개의 방이 초기화
	public Stack() {
		elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}
	
	// elements에 인자값 넣기 후치 증감 연산자로 0부터 차례로 들어간다.
	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}
	
	// elements 배열에 참조된 값 가져오기 전치 감소 연산자로 늘어난 size값을 -1하여 마지막 배열 값부터 가져온다.
	// 값을 가져오는 동시에 해당 배열의 방을 참조 해제해준다.
	public Object pop() {
		if(size == 0) {
			throw new EmptyStackException();
		}
		
		Object result = elements[--size];
		elements[size] = null; // 참조 해제
		return result;
	}
	
	// 기본 16개 배열의 방이 꽉 차면 방을 2배로 늘려준다.
	public void ensureCapacity() {
		if(elements.length == size) {
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
	}
	
	public boolean isEmpty() { return size == 0; }
		
	@Override
	public Stack clone() {
		try {
			Stack result = (Stack) super.clone();
			// 복사할 떄 elements도 같이 복사
			result.elements = elements.clone();
			return result;
		} catch (CloneNotSupportedException e) {
			throw new AssertionError();
		}
	}

	public static void main(String[] args) {		
		
		Object[] values = new Object[2];
		values[0] = new PhoneNumber(123, 456, 7890);
		values[1] = new PhoneNumber(321, 654, 987);
		
		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() + " ");
		}
		
		// 원본인 stack에서 복사할 때 Primitive Type인 size는 원본에서 변경이 되더라도 복사본에 영향이 가지 않아서
		// while문이 2번 돌지만 이미 원본에서 배열을 pop 해서 내용을 찾아볼 수 없다.
		System.out.println("pop of copy");
		while (!copy.isEmpty()) {
			System.out.println(copy.pop() + " ");
		}
		
		System.out.println(stack.elements[0] ==copy.elements[0]);

	}// end of main	

}// end of class

 

 

clone 메서드 내부에 elements 복사하는 내용 주석을 해제하면 

내용이 같은 elements를 복사하여 복사본에 해당 참조값을 넣어주면 각각 알맞게 동작한다.

 

 

이렇게 Reference Type을 참조하고 있는 객체를 복사 할 때 단순히 해당 객체만 복사하는 경우를 shallow copy라고 하고

객체 내부에 모든 Reference Type의 상태값까지 복사하는 경우를 deep copy 라고 한다.

PhoneNumber 클래스는 불변객체니 그럴일이 없겠지만 가변객체라면 한곳에서 값을 변경하면 다른 곳에서도 값이 변경된다. 어떻게 보면 동기화된다고 볼 수 있겠다.

 

package item13;

import java.util.Iterator;

public class HashTable implements Cloneable {
	
	// HashTable의 버킷
	private Entry[] buckets = new Entry[10];
	
	// 버킷 내부의 LinkedList
	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);
		}
		
	}
	
	@Override
	public HashTable clone() {
		HashTable result = null;
		
		try {
			result = (HashTable) super.clone();
			// bukets만 복사할 뿐 내부에 LinkedList Entry는 여전히 원본, 복사본 똑같다.
			result.buckets = this.buckets.clone(); // shallow 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]);
		
	} // end of main

} // end of class

HashTable 객체를 복사 할 때 HashTable과 bucket은 복사가 되어 서로 다른 인스턴스 이지만

bucket안에 들어있는 객체들은 여전히 같은 참조값을 가지고 있어 서로 영향을 받는다 shallow copy가 아닌 deep copy로 bucket 안에 객체들도 복사를 해서 각자 다른 참조값을 가지게 해야 진정한 복사라고 할 수 있다.

지금은 그냥 심볼릭 링크된 상황

 

package item13;

import java.util.Iterator;

public class HashTable implements Cloneable {
	
	// HashTable의 버킷
	private Entry[] buckets = new Entry[10];
	
	// 버킷 내부의 LinkedList
	private static class Entry implements Cloneable {
		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);
		}
		
		// 재귀적임 함수 동작은 제대로 되지만 연결된 배열이 많으면 StackOverFlow가 날 수 있다.
		public Entry deepCopy() {
			return new Entry(key, value, next == null ? null : next.deepCopy());
		}
				
	}
	
	@Override
	public HashTable clone() {
		HashTable result = null;
		
		try {
			result = (HashTable) super.clone();
			// 원본 HashTable과 같은 크기의 버킷을 새로 만들고
            // 내용이 있으면 복사 버킷에 원본 버킷에 있는 Entry 객체를 복사
			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(); // 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); // true
		System.out.println(hashTable.buckets[0] == clone.buckets[0]); // false
		
	} // end of main

} // end of class

버킷 안에 들어간 배열까지 하나씩 돌면서 복사를 해서 넣어주는 작업을 한다.

 

deepCopy의 재귀적인 함수를 사용하지 않고 복사하려면

// StackOverFlow에 안전한 방법
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;
}

 

이 방법을 써도 되긴 하나 그냥 다시 clone 메서드 오버라이딩 해주면 되는게 아닐까..

 

package item13;

import java.util.Iterator;

public class HashTable implements Cloneable {
	
	// HashTable의 버킷
	private Entry[] buckets = new Entry[10];
	
	// 버킷 내부의 LinkedList
	private static class Entry implements Cloneable {
		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);
		}
		
		// 재귀적임 함수 동작은 제대로 되지만 연결된 배열이 많으면 StackOverFlow가 날 수 있다.
//		public Entry deepCopy() {
//			return new Entry(key, value, next == null ? null : next.deepCopy());
//		}

		// StackOverFlow에 안전한 방법
//		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;
//		}
		
		// deepCopy 메서드 말고 clone을 다시 사용하면 안될까?
		@Override
		public Entry clone() {
			try {
				return (Entry) super.clone();
			} catch (CloneNotSupportedException e) {
				throw new AssertionError();
			}
		}
				
	}
	
//	@Override
//	public HashTable clone() {
//		HashTable result = null;
//		
//		try {
//			result = (HashTable) super.clone();
//			// bukets만 복사할 뿐 내부에 LinkedList Entry는 여전히 원본, 복사본 똑같다.
//			result.buckets = this.buckets.clone(); // shallow copy
//			return result;
//		} catch (CloneNotSupportedException e) {
//			throw new AssertionError();
//		}
//		
//	}
	
	@Override
	public HashTable clone() {
		HashTable result = null;
		
		try {
			result = (HashTable) super.clone();
			// 원본 HashTable과 같은 크기의 버킷을 새로 만들고
            // 내용이 있으면 복사 버킷에 원본 버킷에 있는 Entry 객체를 복사
			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(); // deep copy
					result.buckets[i] = this.buckets[i].clone(); // clone
				}
			}
			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); // true
		System.out.println(hashTable.buckets[0] == clone.buckets[0]); // false
		 
	} // end of main

} // end of class

 

고수준의 메서드를 사용하는건 clone메서드 내부에서 super.clone 으로 복사한 뒤

상세한 상태값 설정을 put()이나 get() 메서드로 세팅을 할 수도 있다.

 

주의할 점은 clone 메서드 내부에서 하위클래스에서 재정의 할 수 있는 메서드를 사용하지 말 것

하위 클래스에서 메서드를 다른 방식으로 재정의 하고 clone 메서드를 사용했을 때 부모 clone() 메서드 내부에서 사용되는 재정의 메서드가 부모의 것을 따르지 않고 자식의 것을 따르기때문에 다르게 동작할 수있기 때문

 

상속구조를 지원하는 클래스에서는 Cloneable 선언하지 말것

 

Clone 메서드가 멀티스레드 환경에 안전한 메서드로 만들려면 syncronized 키워드를 사용해라

 

 

 

현실적으로 복잡한 Cloneable 을 사용하기 보다 생성자를 사용하여 객체를 복사하는 방법이 더 좋다.

생성자를 이용한 복사

public class HashSetExample {

	public static void main(String[] args) {

		Set<String> hashSet = new HashSet<>();
		hashSet.add("doyoun");
		hashSet.add("whiteship");
		System.out.println("HashSet : " + hashSet);
		
		// Collection을 받는 생성자로 copy
		Set<String> treeSet = new TreeSet<>(hashSet);
		
		System.out.println("TreeSet : " + treeSet);
		
	}

}

 

 

 

마무리

Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야하지만 새로운 인터페이스, 클래스를 만들 대 절대 Cloneable을 확장하지 말고 복제 기능은 생성자와 팩터리를 사용하는 것이 맞다.

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!