Java/정석

clone

hs_developer 2022. 6. 10. 00:50

clone()

자신을 복제해 새로운 인스턴스를 생성하는 일을 한다. 어떤 인스턴스에 대해 작업을 할 때, 원래의 인스턴스는 보존하고 clone 메서드를 이용해 새로운 인스턴스를 생성하여 작업을 하면 작업 이전의 값이 보존되므로 작업에 실패해 원래의 상태로 되돌리거나 변경되기 전의 값을 참고하는 데 도움이 된다.

 

Object 클래스에 정의된 clone()은 단순히 인스턴스 변수 값만 복사하기 때문에 참조 타입의 인스턴스 변수가 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다.

 

배열의 경우, 복제된 인스턴스 변수의 값만 복사하기 때문에 복제된 인스턴스 작업이 원래의 인스턴스에 영향을 미치게 된다. 이런 경우 clone 메서드를 오버라이딩해서 새로운 배열을 생성하고 배열의 내용을 복사한다.

 

x=3, y=5
x=3, y=5
class Point implements Cloneable // 1. Cloneable 인터페이스 구현
{
	int x, y;
	
	Point(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
	
	public String toString() // 출력하는 형태
	{
		return "x=" + x + ", y=" + y;
	}
	
	// clone 메서드 내 생성
	// 2. clone()을 오버라이딩 하면서 접근 제어자를 protected에서 public으로 변경
	public Object clone()
	{
		Object obj = null;
		
		try
		{
			obj = super.clone(); // 3. 조상클래스의 clone 호출
		} catch(CloneNotSupportedException e) {}
		
		return obj;
	}
}

public class CloneEX1 {

	public static void main(String[] args) {
		
		Point original = new Point(3, 5);
		Point copy = (Point)original.clone();
		
		System.out.println(original);
		System.out.println(copy);
	}
}

 

clone()을 사용하려면,

 

1. 복제할 클래스가 Cloneable 인터페이스를 구현하고,

 

2. clone()을 오버라이딩 하면서 접근 제어자를 protected에서 public으로 변경한다. 그래야만 상속 관계가 없는 다른 클래스에서 clone()을 호출할 수 있다.

 

3.조상클래스의 clone()을 호출하는 코드가 포함된 try-catch문을 작성한다.

 

 

 

공변 반환 타입

오버라이딩 할 때 조상 메서드의 반환 타입을 자손 클래스의 타입으로 변경을 허용하는 것

 

public Point clone() // 1. 반환 타입을 Object에서 Point로 변경
	{
		Object obj = null;
		
		try
		{
			obj = super.clone();
		} catch(CloneNotSupportedException e) {}
		
		return (Point)obj; // 2. Point 타입으로 형변환한다.
	}

 

공변 반환 타입을 이용하면, 조상의 타입이 아닌 실제로 반환되는 자손 객체의 타입으로 반환 할 수 있어서 번거로운 형 변환이 줄어든다는 장점이 있다.

Point copy = (Point)original.clone(); -> Point copy = original.clone()

 

 

공변 반환타입 예제

 

[1, 2, 3, 4, 5]
[6, 2, 3, 4, 5]
import java.util.Arrays;

public class CloneEX2 {

	public static void main(String[] args) {
		
		int[] arr = {1, 2, 3, 4, 5};
		int[] arrClone = arr.clone(); // 배열 arr clone해서 같은 내용의 새 배열 생성
		arrClone[0] = 6;
		
		System.out.println(Arrays.toString(arr));
		System.out.println(Arrays.toString(arrClone));
	}
}

배열도 객체이기 때문에 Object 클래스의 멤버들을 모두 상속받는다. Object 클래스에는 protected로 정의되어 있는 clone()을 배열에서는 public으로 오버라이딩 했기 때문에 예제처럼 직접 호출이 가능하다. 그리고 원본과 같은 타입을 반환하므로 형 변환이 필요 없다.

 

 

일반적으로 배열을 복사할 때, 같은 길이의 새로운 배열을 생성한 다음에 System.arraycopy()를 이용해 내용을 복사하지만, clone()으로도 간단하게 복사 할 수 있다.

int[] arr = {1, 2, 3, 4, 5};
int[] arrClone = arr.clone();
int[] arr = {1, 2, 3, 4, 5};
int[] arrClone = new int[arr.length]; // 배열을 생성하고
System.arraycopy(arr, 0, arrClone, 0, arr.length); // 내용을 복사한다

 

 

 

얕은 복사와 깊은 복사

 

clone()은 단순히 객체 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않는다. 기본형 배열인 경우는 문제가 없지만, 객체 배열을 clone()으로 복제하는 경우에는 원본과 복제본이 같은 객체를 공유하므로 완전한 복제라고 볼 수 없다.

 

이러한 복제를 '얕은 복사'라고 한다. 얕은 복사에서는 원본을 변경하면 복사본도 영향을 받는다.

 

반면, 원본이 참조하고 있는 객체까지 복제하는 것을 '깊은 복사'라고 하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다. 

 

 

예를 들어 Circle 클래스가 아래와 같이 Point 타입의 참조변수를 포함하고, clone()은 단순히 Object 클래스의 clone()을 호출하도록 정의 되어 있을 때,      

class Circle implements Cloneable
{
	// 변수 선언
	Point p; // 원점
	double r; // 반지름
	
	// 생성자
	Circle(Point p, double r)
	{
		this.p = p;
		this.r = r;
	}
	
	// clone 메서드
	public Circle clone() // 얕은 복사
	{
		Object obj = null;
		
		try
		{
			obj = super.clone(); // 조상인 Object의 clone() 호출
		} catch(CloneNotSupportedException e) {}
		
		return (Circle)obj;
	}
}

 

Circle 인스턴스 c1을 생성하고, clone()으로 복제해서 c2를 생성하면, c1과 c2는 같은 Point 인스턴스를 가리키게 되므로 완전한 복제라 할 수 없다.

Circle c1 = new Circle(new Point(1, 1), 2.0);
Circle c2 = c1.clone();  // 얕은 복사

 

 

c1과 c2가 각각의 Point 인스턴스를 가리키도록 하는 예제

 

c1=[p: x=1, y=1, r=2.0]
c2=[p: x=1, y=1, r=2.0]
c3=[p: x=1, y=1, r=2.0]

= c1의 변경 후 =
c1=[p: x=9, y=9, r=2.0]
c2=[p: x=9, y=9, r=2.0]
c3=[p: x=1, y=1, r=2.0]
class Circle implements Cloneable
{
	// 변수 선언
	Point p; // 원점
	double r; // 반지름
	
	// 생성자
	Circle(Point p, double r)
	{
		this.p = p;
		this.r = r;
	}
	
	// 얕은 복사
	public Circle shallowCopy() 
	{
		Object obj = null;
		
		try
		{
			obj = super.clone(); // 조상인 Object의 clone() 호출
		} catch(CloneNotSupportedException e) {}
		
		return (Circle)obj;
	}
	
	// 깊은 복사
	public Circle deepCopy()
	{
		Object obj = null;
		
		try
		{
			obj = super.clone();
		} catch(CloneNotSupportedException e) {}
		
		Circle c = (Circle)obj;
		c.p = new Point(this.p.x, this.p.y);
		
		return c;
		
	}
	
	public String toString()
	{
		return "[p: " + p + ", r=" + r + "]";
	}
}

class Point
{
	int x, y;
	
	Point(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
	
	public String toString()
	{
		return "(" + x + ", " + y + ")";
	}

}

public class CloneEX3 {

	public static void main(String[] args) {
		
		Circle c1 = new Circle(new Point(1, 1), 2.0);
		Circle c2 = c1.shallowCopy();
		Circle c3 = c1.deepCopy();
		
		System.out.println("c1=" + c1);
		System.out.println("c2=" + c2);
		System.out.println("c3=" + c3);
		
		c1.p.x = 9;
		c1.p.y = 9;
		
		System.out.println("= c1의 변경 후 =");
		System.out.println("c1=" + c1);
		System.out.println("c2=" + c2);
		System.out.println("c3=" + c3);
		
		
	}
}

 

1. 인스턴스 c1을 생성한 후, 얕은 복사로 c2를 생성하고, 깊은 복사로 c3를 생성한다.

Circle c1 = new Circle(new Point(1, 1), 2.0);
Circle c2 = c1.shallowCopy();
Circle c3 = c1.deepCopy();

 

 

2. c1이 가리키고 있는 Point 인스턴스의 x와 y의 값을 9로 변경한다.

c1.p.x = 9;
c1.p.y = 9;

 

 

Object의 클래스의 clone()은 원본 객체가 가지고 있는 값만 복사하는 얇은 복사인 반면에, 

 

Circle c = (Circle)obj;
c.p = new Point(this.p.x, this.p.y);

deepCopy()는 shallowCopy()에 위의 두 줄을 추가하여, 복제된 객체가 새로운 Point인 인스턴스를 참조하도록 했다. 원본이 참조하고 있는 객체까지 복사한 것이다.

'Java > 정석' 카테고리의 다른 글

인터페이스  (0) 2022.06.03
추상 클래스  (0) 2022.06.02
다형성  (0) 2022.06.01
오버라이딩  (0) 2022.06.01
static, instance 메소드  (0) 2022.05.29