본문 바로가기

Dev Book Review/Effective Java

[Effective Java] item13. clone 재정의는 주의해서 진행하라

1. Cloneable interface

cloneable : 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이다.

믹스인 : 클래스가 자신의 "본래 타입"에 추가하여 구현할 수 있는 타입. 선택 가능한 기능을 제공하며, 그 기능을 제공받고자 하는 클래스에서 선언한다

clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고 그마저도 protected이다.

즉, Cloneable을 구현하는 것만으로 clone 메서드를 호출 할 수 없다.

그러나 문제점에도 불구하고 Cloneable 방식이 널리쓰이고 잇어서 잘 알아두는게 좋다

 

Cloneable : Object의 protected 메서드인 clone의 동작방식을 결정한다.
상위 클래스(Object)에 정의된 protected 메서드의 동작방식을 변경한 것 : 이례적

  • clone 호출 + Cloneable 구현 O : 객체의 필드들을 하나하나 복사한 객체 반환
  • clone 호출 + Cloneable 구현 X : CloneNotSupportedException

 

2. clone 메서드의 일반 규약

ㄱ. 클론객체와 원본객체의 Identificatinon 값은 다르다.

x.clone() != x

x.clone().getClass() == x.getClass()

 

ㄴ. 클론객체와 원본객체의 logical equation은 같으며 클론 객체와 원본객체의 클래스가 같다.

x.clone().equals(x)

x.clone().getClass() == x.getClass()

 

3. clone 메서드의 사용

상속에서의 주의점

생성자 연쇄(constructor chaining)과 비슷하다 : 상속으로 부모 클래스의 default 생성자를 계속 호출한다. > 마찬가지로 clone에서도 상속으로 연쇄해서 객체를 생성해야한다.

clone 메서드가 super.clone이 아닌 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않을 것이다.

clone을 재정의한 클래스가 final이면 하위 클래스가 없으니 관례를 무시해도 안전하다.

 

상위 클래스를 제대로 상속했을 때 Cloneable 구현

super.clone을 호출한다. (클래스에 정의한 모든 필드가 원본과 같다.)

불변 클래스에서는 굳이 clone 메서드를 제공하지 않는게 좋다 (기존의 super.clone으로 해결)

@Override public PhoneNumber clone(){
  try{
    return (PhoneNumber) super.clone();
  }catch(CloneNotSupportedException e){
    throw new AssertionError();	// 막기위해선 implements Cloneable
  }
}

Object의 clone은 Object를 반환하지만 PhoneNumber에서는 PhoneNumber를 반환하게 하였다.재정의한 메서드의 반환타입은 상위클래스 메서드가 반환하는 타입(Object)의 하위 타입(PhoneNumber)일 수 있다.

자바의 공변 반환 타이핑 (convariant return typing) 지원 덕분이다.

공변 반환 타이핑 : T'가 T의 subType이면, C<T'>는 C<T>의 SubType이다.

 

가변 객체에서 사용할 경우

public class Stack{
  private Object[] elements;
	private int size;
}

@Override public Stack clone(){
  try{
		Stack result = (Stack)super.clone();
    result.elements = elements.clone();
    return result
  }catch (CloneNotSupportedException e){
    throw new AssertionError():
  }
}

이렇게 복사를 하지 않는다면 복사한 객체는 elements의 참조값을 갖고있어, 원본객체를 수정하면 복사객체도 수정되는 현상이 일어날 수 있다.

clone 메서드는 사실상 생성자와 같은 효과를 낸다.
clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.

 

배열의 복제

배열의 clone 메서드를 사용하자!
배열은 clone 기능을 제대로 사용하는 유일한 예이다.

 

HashTable에서의 복제 (복잡한 가변 객체의 복제)

해시테이블 내부 : 버킷들의 배열 + 각 버킷을 키-값 쌍을 담는 연결리스트의 첫 번째 엔트리 참조

1) 배열복제 -> 버킷배열을 복제하나, 버킷 배열안에잇는 연결리스트들은 모두 원본과 같은 것들이다.

2) 각 버킷을 구성하는 연결 리스트를 복사해야한다.

3-1) 연결 리스트를 복제할 때 재귀적으로 호출하는 방식을 사용한다

    스택프레임을 소비하므로 스택 오버플로를 낼 가능성이 있다.

3-2) 재귀 호출대신 반복자를 써서 순회한다.

3-3) 원본 객체의 상태를 다시 생성하는 고수준 메서드를 호출한다. put(key,value)

    저수준 API 보다는 속도가 느리다. + Cloneable 아키텍처의 기초인 필드 단위 객체 복사를 우회한다.

 

4. clone()의 상속에서의 주의점

1) Clonealbe 구현 여부를 하위 클래스에서 선택하도록 해준다.

2) clone을 동작하지 않게 구현해놓고 하위클래스에서 재정의 못하게 한다.

@Override
protected final Object clone() throws CloneNotSupportedException{
  throw new CloneNotSupportedException();
}

 

5. clone() 재정의 방법

  • Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.
  • 접근 제한자는 public으로
  • 반환타입은 클래스 자신으로
  • 가장 먼저 super.clone을 호출 한 후 필요한 필드를 적절히 수정한다
  • 이후 깊은 구조까지 클론한다. (보통 재귀이지만, 항상 정답은 아니다.)
  • 기본 타입 필드, 불변 객체 참조만 갖는 클래스면 아무 필드도 수정할 필요가 없다.
  • 고유 ID는 비록 기본타입, 불변이어도 수정해야한다.

 

6. clone() 재정의 대체법

ㄱ. 복사 생성자 : 단수히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자

public Yum(Yum yum){...};

ㄴ. 복사 팩터리 생성

public static Yum newInstance(Yum yum){...};

ㄷ. 복사 생성자, 팩터리의 장점

  • 위험한 객체 생성 매커니즘을 사용하지 않는다. (생성자 없이 객체 생성)
  • 문서화된 규약에 기대지 않는다
  • final 필드 용법과 충돌하지 않음
  • 불필요한 검사 예외를 던지지 않음
  • 형변환 필요가 없다.
  • 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다.

    인터페이스 기반 복사 생성자 = 변환 생성자 (conversion constructor)
    인터페이스 기반 복사 팩터리 = 변환 팩터리 (conversion factory)