Klonowanie płytkie czy głębokie w Javie

Java Clonable

Klonowanie jest to mechanizm polegający na duplikowaniu, czyli kopiowaniu jednego obiektu na drugi. Jednak w Javie na obiekty wskazują referencje i to właśnie przy ich pomocy można manipulować obiektami. Przypisanie obiektu do innej referencji duplikuje samą referencję, a nie obiekt, na który wskazuje. Mechanizm klonowania i metoda clone() jest jednym z potencjalnych rozwiązań dla brakującej funkcjonalności kopiowania obiektów.

Jak działa clone() i Clonable w Javie

Metoda clone() ma swoją implementację już w klasie Object. Jednak żeby móc sklonować obiekt danej klasy, trzeba jeszcze implementować interfejs Cloneable. W przeciwnym wypadku zostanie rzucony wyjątek: java.lang.CloneNotSupportedException.

 

protected native Object clone() throws CloneNotSupportedException;

public interface Cloneable { }

Domyślna deklaracja metody clone() oznaczona jest modyfikatorem dostępu protected, dlatego, żeby można było ją wywołać z poziomu innej klasy, trzeba ją jeszcze nadpisać i zmienić modyfikator na public. Opcjonalnie można dodatkowo rzutować zwrócony obiekt na aktualną klasę.

Płytkie czy głębokie kopiowanie (deep copy vs shallow copy)

Kopiowanie płytkie (shallow copy)

Jest to podstawowa i najczęściej spotykana metoda kopiowania obiektów. W tym przypadku podczas klonowania jednego obiektu na drugi, wszystkie jego własności (typy proste i referencje) są również kopiowane.

W przypadku typów prostych wartości zostaną skopiowane i można je modyfikować niezależnie w oryginalnym obiekcie i w jego kopii. Jednak dla wszystkich pól przechowujących referencje (adresy w pamięci komputera), to właśnie referencje będą skopiowane i po skopiowaniu będą dalej wskazywały dokładnie na ten sam obiekt. W efekcie czego oba obiekty, oryginał i kopia, wskazują na ten sam obiekt zależny. Jego modyfikacja zmieni stan obu obiektów.

Kopiowanie płytkie (shallow copy)

Kopiowanie płytkie (shallow copy)

Kopiowanie głębokie (deep copy)

Alternatywą dla kopiowania płytkiego jest kopiowanie głębokie. W tym wypadku referencje zamiast być kopiowane, są tak modyfikowane, by wskazywały na obiekty zależne, które również są klonowane.

Takie podejście jest jednak zdecydowanie bardziej skomplikowane i kosztowne. Powoduje to konieczność tworzenia wielu nowych obiektów. Mogą również pojawić się problemy, jeżeli w grafie zależności obiektów będą cykle.

Kopiowanie głębokie (deep copy)

Kopiowanie głębokie (deep copy)

Powyższe trudności powodują, że bardzo często powstają rozwiązania hybrydowe, łączące kopiowanie płytkie i głębokie.

Klonowanie półautomatyczne

Poniższy kod jest przykładem klonowania półautomatycznego. W pierwszym kroku tworzona jest kopia aktualnego obiektu, zawierająca również kopię wszystkich jego trzech pól.

Referencje dla pól: name, age i interest prowadzą dokładnie do tych samych obiektów, co w oryginalnym obiekcie.

W kolejnym kroku tworzony jest nowy obiekt typu Interest i nadpisywana jest referencja prowadząca do niego. W tym wypadku do utworzenia został wykorzystany zwykły konstruktor, jednak gdyby klasa Interest obsługiwała klonowanie, mogłaby być to metoda clone().

Błędy architektoniczne, czyli dlaczego klonowanie jest ZŁE

Skoro Java posiada wbudowany mechanizm do obsługi klonowania obiektów, wydawałoby się, że będzie to mechanizm dopracowany i polecany przy rozwiązywaniu podobnych problemów. Niestety ta część API Javy powstała dość dawno i zawiera bardzo dużo błędów architektonicznych, uniemożliwiających wręcz wykorzystanie jej w wielu sytuacjach. Nawet sam Josh Bloch opisuje to rozwiązanie jako deeply broken.

Spośród błędów projektowych klonowania w Javie najważniejsze to:

  • Interfejs Cloneable nie ma metody clone()! – jest to największy błąd, który skutkuje licznymi negatywnymi konsekwencjami:
    • oznaczenie klasy jako Cloneable nic nie mówi, na temat tego, co można z danym obiektem zrobić. Wskazuje jedynie, że “coś” ma zadziać się lokalnie, w implementacji Javy;
    • przechowując np. kolekcję obiektów implementujących Cloneable, nie można na nich wykonać polimorficznej operacji clone;
    • nie można rzutować obiektu na Cloneable i wykonać na nim operacji clone();
    • również rzutowanie obiektu do klasy Object niewiele pomoże, ponieważ zadeklarowana w niej metoda clone jest chroniona (protected);
  • Przy powyższych ograniczeniach dla klienta klasy nie ma właściwie żadnych korzyści z takiego rozwiązania. Równie dobrze można utworzyć dowolną inną metodę (np. copy) i zrealizować w niej kopiowania/klonowania w inny, dowolny sposób;
  • Mechanizm klonowania może okazać się zdradliwy, ponieważ zamiast tworzyć na nowo pola obiektów, opiera swoje działanie na ich kopiowaniu. W praktyce oznacza to, że przy tworzeniu nowych obiektów nie jest wywoływany ich konstruktor i inicjalizacja stanu obiektu może przebiec w inny sposób, niż programista to pierwotnie zakładał;
  • Standardowa implementacja polega na wywołaniu metody rodzica: super.clone(), w całej strukturze dziedziczenia, aż do klasy Object. Takie rozwiązanie skutkuje tak zwanym płytkim kopiowaniem (shallow copy). W efekcie czego sklonowane obiekty mogą współdzielić między sobą stan. To z kolei może prowadzić do niespodziewanego modyfikowania obiektów zależnych i trudnych do wychwycenia błędów. Alternatywą tego rozwiązania jest kopiowanie głębokie (deep copy);
Java clone - inne sposoby

Java clone – inne sposoby

Skoro nie klonowanie, to co?

1. Copy constructor

Wzorzec projektowy copy constructor to jedna z częściej wybieranych alternatyw dla klonowania obiektów. W tym wzorcu jeden z konstruktorów klasy przyjmuje obiekt, na podstawie którego inicjowany jest wewnętrzny stan obiektu. Nowo tworzony obiekt i obiekt przekazywany jako argument są zazwyczaj tego samego typu, jednak nie jest to wymóg.

Powyższy przykład konstruktora kopiującego realizuje kopiowanie płytkie (shallow copy), jednak nic nie stoi na przeszkodzie, żeby wykorzystać ten wzorzec do kopiowania głębokiego (deep copy).

Copy constructor jest bardzo często wykorzystywany w operacjach na kolekcjach w Javie.

new ArrayList(new LinkedList());

new LinkedHashSet(new HashSet());

2. Statyczna metoda kopiująca

Jest to modyfikacja metody z copy constructor, w której konstruktor został zamieniony na statyczną metodę kopiującą.

3. Klonowanie przez serializację

Serializacja jest dość prostym sposobem na osiągnięcie kopiowania głębokiego.

Trzeba jednak pilnować, żeby klasa główna oraz wszystkie obiekty zależne były serializowane. W przeciwnym wypadku dostaniemy wyjątek: java.io.NotSerializableException.

4. Klonowanie z wykorzystaniem Apache Commons

Apache Commons udostępnia gotową funkcjonalność klonowania obiektów. Wykorzystanie jej jest wyjątkowo proste, wystarczy wywołać gotową metodę z klasy narzędziowej: SerializationUtils.

W tym wypadku klonowanie wewnętrznie zostało zaimplementowane z wykorzystaniem serializacji, dlatego wszystkie uwarunkowania są prawie identyczne jak w poprzednim przykładzie.

Testy porównawcze

Test polegał na przygotowaniu i uruchomieniu 1 000 000 (milion) razy niezależnych funkcji kopiujących obiekty i zmierzeniu czasu ich wykonania.

Cały kod testu dostępny jest na repozytorium.

  • Utworzenie obiektu – test polegał na samym tworzeniu nowego obiektu przez operator new
  • Klonowanie płytkie – na początku został utworzony jeden obiekt, a następnie był klonowany przy każdej iteracji testu
  • Klonowanie głębokie – podobnie jak przykład wyżej, tylko dla klonowania głębokiego
  • Serializacja – uruchomienie klonowania głębokiego przez serializację
  • Apache Commons – klonowanie z wykorzystaniem biblioteki Apache Commons wewnętrznie zrealizowane jest przez serializację i deserializację obiektu
  • Copy constructor – kopiowanie głębokie z wykorzystaniem wzorca projektowego copy constructor
Utworzenie obiektuKlonowanie płytkieKlonowanie głębokieSerializacjaApache CommonsCopy constructor
23ms15ms55ms23485ms24429ms60ms

Wnioski

Kopiowanie obiektów jest bardzo przydatną  i dość często realizowaną funkcjonalnością, dlatego warto chwilę się zastanowić, by dobrze ją realizować.

Zważywszy na liczne wady wbudowanego rozwiązania do klonowania, warto poszukać alternatywnych rozwiązań.

Przy ręcznej realizacji tej funkcjonalności bardzo dobrze sprawuje się wzorzec projektowy copy constructor. Przy jego pomocy można bardzo precyzyjnie określić, które obiekty mają być kopiowane, a które referencje zachowane w obecnej formie.

W przypadku chęci zautomatyzowania całego procesu można posłużyć się serializacją. To rozwiązanie dużo łatwiej jest zrealizować, jednak nie daje tak rozbudowanych możliwości konfiguracji. Jest też zdecydowanie wolniejsze, jednak to nie zawsze ma znaczenie.

 

Programista – Pytania rekrutacyjne

Lista pytań rekrutacyjnych, które pozwolą przygotować Ci się na rozmowę kwalifikacyjną.

No comments
Share:

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *