clone()vsコピーコンストラクタvsファクトリメソッド?


81

私はJavaでclone()を実装することについて簡単なグーグルをしました、そして見つけました:http//www.javapractices.com/topic/TopicAction.do?Id = 71

次のコメントがあります。

コピーコンストラクターと静的ファクトリメソッドは、クローンの代替手段を提供し、実装がはるかに簡単です。

私がやりたいのは、深いコピーを作成することだけです。clone()を実装することは非常に理にかなっているようですが、このグーグルランクの高い記事は私を少し恐れさせます。

これが私が気付いた問題です:

コピーコンストラクターはジェネリックスでは機能しません。

コンパイルされない疑似コードを次に示します。

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 2009

@ kd304、コンパイラの警告を抑制することを意味する場合は、trueですが、実際には違いはありません。警告の必要性を完全に回避できることを意味する場合は、詳しく説明してください。
Yishai

@Yishai:私の答えを見てください。そして、私は抑制警告について言及していませんでした。
akarnokd 2009

kd304Copyable<T>の回答では、使用する方がはるかに理にかなっているため、反対票を投じます。
Simon Forsberg

25

Builderパターンもあります。詳細については、EffectiveJavaを参照してください。

あなたの評価がわかりません。コピーコンストラクターでは、型を完全に認識していますが、なぜジェネリックスを使用する必要があるのですか?

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

上記のMyClassは汎用であり、StackOverflowが<T>を飲み込んだことに注意してください。
user1

質問のフォーマットを修正しました。pre + codeタグの代わりに、各行に4つのスペースを使用します。
akarnokd 2009

「スーパークラスのメソッドをオーバーライドする必要があります」 -私はGのコピー方法でエラーを取得
カール・プリチェット

3
(私はこれが古いことを知っていますが、後世のために)@CarlPritchett:このエラーはJava1.5以下で表示されます。Java 1.6以降、インターフェースメソッドを@Overrideとしてマークすることが許可されています。
carrotman42 2013年

10

Javaには、C ++と同じ意味でのコピーコンストラクタはありません。

引数と同じ型のオブジェクトを受け取るコンストラクターを持つことができますが、これをサポートするクラスはほとんどありません。(クローン可能をサポートする数より少ない)

一般的なクローンの場合、クラスの新しいインスタンスを作成し、リフレクション(実際にはリフレクションのようなものですがより高速)を使用して元の(浅いコピー)からフィールドをコピーするヘルパーメソッドがあります

ディープコピーの場合、簡単なアプローチは、オブジェクトをシリアル化し、逆シリアル化することです。

ところで:私の提案は不変のオブジェクトを使用することです、そうすればそれらを複製する必要はありません。;)


不変オブジェクトを使用した場合にクローンを作成する必要がない理由を説明してください。私のアプリケーションでは、既存のオブジェクトとまったく同じデータを持つ別のオブジェクトが必要です。だから私はどういうわけかそれをコピーする必要があります
Zhenya

2
@Ievgen同じデータへの2つの参照が必要な場合は、参照をコピーできます。オブジェクトのコンテンツが変更される可能性がある場合にのみコピーする必要があります(ただし、不変のオブジェクトの場合は変更されないことがわかっています)
Peter Lawrey

おそらく私は不変オブジェクトの利点を理解していません。JPA /休止状態エンティティがあり、既存のエンティティに基づいて別のJPAエンティティを作成する必要があるが、新しいエンティティのIDを変更する必要があるとします。不変オブジェクトでそれをどのように行うのですか?
Zhenya

@Ievgenの定義により、不変オブジェクトを変更することはできません。 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();
}

このように、コピー可能なインターフェイスを実装する必要があるクラスは、次のようにする必要があります。

public class MyClass implements Copyable<MyClass> {

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

}

2
ほとんど。Copyableインターフェースは、次のように宣言する必要がありますinterface Copyable<T extends Copyable>。これは、Javaでエンコードできる自己型に最も近いものです。
2015年

もちろん、あなたはそういう意味interface Copyable<T extends Copyable<T>>でしたね?;)
Adowrath 2017年

3

以下は、多くの開発者が使用しないいくつかの短所です Object.clone()

  1. Object.clone()メソッドを使用するにはCloneable、インターフェースの実装、clone()メソッドとハンドルの定義CloneNotSupportedException、最後にObject.clone()オブジェクトの呼び出しとキャストなど、コードに多くの構文を追加する必要があります。
  2. Cloneableインターフェイスにはclone()メソッドがなく、マーカーインターフェイスであり、メソッドが含まれていません。それでもclone()、オブジェクトに対して実行できることをJVMに通知するためだけに実装する必要があります。
  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で一意にしたい場合、コンストラクターを呼び出さず、finalフィールドをから変更できないObject.clone()ため、を使用すると重複オブジェクトを取得します。Object.clone()idPerson.clone()

コピーコンストラクターはObject.clone()

  1. インターフェイスの実装や例外のスローを強制しないでください。ただし、必要に応じて確実に実装できます。
  2. キャストは必要ありません。
  3. 未知のオブジェクト作成メカニズムに依存する必要はありません。
  4. 親クラスが契約に従うことや何かを実装することを要求しないでください。
  5. 最終フィールドの変更を許可します。
  6. オブジェクトの作成を完全に制御できるようにします。初期化ロジックをその中に記述できます。

Javaのクローン作成についてもっと読む-コピーコンストラクターとクローン作成


1

通常、clone()は保護されたコピーコンストラクターと連携して機能します。これは、コンストラクターとは異なり、clone()が仮想である可能性があるためです。

スーパークラスベースから派生したクラス本体には、

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()メソッドに戻ると、非最終メンバーを設定します。

賢明な読者は、ベースにコピーコンストラクターがある場合、それはsuper.clone()によって呼び出され、保護されたコンストラクターでスーパーコンストラクターを呼び出すと再び呼び出されることに気付くでしょう。スーパーコピーコンストラクターを2回。うまくいけば、それがリソースをロックしている場合、それはそれを知っているでしょう。


0

あなたのために働くかもしれない1つのパターンはBeanレベルのコピーです。基本的に、引数なしのコンストラクターを使用し、さまざまなセッターを呼び出してデータを提供します。さまざまな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()のディープコピーを実行することは不可能かもしれませんが、実際には、これは多くのオブジェクト階層で管理可能です。それはあなたが持っているオブジェクトの種類とそれらのメンバー変数が何であるかに依存します。
シャイニー氏と

多くの人が主張するよりもはるかに曖昧さが少ない。SuperDuperList<T>クローン可能またはその派生物がある場合、それをクローンすると、クローンされたものと同じタイプの新しいインスタンスが生成されます。オリジナルから切り離す必要がありますが、オリジナルTと同じ順序で同じを参照する必要があります。その時点からどちらかのリストに対して行われることは、もう一方に格納されているオブジェクトのIDに影響を与えるべきではありません。私は、他の振る舞いを示すためにジェネリックコレクションを必要とする有用な「慣習」を知りません。
スーパーキャット2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.