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.
Spis treści
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ę.
public class UserShallow implements Cloneable { @Override public UserShallow clone() throws CloneNotSupportedException { return (UserShallow) super.clone(); } }
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 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.
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().
public class UserSemiAutomatic implements Cloneable { private String name; private Integer age; private Interest interest; public UserSemiAutomatic(String name, Integer age, Interest interest) { this.name = name; this.age = age; this.interest = interest; } @Override public UserSemiAutomatic clone() throws CloneNotSupportedException { UserSemiAutomatic userCloned = (UserSemiAutomatic) super.clone(); userCloned.interest = new Interest(interest.getName()); return userCloned; } }
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);
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.
public class UserCopyConstructor { private String name; private Integer age; private List<Interest> interests; public UserCopyConstructor(UserCopyConstructor userCopyConstructor) { name = userCopyConstructor.name; age = userCopyConstructor.age; interests = userCopyConstructor.interests; } public UserCopyConstructor(String name, Integer age) { this.name = name; this.age = age; } }
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ą.
public class UserCopyMethod { private String name; private Integer age; private List<Interest> interests; public static UserCopyMethod newInstance(UserCopyMethod userCopyConstructor) { UserCopyMethod newUserCopyMethod = new UserCopyMethod(); newUserCopyMethod.name = userCopyConstructor.name; newUserCopyMethod.age = userCopyConstructor.age; newUserCopyMethod.interests = userCopyConstructor.interests; return newUserCopyMethod; } }
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.
class SerializationService { public static <T> T deepCopy(T object) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(object); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); return (T) ois.readObject(); } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); } } } public class UserSerialization implements Serializable { private String name; private Integer age; private List<Interest> interests; public UserSerialization deepCopy() { return SerializationService.deepCopy(this); } }
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.
UserSerialization userCloned = (UserSerialization) SerializationUtils.clone(user);
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 obiektu | Klonowanie płytkie | Klonowanie głębokie | Serializacja | Apache Commons | Copy constructor |
---|---|---|---|---|---|
23ms | 15ms | 55ms | 23485ms | 24429ms | 60ms |
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.
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!
1 Comment
A czym się różni klonowanie głęmbokie a półautomatyczne?
Wg przykładu w półautomatycznym kopiowana jest referencja interest a następnie modyfikowana na nowy obiejt.
To chyba dokładnie tak samo jak w głęmbokim???