2017년 5월 17일 수요일

effective java 규칙 11. clone을 재정의할 때는 신중하라

객체의 복사기능을 제공 하려면 Object 의 clone method 를 재정의 해야한다.
Object 의 clone 메서드는 protected 로 제한되어 있다. 그리고 모든 class 는 object 를 상속받는다. 
따라서 모든 class 는 자기자신을 복제할 수 있다 -cloneable implements 시-. 하지만 이렇게 구현된 클래스를 사용하게되는 클라이언트 코드에 구현한 클래스의 clone 을 제공 하는 것은 좀더 까다로운 규칙이 존재한다.
여기서 제공이 의미하는것은 어떠한 class 를 구현하고 그 class 를 사용하는 클라이언트 코드에서 그 class 로 생성된 객체의 복사의 기능을 제공 한다는 의미이다.

위에서 언급 되었듯이 Object  의 clone 은 protected 이다. 따라서 클라이언트 코드에서는 (복사를 제공할) 클래스의 clone method 를 호출할 수 가 없다.

그렇기 때문에 복사기능을 제공할 클래스에선 Object 의 clone 메소드를 재정의(override) 해야 하며
재정의시 접근 제한자를 public 으로 변경해야한다. (override 시 더 넓은 허용범위로의 변경은 허용되지만 그반대는 불가)

단순히 이렇게 clone 을 override 하고 그 override 한 메소드에서 object 의 clone 메소드를 호출하게 하면
해당 객체는 CloneNotSupportedException 을 throw 한다.
그 이유는 Object 의 clone 은 native 메서드이고 이 메서드가 실행 되려면 대상 클래스가 Cloneable 이라는 믹스인 인터페이스 - 선택적 기능을 제공한다는 선언 - 로 복제를 허용한다는 사실을 알려야 한다.
다만 재정의한 class 에서 super.clone() 으로 object 의 clone 을 사용하지 않고 생성자로 객체를 생성해 복제와 유사한 효과를 내어 생성된 객체를 반환한다면 - clone 의 일반규약에는 위배되지만 - Cloneable 을 생략할 수 있다.

즉 class 에서 clone method 를 제공하려면
1. Cloneable interface 를 구현 해야하며 - mixin interface (주 기능 이외의 선택적 기능의 제공자로 선언) 로서의 역할
      - Cloneable 선언하지 않을 경우 CloneNotSupportedException 을 throw 한다.
2. Object.clone() 을 Override 해야 한다.
      - 이때 override clone method 의 접근제한자는 public 으로 변경해야만 상속구조 이외의 class (예 : client class) 에서
      clone 메소드를 사용할 수 있다 - Object.clone() 의 접근제한자는 protected 이므로 -
      - super.clone() 호출

[sample]

/**
* Created by fall1999y on 2017. 5. 12. 오전 6:26
* <pre>
* <b>Description</b>
*
* class 에서 clone method 를 제공하려면
* 1. Cloneable interface 를 구현 해야하며 - mixin interface (주 기능 이외의 선택적 기능의 제공자로 선언) 로서의 역할
* - Cloneable 선언하지 않을 경우 CloneNotSupportedException 을 throw 한다.
* 2. Object.clone() 을 Override 해야 한다.
* - 이때 override clone method 의 접근제한자는 public 으로 변경해야만 상속구조 이외의 class (예 : client class) 에서
* clone 메소드를 사용할 수 있다 - Object.clone() 의 접근제한자는 protected 이므로 -
* - super.clone() 호출
*
* Object clone 의 일반규약
* 1. x.clone != x
* 2. x.clone().getClass() == x.getClass() - 필수 조건은 아님 ( override 시 하위 클래스를 return 할 수 도 있다. )
* 3. x.clone().equals(x) - '값'의 동일성을 의미함, 필수 조건은 아님 ( 기본적으로 object 의 equals 는 객체 주소를 비교하고 재정의시 구현에 따라 참이 아니게 될 수 도 있다.)
* 4. 어떤 생성자도 호출되지 않는다. - final class 에서는 생성자를 호출해도 무방하다.
* </pre>
*/
public class CloneSample implements Cloneable {

private String var;

public CloneSample(String var) {
this.var = var;
}

/**
* Object 의 clone method 의 return type 은 Object 이지만
* override method 의 return type 이 Object 가 아닌 class 로 형변환하므로
* 클라이언트에서 형변환하는 수고를 덜 수 있다.
* - 라이브러리가 할 수 있는 일을 클라이언트에게 미루지 말아야 더 좋은 코드
*
* * override 시 접근제한자의 더 넓은 허용범위로 변경은 허용된다.
* * override 시 return type 은 하위 클래스로 변경 가능하다 (여기서는 Object -> CloneSample 로 return)
* - jdk 1.5 이후
* - 제네릭의 공변 반환형 (covariant return type)
* - override(재정의) 메서드의 반환값 자료형은 재정의되는 메서드의 반환값 자료형의 하위 클래스가 될 수 있다.
* - 아래와 같은 코드로 현재 클래스의 하위 자료형을 return 할 수 도 있다
* return (CloneSampleExt) new CloneSampleExt("a");
*
* @return
* @throws CloneNotSupportedException
*/
@Override
public CloneSample clone() throws CloneNotSupportedException {
// 재정의할 때는 반드시 super.clone 을 호출해 얻은 객체를 반환해야 원하는 클래스의 객체가 생성
// 상속구조의 모든 클래스가 super.clone 을 호출한다면 최종적으로 Object 의 clone 메서드를 호출하게 됨
return (CloneSample) super.clone();

// 아래 코드는 clone 메서드의 일반 규약 '어떤 생성자도 호출되지 않는다' 에 위배되지만 - final class 에서의 재정의에는 - 충분히 실행 가능한 코드
// 그렇지만 비 final class 에서 clone 을 재정의할 때는 반드시 super.clone 을 호출해 얻은 객체를 반환 해야해야만 이후에 상속받는 class 에서
// clone override 코드에서 super.clone 을 했을때 현재 클래스 객체가 반환될 거라는 가정을 충족 시킬수 있다.

// return (CloneSampleExt) new CloneSampleExt("a");
}

public static void main(String[] args) throws CloneNotSupportedException {
CloneSample c = new CloneSample("test");

Object c1 = c.clone();

System.out.println(c1);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("var", var)
.toString();
}
}


clone 를 override 시 지켜야 할 일반 규칙은 아래와 같다.

1. x.clone != x
2. x.clone().getClass() == x.getClass() - 필수 조건은 아님 ( override 시 하위 클래스를 return 할 수 도 있다. )
3. x.clone().equals(x) - '값'의 동일성을 의미함, 필수 조건은 아님 ( 기본적으로 object 의 equals 는 객체 주소를 비교하고 재정의시 구현에 따라 참이 아니게 될 수 도 있다.)
4. 어떤 생성자도 호출되지 않는다. - final class 에서는 생성자를 호출해도 무방하다.

만약 클래스의 모든 필드가 기본 자료형이거나 변경 불가능 객체 참조라면 위와 같이 구현하기만 하면 된다.

하지만 참조 변수가 있을때는 좀더 복잡한 과정이 필요하다.
복잡한 과정을 살펴보기전에 다음을 이해해야 한다.
"clone 메서드는 또 다른 형태의 생성자이며 이 메서드로 인해 원 객체를 손상시키면 안되고 복사본의 불변식도 제대로 만족시켜야 한다.”
effective java 에선 위와 같이 clone 을 정의 하고 있으며 위 정의를 만족하는 clone 메서드를 재정의하라고 한다.
참조변수는 reference 주소”값"이 저장되어 있고 기본자료형은 “값” 이 저장되어 있다.
object clone 은 값을 복사하는 native method 이며 참조변수가 가지고 있는 값인 주소를 복사 - Shallow Copy - 하는 object clone 이 동작하는 방식이 이치에 어긋나지 않다. 

하지만 책에서의 정의를 만족하려면 - 우리가 기대하는 복사라는 기능을 만족하려면 - 
실제 값의 복사 - deep copy - 과정이 필요하다.
따라서 배열이나 객체를 담고 있는 변수는 clone override 시 추가적인 작업이 필요하다.



private Object[] elemetns;
private List list;
private ArrayList array;

@Override
protected Object clone() throws CloneNotSupportedException {
CloneTest obj = (CloneTest) super.clone();
// 참조형 변수를 위한 추가적인 작업
obj.elemetns = elemetns.clone();
obj.array = (ArrayList) array.clone();

// List 는 interface 이기 때문에 clone 메서드가 없다. 따라서 아래와 같은 코드는 불가능하다
// obj.list = list.clone();
// 아래와 같이 해줘야 한다
obj.list = (List) ((ArrayList) list).clone();

return obj;
}

그리고 직접구현한 배열형 자료구조의 원소가 객체형일때는 각 원소도 copy 를 지원할 수 있도록 만들어야 한다. - deep copy

이렇게 clone 의 대해 알아봤다. 하지만 effective java 에선 이러한 복잡한 clone 메서드가 정말 필요한지 반문한다.
책의 결론은 차라리 객체를 복사할 대안을 제공하거나 아에 복제 기능을 제공하지 말라고 조언한다.
책에서 제시하는 clone 의 대안은
복사 생성자
public Yum(Yum copy) {
    this.some1 = copy.some1;
    ...
}
또는 복사 팩토리 메서드를 제시하고 있다.
public static Yum newInstance(Yum yum) {
    ...
}

위 방법으로 언어 외적 객체 생성수단 (native method) 에 의존하지 않으며 여러 제약 조건에 자유로워 진다.

결론은
Cloneable 을 계승하는 인터페이스는 만들지 말아야 하며, 계승 목적으로 설계하는 클래스는 Cloneable을 구현하지 말아야 한다.

 

댓글 없음:

댓글 쓰기

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor &...