Jak poprawnie zastąpić metodę klonowania?

114

Muszę zaimplementować głębokiego klona w jednym z moich obiektów, który nie ma nadklasy.

Jaki jest najlepszy sposób na obsłużenie czeku CloneNotSupportedExceptionwyrzuconego przez superklasę (czyli Object)?

Współpracownik poradził mi, żebym to zrobił w następujący sposób:

@Override
public MyObject clone()
{
    MyObject foo;
    try
    {
        foo = (MyObject) super.clone();
    }
    catch (CloneNotSupportedException e)
    {
        throw new Error();
    }

    // Deep clone member fields here

    return foo;
}

Wydaje mi się, że jest to dobre rozwiązanie, ale chciałem przekazać to społeczności StackOverflow, aby sprawdzić, czy są jakieś inne spostrzeżenia, które mogę uwzględnić. Dzięki!

Cuga
źródło
6
Jeśli wiesz, że klasa nadrzędna implementuje, Cloneableto rzucanie AssertionErrorzamiast zwykłego zwykłego Errorjest nieco bardziej wyraziste.
Andrew Duffy,
Edycja: Wolałbym nie używać clone (), ale projekt jest już na nim oparty i nie warto byłoby w tym momencie refaktoryzować wszystkich odniesień.
Cuga
Lepiej ugryźć kulę teraz niż później (no chyba, że ​​masz zamiar rozpocząć produkcję).
Tom Hawtin - tackline
Zostało nam półtora miesiąca w harmonogramie do zakończenia tego. Nie możemy uzasadnić czasu.
Cuga,
Myślę, że to rozwiązanie jest idealne. I nie przejmuj się używaniem klonu. Jeśli masz klasę, która jest używana jako obiekt transportowy i zawiera tylko elementy pierwotne i niezmienne, takie jak String i Enums, wszystko inne niż clone byłoby całkowitą stratą czasu z dogmatycznych powodów. Pamiętaj tylko, co robi klon, a co nie! (bez głębokiego klonowania)
TomWolk

Odpowiedzi:

125

Czy absolutnie musisz użyć clone? Większość ludzi zgadza się, że Java clonejest zepsuta.

Josh Bloch on Design - Copy Constructor versus Cloning

Jeśli przeczytałeś artykuł o klonowaniu w mojej książce, zwłaszcza jeśli czytasz między wierszami, będziesz wiedział, że moim zdaniem clonejest głęboko zepsuty. […] Szkoda, że Cloneablejest zepsuty, ale się zdarza.

Możesz przeczytać więcej dyskusji na ten temat w jego książce Effective Java 2nd Edition, Item 11: Override clonerozsądnie . Zamiast tego zaleca użycie konstruktora kopiującego lub fabryki kopiowania.

Następnie napisał kilka stron o tym, jak, jeśli uważasz, że musisz, powinieneś wdrażać clone. Ale zakończył tym:

Czy wszystkie te zawiłości są naprawdę konieczne? Rzadko. Jeśli rozszerzasz klasę, która implementuje Cloneable, nie masz innego wyboru, jak zaimplementować dobrze zachowaną clonemetodę. W przeciwnym razie lepiej będzie zapewnić alternatywne sposoby kopiowania obiektów lub po prostu nie zapewnić takiej możliwości .

Nacisk był jego, nie mój.


Ponieważ jasno wskazałeś, że nie masz innego wyboru, jak tylko wdrożyć clone, oto, co możesz zrobić w tym przypadku: upewnij się, że MyObject extends java.lang.Object implements java.lang.Cloneable. W takim przypadku możesz zagwarantować, że NIGDY nie złapiesz CloneNotSupportedException. Rzucanie, AssertionErrorjak sugerowali niektórzy, wydaje się rozsądne, ale możesz również dodać komentarz wyjaśniający, dlaczego blok catch nigdy nie zostanie wprowadzony w tym konkretnym przypadku .


Alternatywnie, jak sugerowali inni, możesz być może zaimplementować clonebez wywoływania super.clone.

smary wielogenowe
źródło
1
Niestety projekt jest już napisany metodą klonowania, w przeciwnym razie absolutnie bym go nie użył. Całkowicie zgadzam się z Tobą, że implementacja klona w Javie to fakakta.
Cuga,
5
Jeśli klasa i wszystkie jej nadklasy wywołują super.clone()w swoich metodach klonowania, podklasa będzie musiała nadpisać tylko clone()wtedy, gdy doda nowe pola, których zawartość będzie musiała zostać sklonowana. Jeśli jakakolwiek nadklasa używa newzamiast super.clone(), to wszystkie podklasy muszą przesłonić clone()to, czy dodają nowe pola, czy nie.
supercat
56

Czasami łatwiej jest zaimplementować konstruktor kopiujący:

public MyObject (MyObject toClone) {
}

Oszczędza Ci to kłopotów z obsługą CloneNotSupportedException, działa z finalpolami i nie musisz martwić się o typ do zwrotu.

Aaron Digulla
źródło
11

Sposób działania twojego kodu jest bardzo zbliżony do „kanonicznego” sposobu jego pisania. Ale rzuciłbym AssertionErrorw pułapkę. Sygnalizuje, że ta linia nigdy nie powinna zostać osiągnięta.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
źródło
Zobacz odpowiedź @polygenelubricants, aby dowiedzieć się, dlaczego może to być zły pomysł.
Karl Richter,
@KarlRichter Przeczytałem ich odpowiedź. Nie oznacza to, że jest to zły pomysł, poza tym, że Cloneableogólnie jest to zepsuty pomysł, jak wyjaśniono w Efektywna Java. PO już wyraził, że muszą korzystać Cloneable. Nie mam więc pojęcia, jak inaczej można by ulepszyć moją odpowiedź, może poza jej całkowitym usunięciem.
Chris Jester-Young
9

Istnieją dwa przypadki, w których CloneNotSupportedExceptionzostanie wyrzucony testament:

  1. Klonowana klasa nie została zaimplementowana Cloneable(zakładając, że faktyczne klonowanie ostatecznie odroczone zostanie do Objectmetody clone). Jeśli klasa, w której piszesz tę metodę w implementacjach Cloneable, nigdy się nie wydarzy (ponieważ wszystkie podklasy odziedziczą ją odpowiednio).
  2. Wyjątek jest jawnie zgłaszany przez implementację - jest to zalecany sposób zapobiegania klonowaniu w podklasie, gdy nadklasa jest Cloneable.

Ten drugi przypadek nie może wystąpić w twojej klasie (ponieważ bezpośrednio wywołujesz metodę nadklasy w trybloku, nawet jeśli jest wywoływana z wywołania podklasy super.clone()), a ten pierwszy nie powinien, ponieważ twoja klasa wyraźnie powinna implementować Cloneable.

Zasadniczo powinieneś na pewno zarejestrować błąd, ale w tym konkretnym przypadku stanie się to tylko wtedy, gdy zepsujesz definicję swojej klasy. Dlatego traktuj go jak sprawdzoną wersję NullPointerException(lub podobną) - nigdy nie zostanie wyrzucona, jeśli twój kod działa.


W innych sytuacjach trzeba by być przygotowanym na taką ewentualność - nie ma gwarancji, że dany przedmiot jest Cloneable, więc podczas połowu wyjątek należy podjąć odpowiednie działania w zależności od tego warunku (kontynuacja istniejącego obiektu, wziąć alternatywną strategię klonowania np. serialize-deserialize, wyrzuć, IllegalParameterExceptionjeśli twoja metoda wymaga parametru przez cloneable itp.).

Edycja : Chociaż ogólnie powinienem zaznaczyć, że tak, clone()naprawdę trudno jest poprawnie zaimplementować i trudno jest dzwoniącym wiedzieć, czy wartość zwracana będzie taka, jakiej chcą, podwójnie, jeśli weźmie się pod uwagę klony głębokie i płytkie. Często lepiej jest po prostu całkowicie uniknąć całej sytuacji i użyć innego mechanizmu.

Andrzej Doyle
źródło
Jeśli obiekt ujawnia publiczną metodę klonowania, każdy obiekt pochodny, który jej nie obsługuje, naruszyłby zasadę zastępowania Liskova. Jeśli metoda klonowania jest chroniona, myślę, że lepiej byłoby umieścić ją w cieniu czymś innym niż metoda zwracająca odpowiedni typ, aby zapobiec nawet próbie wywołania przez podklasę super.clone().
supercat
5

Użyj serializacji, aby wykonać głębokie kopie. Nie jest to najszybsze rozwiązanie, ale nie zależy od typu.

Michał Ziober
źródło
To było świetne i oczywiste rozwiązanie, biorąc pod uwagę, że używałem już GSON.
Jacob Tabak,
3

Możesz zaimplementować chronione konstruktory kopiujące, takie jak:

/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) {
    this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
    this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
    // etc
}

public MyObject clone() {
    return new MyObject(this);
}
Dolph
źródło
3
To nie działa przez większość czasu. Nie możesz wywołać clone()obiektu zwróconego przez, getMySecondMember()chyba że ma public clonemetodę.
poligenelubricants
Oczywiście musisz zaimplementować ten wzorzec na każdym obiekcie, który chcesz sklonować, lub znaleźć inny sposób głębokiego sklonowania każdego elementu.
Dolph
Tak, to konieczne.
poligenelubricants
1

O ile większość odpowiedzi tutaj jest poprawnych, muszę powiedzieć, że Twoje rozwiązanie jest również sposobem, w jaki robią to faktyczni programiści Java API. (Albo Josh Bloch albo Neal Gafter)

Oto wyciąg z openJDK, klasy ArrayList:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

Jak zauważyłeś i inni wspomnieli, CloneNotSupportedExceptionnie ma prawie żadnych szans na wyrzucenie, jeśli zadeklarowałeś, że implementujesz Cloneableinterfejs.

Ponadto, nie ma potrzeby, aby zastąpić metodę, jeśli nie nic nowego w zastąpiona metoda zrobić. Musisz to zmienić tylko wtedy, gdy musisz wykonać dodatkowe operacje na obiekcie lub musisz go upublicznić.

Ostatecznie nadal najlepiej jest tego unikać i robić to w inny sposób.

Haggra
źródło
0
public class MyObject implements Cloneable, Serializable{   

    @Override
    @SuppressWarnings(value = "unchecked")
    protected MyObject clone(){
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            ByteArrayOutputStream bOs = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bOs);
            oos.writeObject(this);
            ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
            return  (MyObject)ois.readObject();

        } catch (Exception e) {
            //Some seriouse error :< //
            return null;
        }finally {
            if (oos != null)
                try {
                    oos.close();
                } catch (IOException e) {

                }
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {

                }
        }
    }
}
Kvant_hv
źródło
Więc właśnie zapisałeś go w systemie plików i odczytałeś obiekt z powrotem. OK, czy to najlepsza metoda obsługi klonowania? Czy ktokolwiek ze społeczności SO może skomentować to podejście? Myślę, że to niepotrzebnie wiąże klonowanie i serializację - dwie całkowicie różne koncepcje. Poczekam, aby zobaczyć, co inni mają do powiedzenia na ten temat.
Saurabh Patil
2
Nie ma go w systemie plików, ale w pamięci głównej (ByteArrayOutputStream). w przypadku obiektów głęboko zagnieżdżonych jest to rozwiązanie, które działa dobrze. Zwłaszcza jeśli nie potrzebujesz tego często, np. W teście jednostkowym. gdzie wydajność nie jest głównym celem.
AlexWien
0

Tylko dlatego, że implementacja Cloneable w Javie jest zepsuta, nie oznacza to, że nie możesz stworzyć własnego.

Jeśli prawdziwym celem OP było stworzenie głębokiego klonu, myślę, że można stworzyć taki interfejs:

public interface Cloneable<T> {
    public T getClone();
}

następnie użyj wspomnianego wcześniej konstruktora prototypów, aby go zaimplementować:

public class AClass implements Cloneable<AClass> {
    private int value;
    public AClass(int value) {
        this.vaue = value;
    }

    protected AClass(AClass p) {
        this(p.getValue());
    }

    public int getValue() {
        return value;
    }

    public AClass getClone() {
         return new AClass(this);
    }
}

i inna klasa z polem obiektu AClass:

public class BClass implements Cloneable<BClass> {
    private int value;
    private AClass a;

    public BClass(int value, AClass a) {
         this.value = value;
         this.a = a;
    }

    protected BClass(BClass p) {
        this(p.getValue(), p.getA().getClone());
    }

    public int getValue() {
        return value;
    }

    public AClass getA() {
        return a;
    }

    public BClass getClone() {
         return new BClass(this);
    }
}

W ten sposób można łatwo i głęboko sklonować obiekt klasy BClass bez potrzeby używania @SuppressWarnings lub innego sztucznego kodu.

Francesco Rogo
źródło