clone () vs konstruktor kopiujący vs metoda fabryczna?

81

Zrobiłem szybkie wyszukiwanie w Google na temat implementacji clone () w Javie i znalazłem: http://www.javapractices.com/topic/TopicAction.do?Id=71

Zawiera następujący komentarz:

konstruktory kopiujące i statyczne metody fabryczne stanowią alternatywę dla klonowania i są znacznie łatwiejsze do wdrożenia.

Chcę tylko zrobić głęboką kopię. Wydaje się, że wdrożenie clone () ma wiele sensu, ale ten artykuł wysoko oceniany w Google trochę mnie przeraża.

Oto problemy, które zauważyłem:

Konstruktory kopiujące nie działają z typami generycznymi.

Oto pseudokod, który się nie kompiluje.

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

Przykład 1: Używanie konstruktora kopiującego w klasie ogólnej.

Metody fabryczne nie mają standardowych nazw.

Miło jest mieć interfejs do kodu wielokrotnego użytku.

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

Przykład 2: Używanie clone () w klasie ogólnej.

Zauważyłem, że klon nie jest metodą statyczną, ale czy nadal nie byłoby konieczne tworzenie głębokich kopii wszystkich chronionych pól? Podczas implementacji clone () dodatkowy wysiłek związany z rzucaniem wyjątków w nieklonowalnych podklasach wydaje mi się trywialny.

Czy coś mi brakuje? Wszelkie spostrzeżenia będą mile widziane.

Użytkownik1
źródło

Odpowiedzi:

52

Zasadniczo klon jest uszkodzony . Nic nie będzie łatwo działać z typami generycznymi. Jeśli masz coś takiego (skróconego, aby przekazać punkt):

public class SomeClass<T extends Copyable> {


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

interface Copyable {
    Copyable copy();
}

Następnie z ostrzeżeniem kompilatora możesz wykonać zadanie. Ponieważ typy generyczne są usuwane w czasie wykonywania, coś, co wykonuje kopię, będzie miało ostrzeżenie kompilatora generujące rzutowanie. W tym przypadku nie da się tego uniknąć. . W niektórych przypadkach można tego uniknąć (dzięki, kb304), ale nie we wszystkich. Rozważ przypadek, w którym musisz obsługiwać podklasę lub nieznaną klasę implementującą interfejs (na przykład iterowałeś przez kolekcję elementów do kopiowania, które niekoniecznie generowały tę samą klasę).

Yishai
źródło
2
Można też obejść się bez ostrzeżenia kompilatora.
akarnokd
@ kd304, jeśli masz na myśli wyłączenie ostrzeżenia kompilatora, prawda, ale tak naprawdę nie jest inaczej. Jeśli masz na myśli, że możesz całkowicie uniknąć potrzeby ostrzeżenia, opisz to szczegółowo.
Yishai,
@Yishai: Spójrz na moją odpowiedź. I nie mówiłem o ostrzeżeniu z tłumieniem.
akarnokd
Głosowanie Copyable<T>w dół, ponieważ odpowiedź kd304 ma znacznie większy sens.
Simon Forsberg
25

Istnieje również wzorzec Builder. Aby uzyskać szczegółowe informacje, zobacz Efektywna Java.

Nie rozumiem twojej oceny. W konstruktorze kopiującym jesteś w pełni świadomy typu, dlaczego istnieje potrzeba używania typów generycznych?

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

Ostatnio pojawiło się podobne pytanie .

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

Próbka do uruchomienia:

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);
    }
}
akarnokd
źródło
1
+1 to najprostszy, ale skuteczny sposób na zaimplementowanie konstruktora kopiującego
dfa,
Należy zauważyć, że powyższa MyClass jest ogólną, StackOverflow połknął <T>.
Użytkownik 1,
Poprawiono formatowanie pytania. Użyj czterech spacji w każdym wierszu zamiast tagów pre + code.
akarnokd
Otrzymuję błąd w metodzie kopiowania G - „musi zastąpić metodę
superklasy
3
(Wiem, że to jest stare, ale dla potomności) @CarlPritchett: ten błąd pojawi się w Javie 1.5 i niższych. Oznaczanie metod interfejsu jako @ Override jest dozwolone począwszy od języka Java 1.6.
Carrotman42
10

Java nie ma konstruktorów kopiujących w tym samym sensie, co C ++.

Możesz mieć konstruktor, który pobiera obiekt tego samego typu jako argument, ale niewiele klas to obsługuje. (mniejsza niż liczba, która obsługuje klonowanie)

Dla klonu ogólnego mam metodę pomocniczą, która tworzy nową instancję klasy i kopiuje pola z oryginału (płytka kopia) za pomocą odbić (właściwie coś w rodzaju odbić, ale szybciej)

W przypadku głębokiej kopii prostym podejściem jest serializacja obiektu i deserializacja go.

BTW: Moja sugestia to użycie niezmiennych obiektów, wtedy nie będziesz musiał ich klonować. ;)

Peter Lawrey
źródło
Czy mógłbyś wyjaśnić, dlaczego nie potrzebowałbym klonowania, gdybym używał niezmiennych obiektów? W mojej aplikacji potrzebuję innego obiektu, który ma dokładnie takie same dane jak istniejący obiekt. Więc muszę jakoś to skopiować
Zhenya
2
@Ievgen, jeśli potrzebujesz dwóch odniesień do tych samych danych, możesz skopiować odniesienie. Musisz tylko skopiować zawartość obiektu, jeśli może się zmienić (ale w przypadku niezmiennych obiektów, o których wiesz, że tak się nie stanie)
Peter Lawrey
Prawdopodobnie po prostu nie rozumiem korzyści płynących z niezmiennych obiektów. Załóżmy, że mam jednostkę JPA / hibernację i muszę utworzyć inną jednostkę JPA na podstawie istniejącej, ale muszę zmienić identyfikator nowej jednostki. Jak bym to zrobił z niezmiennymi obiektami?
Zhenya
@Ievgen z definicji nie można zmieniać niezmiennych obiektów. Stringna przykład jest niezmiennym obiektem i możesz przekazać String bez konieczności kopiowania go.
Peter Lawrey
6

Myślę, że odpowiedź Yishai można poprawić, więc nie możemy mieć żadnego ostrzeżenia z następującym kodem:

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

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

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

W ten sposób klasa która musi implementować interfejs Copyable musi wyglądać następująco:

public class MyClass implements Copyable<MyClass> {

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

}
Alepac
źródło
2
Prawie. Interfejs do kopiowania powinien być zadeklarowany jako:, interface Copyable<T extends Copyable>co jest najbardziej zbliżone do typu własnego, który można zakodować w Javie.
Powtórzenie
Oczywiście, że miałeś na myśli interface Copyable<T extends Copyable<T>>, prawda? ;)
Adowrath
3

Poniżej znajduje się kilka wad, z powodu których wielu programistów nie używa Object.clone()

  1. Użycie Object.clone()metody wymaga od nas dodania wielu składni do naszego kodu, takich jak Cloneableinterfejs implementacji , zdefiniowanie clone()metody i uchwytu, CloneNotSupportedExceptiona na koniec wywołanie Object.clone()i rzutowanie tego obiektu.
  2. Cloneableinterfejs nie ma clone()metody, jest to interfejs znacznika i nie ma w nim żadnej metody, a mimo to musimy go zaimplementować tylko po to, aby powiedzieć JVM, że możemy wykonaćclone() na naszym obiekcie.
  3. Object.clone()jest chroniony, więc musimy zapewnić własne clone()i pośrednie połączenie Object.clone()z niego.
  4. Nie mamy żadnej kontroli nad konstrukcją obiektów, ponieważ Object.clone()nie wywołujemy żadnego konstruktora.
  5. Jeśli piszemy clone()metodę w klasie Personpotomnej, to np. Wszystkie jej nadklasy powinny definiować clone()w nich metodę lub dziedziczyć ją z innej klasy nadrzędnej, w przeciwnym razie super.clone()łańcuch się nie powiedzie.
  6. Object.clone()obsługują tylko płytką kopię, więc pola referencyjne naszego nowo sklonowanego obiektu będą nadal zawierać obiekty, które zawierały pola naszego oryginalnego obiektu. Aby temu zaradzić, musimy wdrożyćclone() w każdej klasie, do której odwołuje się nasza klasa, a następnie sklonować je oddzielnie w naszej clone()metodzie, jak w poniższym przykładzie.
  7. Nie możemy manipulować końcowymi polami w, Object.clone()ponieważ końcowe pola można zmienić tylko za pomocą konstruktorów. W naszym przypadku, jeśli chcemy, aby każdy Personobiekt był unikalny według identyfikatora, otrzymamy zduplikowany obiekt, jeśli użyjemy, Object.clone()ponieważ Object.clone()nie wywoła konstruktora, a idpola końcowego nie można zmodyfikować Person.clone().

Konstruktory kopiujące są lepsze niż Object.clone() ponieważ one

  1. Nie zmuszaj nas do implementacji żadnego interfejsu ani nie zgłaszaj żadnego wyjątku, ale z pewnością możemy to zrobić, jeśli jest to wymagane.
  2. Nie wymagają odlewów.
  3. Nie wymagaj od nas polegania na nieznanym mechanizmie tworzenia obiektów.
  4. Nie wymagaj od klasy rodzicielskiej przestrzegania jakiejkolwiek umowy lub implementacji czegokolwiek.
  5. Pozwól nam zmodyfikować końcowe pola.
  6. Pozwól nam mieć pełną kontrolę nad tworzeniem obiektów, możemy w nim zapisać naszą logikę inicjalizacji.

Przeczytaj więcej na temat klonowania języka Java - konstruktor kopiowania a klonowanie

Naresh Joshi
źródło
1

Zwykle clone () działa w tandemie z chronionym konstruktorem kopiowania. Dzieje się tak, ponieważ clone (), w przeciwieństwie do konstruktora, może być wirtualne.

W treści klasy dla klasy Derived from super class Base mamy

class Derived extends Base {
}

Tak więc, w najbardziej uproszczonym przypadku, można dodać do tego wirtualnego konstruktora kopiującego za pomocą funkcji clone (). (W C ++ Joshi zaleca clone jako konstruktor wirtualnego kopiowania).

protected Derived() {
    super();
}

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

To staje się bardziej skomplikowane, jeśli chcesz wywołać super.clone () zgodnie z zaleceniami i musisz dodać tych członków do klasy, możesz spróbować tego

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();
}

Teraz, jeśli egzekucja, masz

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

i wtedy to zrobiłeś

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

wywołałoby to clone () w klasie pochodnej (powyższej), wywołałoby super.clone () - które może być zaimplementowane lub nie, ale zaleca się wywołanie tego. Następnie implementacja przekazuje wynik super.clone () do chronionego konstruktora kopiującego, który pobiera Base i przekazuje do niego wszystkie końcowe elementy członkowskie.

Ten konstruktor kopiujący następnie wywołuje konstruktor kopiujący superklasy (jeśli wiesz, że ma taki konstruktor) i ustawia finały.

Wracając do metody clone (), ustawiasz dowolne elementy, które nie są ostateczne.

Wnikliwi czytelnicy zauważą, że jeśli masz konstruktora kopiującego w Base, zostanie on wywołany przez super.clone () - i zostanie wywołany ponownie po wywołaniu superkonstruktora w chronionym konstruktorze, więc możesz wywołać funkcję super-kopiujący konstruktor dwukrotnie. Miejmy nadzieję, że jeśli blokuje zasoby, będzie o tym wiedział.

Bonaparte
źródło
0

Jednym ze wzorców, który może Ci się przydać, jest kopiowanie na poziomie fasoli. Zasadniczo używasz konstruktora bez arg i wywołujesz różne metody ustawiające w celu udostępnienia danych. Możesz nawet użyć różnych bibliotek właściwości bean, aby stosunkowo łatwo ustawić właściwości. To nie to samo, co wykonanie clone (), ale z wielu praktycznych powodów jest w porządku.

Pan Lśniący i Nowy 安 宇
źródło
0

Interfejs Cloneable jest zepsuty w tym sensie, że jest bezużyteczny, ale klon działa dobrze i może prowadzić do lepszej wydajności dla dużych obiektów - 8 pól i więcej, ale wtedy nie przejdzie analizy ucieczki. więc preferowane jest używanie konstruktora kopiującego przez większość czasu. Używanie clone na array jest szybsze niż Arrays.copyOf, ponieważ gwarantujemy, że długość będzie taka sama.

więcej szczegółów tutaj https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html

user3996996
źródło
0

Jeśli ktoś nie jest w 100% świadomy wszystkich dziwactw clone(), radziłbym trzymać się od tego z daleka. Nie powiedziałbym, że to clone()zepsute. Powiedziałbym: używaj go tylko wtedy, gdy jesteś całkowicie pewien, że jest to najlepsza opcja. Konstruktor kopiujący (lub metoda fabryczna, myślę, że nie ma to większego znaczenia) jest łatwy do napisania (może długi, ale łatwy), kopiuje tylko to, co chcesz skopiować, i kopiuje tak, jak chcesz. Możesz go dopasować do swoich potrzeb.

Dodatkowo: łatwo jest debugować, co się dzieje, gdy wywołujesz metodę konstruktora kopiującego / fabryki.

I clone()nie tworzy „głębokiej” kopii twojego obiektu po wyjęciu z pudełka, zakładając, że masz na myśli, że nie tylko odniesienia (np. Do a Collection) są kopiowane. Ale przeczytaj więcej o głębokiej i płytkiej kopii tutaj: Głęboka kopia, płytka kopia, klon

sorrymissjackson
źródło
-2

Brakuje ci tego, że klon tworzy płytkie kopie domyślnie i zgodnie z konwencją, a tworzenie głębokich kopii jest generalnie niewykonalne.

Problem polega na tym, że tak naprawdę nie można tworzyć głębokich kopii cyklicznych wykresów obiektów bez możliwości śledzenia, które obiekty zostały odwiedzone. clone () nie zapewnia takiego śledzenia (ponieważ musiałby to być parametr .clone ()), a zatem tworzy tylko płytkie kopie.

Nawet jeśli twój własny obiekt wywołuje .clone dla wszystkich swoich członków, nadal nie będzie to głęboka kopia.

Martin przeciwko Löwis
źródło
4
Głębokie kopiowanie clone () dla dowolnego dowolnego obiektu może być niewykonalne, ale w praktyce jest to możliwe do zarządzania w przypadku wielu hierarchii obiektów. Zależy to tylko od rodzaju posiadanych obiektów i ich zmiennych składowych.
Mr. Shiny and New 安 宇
Jest o wiele mniej niejasności, niż twierdzi wielu ludzi. Jeśli ktoś ma klonowalną SuperDuperList<T>lub pochodną, ​​to klonowanie powinno dać nową instancję tego samego typu, co sklonowana; powinien być oddzielony od oryginału, ale powinien odnosić się do tych samych T, w tej samej kolejności, co oryginał. Nic, co zostanie zrobione na którejkolwiek z list od tego momentu, nie powinno wpłynąć na tożsamości obiektów przechowywanych na drugiej. Nie znam żadnej użytecznej „konwencji”, która wymagałaby, aby kolekcja ogólna wykazywała jakiekolwiek inne zachowanie.
supercat