clone () 대 복사 생성자 대 팩토리 메서드?


81

Java에서 clone () 구현에 대한 빠른 Google을 수행 한 결과 http://www.javapractices.com/topic/TopicAction.do?Id=71

다음과 같은 주석이 있습니다.

복사 생성자와 정적 팩토리 메소드는 복제에 대한 대안을 제공하며 구현하기가 훨씬 쉽습니다.

내가 원하는 것은 깊은 사본을 만드는 것뿐입니다. clone ()을 구현하는 것은 많은 의미가있는 것 같지만, 구글 순위가 높은이 기사는 저를 조금 두렵게 만듭니다.

내가 발견 한 문제는 다음과 같습니다.

복사 생성자는 Generics에서 작동하지 않습니다.

다음은 컴파일되지 않는 의사 코드입니다.

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

샘플 1 : 제네릭 클래스에서 복사 생성자 사용.

팩토리 메서드에는 표준 이름이 없습니다.

재사용 가능한 코드를위한 인터페이스를 갖는 것은 매우 좋습니다.

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

샘플 2 : 제네릭 클래스에서 clone () 사용.

복제가 정적 메서드가 아니라는 것을 알았지 만 보호 된 모든 필드의 전체 복사본을 만들 필요는 없습니까? clone ()을 구현할 때 복제 불가능한 하위 클래스에서 예외를 던지는 추가 노력은 나에게 사소한 것 같습니다.

내가 뭔가를 놓치고 있습니까? 모든 통찰력을 주시면 감사하겠습니다.


답변:


52

기본적으로 클론이 손상되었습니다 . 제네릭으로 쉽게 작동하는 것은 없습니다. 다음과 같은 것이있는 경우 (요점을 파악하기 위해 단축) :

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

그런 다음 컴파일러 경고로 작업을 완료 할 수 있습니다. 제네릭은 런타임에 지워지기 때문에 복사를 수행하는 작업에는 캐스트를 생성하는 컴파일러 경고가 표시됩니다. 이 경우 피할 수 없습니다. . 어떤 경우에는 피할 수 있지만 (감사합니다, kb304) 전부는 아닙니다. 인터페이스를 구현하는 하위 클래스 또는 알 수없는 클래스를 지원해야하는 경우를 고려하십시오 (예 : 동일한 클래스를 생성하지 않아도되는 복사 가능 컬렉션을 반복하는 경우).


2
컴파일러 경고 없이도 가능합니다 .
akarnokd

@ kd304, 컴파일러 경고를 억제한다는 의미라면 사실이지만 실제로 다르지는 않습니다. 경고의 필요성을 완전히 피할 수 있다는 의미라면 자세히 설명하십시오.
Yishai

@Yishai : 내 대답을보세요. 그리고 나는 억제 경고를 언급 한 것이 아닙니다.
akarnokd

kd304 Copyable<T>의 답변에서 사용하는 것이 훨씬 더 합리적 이기 때문에 Downvoting.
Simon Forsberg

25

빌더 패턴도 있습니다. 자세한 내용은 효과적인 Java를 참조하십시오.

나는 당신의 평가를 이해하지 못합니다. 복사 생성자에서 유형을 완전히 알고 있는데 왜 제네릭을 사용해야합니까?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

비슷한 질문이 최근에 있었다 여기 .

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

실행 가능한 샘플 :

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}

1
+1 이것은 복사 생성자를 구현하는 가장 간단하지만 효과적인 방법입니다
dfa 2009-07-09

위의 MyClass는 일반적인 StackOverflow가 <T>를 삼켰습니다.
User1

질문 형식을 수정했습니다. 프리 + 코드 태그 대신 각 줄에 4 개의 공백을 사용하십시오.
akarnokd

나는 G의 복사 방법에 오류가 - "슈퍼 클래스 메서드 오버라이드 (override) 할 필요가 있습니다"
칼 프리 쳇 (Pritchett)

3
(나는 이것이 오래되었다는 것을 알고 있지만 후손을 위해) @CarlPritchett :이 오류는 Java 1.5 이하에서 표시됩니다. 인터페이스 메소드를 @Override로 표시하는 것은 Java 1.6부터 허용됩니다.
Carrotman42

10

Java에는 C ++와 같은 의미의 복사 생성자가 없습니다.

인수와 동일한 유형의 객체를 취하는 생성자를 가질 수 있지만이를 지원하는 클래스는 거의 없습니다. (클론을 지원하는 수 미만)

일반 복제의 경우 클래스의 새 인스턴스를 만들고 반사를 사용하여 원본 (얕은 복사본)에서 필드를 복사하는 도우미 메서드가 있습니다 (실제로는 반사와 비슷하지만 더 빠름).

전체 복사의 경우 간단한 방법은 개체를 직렬화하고 역 직렬화하는 것입니다.

BTW : 내 제안은 변경 불가능한 객체를 사용하는 것입니다. 그러면 복제 할 필요가 없습니다. ;)


변경 불가능한 객체를 사용하면 복제가 필요하지 않은 이유를 설명해 주시겠습니까? 내 응용 프로그램에서 기존 개체와 정확히 동일한 데이터를 가진 다른 개체가 필요합니다. 그래서 어떻게 든 복사해야합니다
Zhenya

2
@Ievgen 같은 데이터에 대한 두 개의 참조가 필요한 경우 참조를 복사 할 수 있습니다. 당신은이 변경 될 경우 개체의 내용을 복사해야합니다 (그러나 불변의 객체를 위해 당신은하지 않습니다 알고)
피터 Lawrey

아마도 나는 불변 객체의 이점을 이해하지 못할 것입니다. JPA / 최대 절전 엔터티가 있고 기존 엔터티를 기반으로 다른 JPA 엔터티를 만들어야하지만 새 엔터티의 ID를 변경해야한다고 가정합니다. 불변 객체로 어떻게할까요?
Zhenya

@Ievgen은 정의에 따라 변경할 수없는 개체를 변경할 수 없습니다. String예를 들어 불변 객체이며 복사하지 않고도 String을 전달할 수 있습니다.
Peter Lawrey

6

Yishai 답변이 개선되어 다음 코드로 경고가 표시되지 않을 수 있다고 생각합니다.

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

이렇게하면 Copyable 인터페이스를 구현해야하는 클래스는 다음과 같아야합니다.

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}

2
거의. Copyable 인터페이스는 다음과 같이 선언해야합니다. interface Copyable<T extends Copyable>이는 Java로 인코딩 할 수있는 자체 유형에 가장 가까운 것입니다.
Recurse

물론 당신은 의미 interface Copyable<T extends Copyable<T>>했죠? )
Adowrath

3

다음은 많은 개발자가 사용하지 않는 몇 가지 단점입니다. Object.clone()

  1. Object.clone()메소드를 사용 하려면 Cloneable인터페이스 구현 , clone()메소드 및 핸들 정의 CloneNotSupportedException, 마지막으로 Object.clone()객체 호출 및 캐스팅 과 같은 코드에 많은 구문을 추가해야 합니다.
  2. Cloneable인터페이스에는 clone()메소드 가없고 마커 인터페이스이며 그 안에 메소드가 없습니다. 여전히 JVM clone()에 객체에서 수행 할 수 있음을 알리기 위해 구현해야 합니다.
  3. Object.clone()그래서 우리는 우리 자신의 clone()간접적 인 호출 Object.clone()을 제공해야합니다.
  4. 생성자를 Object.clone()호출하지 않기 때문에 객체 생성을 제어 할 수 없습니다 .
  5. clone()Person를 들어 자식 클래스에 메서드를 작성 하는 경우 모든 수퍼 클래스가 clone()메서드를 정의 하거나 다른 부모 클래스에서 상속해야합니다 super.clone(). 그렇지 않으면 체인이 실패합니다.
  6. Object.clone()얕은 복사 만 지원하므로 새로 복제 된 객체의 참조 필드는 원래 객체의 필드가 보유한 객체를 계속 보유합니다. 이를 극복하기 위해서는 clone()우리 클래스가 보유하고있는 모든 클래스에서 구현 한 다음 clone()아래 예제와 같이 메서드 에서 별도로 복제해야합니다 .
  7. Object.clone()최종 필드는 생성자를 통해서만 변경할 수 있으므로 최종 필드를 조작 할 수 없습니다 . 우리의 경우 모든 Person객체가 id로 고유하도록 하려면 생성자를 호출하지 않고 최종 필드를에서 수정할 수 Object.clone()없기 때문에 사용하면 중복 객체를 얻게 됩니다 .Object.clone()idPerson.clone()

복사 생성자는 더 나은보다 Object.clone()그들이 때문에

  1. 인터페이스를 구현하거나 예외를 던지도록 강요하지 마십시오. 그러나 필요한 경우 확실히 할 수 있습니다.
  2. 캐스트가 필요하지 않습니다.
  3. 알려지지 않은 개체 생성 메커니즘에 의존 할 필요가 없습니다.
  4. 계약을 따르거나 어떤 것을 구현하기 위해 부모 클래스가 필요하지 않습니다.
  5. 최종 필드를 수정할 수 있습니다.
  6. 객체 생성을 완벽하게 제어 할 수 있도록 초기화 로직을 작성할 수 있습니다.

Java Cloning-Copy Constructor와 Cloning 에 대해 자세히 알아보기


1

일반적으로 clone ()은 보호 된 복사 생성자와 함께 작동합니다. 이는 생성자와 달리 clone ()이 가상 일 수 있기 때문에 수행됩니다.

Derived from a super class Base에 대한 클래스 본문에는

class Derived extends Base {
}

따라서 가장 단순하게 clone ()을 사용하여 가상 복사 생성자를 여기에 추가합니다. (C ++에서 Joshi는 가상 복사 생성자로 clone을 권장합니다.)

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

권장되는대로 super.clone ()을 호출하고 이러한 멤버를 클래스에 추가해야하는 경우 더 복잡해집니다.

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

자, 처형하면

Base base = (Base) new Derived("name");

그리고 당신은

Base clone = (Base) base.clone();

이것은 Derived 클래스 (위의 것)에서 clone ()을 호출합니다. 이것은 super.clone ()을 호출합니다-구현 될 수도 있고 구현되지 않을 수도 있지만, 호출하는 것이 좋습니다. 그런 다음 구현은 super.clone ()의 출력을 Base를 사용하는 보호 된 복사 생성자에 전달하고 최종 멤버를 여기에 전달합니다.

그런 다음 해당 복사 생성자는 수퍼 클래스의 복사 생성자를 호출하고 (있는 경우) 최종 결과를 설정합니다.

clone () 메서드로 돌아 오면 최종 멤버가 아닌 모든 멤버를 설정합니다.

예리한 독자는 Base에 복사 생성자가있는 경우 super.clone ()에 의해 호출되고 보호 된 생성자에서 슈퍼 생성자를 호출 할 때 다시 호출되므로 다음을 호출 할 수 있습니다. 슈퍼 복사 생성자 두 번. 리소스를 잠그는 경우이를 알 수 있기를 바랍니다.


0

당신에게 맞는 한 가지 패턴은 빈 레벨 복사입니다. 기본적으로 인수가없는 생성자를 사용하고 다양한 setter를 호출하여 데이터를 제공합니다. 다양한 bean 속성 라이브러리를 사용하여 속성을 비교적 쉽게 설정할 수도 있습니다. 이것은 clone ()을하는 것과 같지는 않지만 많은 실용적인 목적을 위해 괜찮습니다.


0

Cloneable 인터페이스는 쓸모가 없지만 복제가 잘 작동하고 8 개 필드 이상의 큰 개체에 대해 더 나은 성능을 제공 할 수 있다는 점에서 손상되었지만 탈출 분석에 실패합니다. 대부분의 경우 복사 생성자를 사용하는 것이 좋습니다. 길이가 동일하게 보장되기 때문에 어레이에서 복제를 사용하는 것이 Arrays.copyOf보다 빠릅니다.

자세한 내용은 여기 https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html


0

의 모든 단점을 100 % 인식하지 못한다면 clone()멀리있는 것이 좋습니다. 나는 그것이 clone()깨 졌다고 말하지 않을 것입니다 . 나는 이것이 당신의 최선의 선택이라고 확신 할 때만 사용하라. 복사 생성자 (또는 팩토리 메서드, 내 생각에는 중요하지 않음)는 작성하기 쉽고 (길지만 쉬울 수 있음) 복사 할 내용 만 복사하고 원하는 내용을 복사하는 방식으로 복사합니다. 정확한 필요에 맞게 다듬을 수 있습니다.

또한 복사 생성자 / 팩토리 메소드를 호출 할 때 발생하는 상황을 디버그하기 쉽습니다.

그리고 clone()참조 (예 :에 대한 Collection) 만 복사 되지 않는다는 것을 의미한다고 가정 할 때 개체의 "깊은"사본을 즉시 생성하지 않습니다 . 그러나 딥 및 얕은에 대한 자세한 내용은 여기에서 읽어보십시오 : 딥 복사, 얕은 복사, 복제


-2

당신이 놓친 것은 복제가 기본 및 관례에 따라 얕은 사본을 생성하고 일반적으로 딥 사본을 생성하는 것이 불가능하다는 것입니다.

문제는 방문한 객체를 추적 할 수 없으면 순환 객체 그래프의 깊은 복사본을 실제로 만들 수 없다는 것입니다. clone ()은 이러한 추적을 제공하지 않으므로 (.clone ()에 대한 매개 변수 여야하므로) 얕은 복사본 만 생성합니다.

자신의 개체가 모든 구성원에 대해 .clone을 호출하더라도 여전히 전체 복사본이 아닙니다.


4
임의의 개체에 대해 딥 복사 clone ()을 수행하는 것은 불가능할 수 있지만 실제로는 많은 개체 계층 구조에서 관리 할 수 ​​있습니다. 그것은 당신이 가지고있는 객체의 종류와 그들의 멤버 변수가 무엇인지에 달려 있습니다.
Mr. Shiny and New 安 宇

많은 사람들이 주장하는 것보다 모호성이 훨씬 적습니다. 복제 가능 SuperDuperList<T>하거나 그 파생물 이있는 경우 복제하면 복제 된 것과 동일한 유형의 새 인스턴스가 생성됩니다. 원본에서 분리되어야하지만 원본과 T동일한 순서 로 동일한 의를 참조해야합니다 . 그 시점부터 목록에 수행되는 작업은 다른 목록에 저장된 개체 의 ID 에 영향을주지 않아야합니다 . 다른 동작을 나타 내기 위해 일반 컬렉션을 요구하는 유용한 "컨벤션"이 없다는 것을 알고 있습니다.
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.