Java Tip 76: Алтернатива на техниката за дълбоко копиране

Внедряването на дълбоко копие на обект може да бъде учебно преживяване - научавате, че не искате да го правите! Ако въпросният обект се отнася до други сложни обекти, които от своя страна се отнасят до други, тогава тази задача може да бъде наистина плашеща. Традиционно всеки клас в обекта трябва да бъде индивидуално проверен и редактиран, за да се приложи Cloneableинтерфейсът и да се замени clone()методът му, за да се направи дълбоко копие на себе си, както и на съдържащите се обекти. Тази статия описва проста техника, която да се използва вместо това отнемащо време конвенционално дълбоко копие.

Концепцията за дълбоко копиране

За да разберем какво е дълбоко копие , нека първо разгледаме концепцията за плитко копиране.

В предишна статия на JavaWorld , „Как да избегнем капани и правилно да заменим методите от java.lang.Object“, Марк Руло обяснява как да клонирате обекти, както и как да постигнете плитко копиране вместо дълбоко копиране. За да обобщим накратко тук, при копиране на обект без съдържащите се обекти възниква плитко копие. За илюстрация на фигура 1 е показан обект obj1, който съдържа два обекта, containedObj1и containedObj2.

Ако се изпълни плитко копие obj1, то се копира, но съдържащите се обекти не са, както е показано на фигура 2.

Дълбоко копие възниква, когато обект се копира заедно с обектите, за които се отнася. Фигура 3 показва obj1след като е извършено дълбоко копиране върху него. Не само е obj1копирано, но и обектите, съдържащи се в него, също са копирани.

Ако някой от тези съдържащи се обекти сам съдържа обекти, тогава в дълбоко копие се копират и тези обекти и така нататък, докато цялата графика не бъде обходена и копирана. Всеки обект е отговорен за клонирането по своя clone()метод. Методът по подразбиране clone(), наследен от Object, прави плитко копие на обекта. За да се постигне дълбоко копие, трябва да се добави допълнителна логика, която изрично извиква clone()методите на всички съдържащи се обекти , които от своя страна извикват clone()методите на съдържащите се обекти и т.н. Получаването на това правилно може да бъде трудно и отнема много време и рядко е забавно. За да направим нещата още по-сложни, ако обект не може да бъде модифициран директно и clone()методът му създава плитко копие, тогава класът трябва да бъде разширен,clone()метод е заменен и този нов клас се използва вместо стария. (Например, Vectorне съдържа логиката, необходима за дълбоко копиране.) И ако искате да напишете код, който отлага до изпълнението на въпроса дали да направите дълбоко или плитко копие на обект, ви очаква още по-сложно ситуация. В този случай трябва да има две функции за копиране за всеки обект: една за дълбоко копие и една за плитка. И накрая, дори ако обектът, който се копира дълбоко, съдържа множество препратки към друг обект, последният обект трябва да бъде копиран само веднъж. Това предотвратява разпространението на обекти и премахва специалната ситуация, при която кръгова препратка създава безкраен цикъл от копия.

Сериализация

Още през януари 1998 г. JavaWorld инициира своята колона JavaBeans от Марк Джонсън със статия за сериализацията „Направете го по начина„ Nescafé “- с лиофилизиран JavaBeans.“ За да обобщим, сериализацията е способността да се превърне графика на обекти (включително дегенерирания случай на единичен обект) в масив от байтове, които могат да бъдат превърнати обратно в еквивалентна графика на обекти. Казва се, че даден обект е сериализуем, ако той или някой от неговите предци прилага java.io.Serializableили java.io.Externalizable. Сериализуем обект може да се сериализира, като го предаде на writeObject()метода на ObjectOutputStreamобект. Това изписва примитивните типове данни на обекта, масиви, низове и други препратки към обекти. ThewriteObject()След това методът се извиква към препратените обекти, за да сериализира и тях. Освен това, всеки от тези обекти има своите референции и сериализирани обекти; този процес продължава и продължава, докато цялата графика не бъде обходена и сериализирана. Звучи ли ви познато? Тази функционалност може да се използва за постигане на дълбоко копие.

Дълбоко копиране с помощта на сериализация

Стъпките за създаване на дълбоко копие чрез сериализация са:

  1. Уверете се, че всички класове в графиката на обекта са сериализуеми.

  2. Създайте входни и изходни потоци.

  3. Използвайте входните и изходните потоци, за да създадете входни и изходни потоци на обекти.

  4. Предайте обекта, който искате да копирате, в изходния поток на обекта.

  5. Прочетете новия обект от входния поток на обекта и го върнете обратно в класа на обекта, който сте изпратили.

Написах клас, наречен, ObjectClonerкойто изпълнява стъпки от две до пет. Редът, означен с „A“, задава a, ByteArrayOutputStreamкойто се използва за създаване на ObjectOutputStreamон-лайн линия B. В линия C се прави магията. В writeObject()метода рекурсивно пресича графиката на обекта, генерира нов обект в байт форма и я изпраща до ByteArrayOutputStream. Ред D гарантира, че целият обект е изпратен. След това кодът на ред E създава a ByteArrayInputStreamи го попълва със съдържанието на ByteArrayOutputStream. Ред F създава екземпляр с ObjectInputStreamпомощта на ByteArrayInputStreamсъздадения на ред E и обектът се десериализира и се връща към извикващия метод на ред G. Ето кода:

импортиране на java.io. *; импортиране на java.util. *; импортиране на java.awt. *; публичен клас ObjectCloner {// така че никой да не може случайно да създаде обект ObjectCloner private ObjectCloner () {} // връща дълбоко копие на обект статичен публичен обект deepCopy (Object oldObj) хвърля изключение {ObjectOutputStream oos = null; ObjectInputStream ois = null; опитайте {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = нов ObjectOutputStream (bos); // B // сериализираме и предаваме обекта oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = нов ByteArrayInputStream (bos.toByteArray ()); // E ois = нов ObjectInputStream (bin); // F // връщане на новия обект return ois.readObject (); // G} улов (Изключение e) {System.out.println ("Изключение в ObjectCloner =" + e); хвърляне (д); } накрая {oos.close (); ois.close (); }}}

Всичко, което трябва да направи разработчик с достъп до ObjectCloner, преди да стартира този код, е да гарантира, че всички класове в графиката на обекта са сериализуеми. В повечето случаи това вече е трябвало да се направи; ако не, трябва да бъде относително лесно да се направи с достъп до изходния код. Повечето класове в JDK са сериализуеми; само тези, които са зависими от платформата, като например FileDescriptor, не са. Също така всички класове, които получавате от доставчик на трета страна, които са съвместими с JavaBean, по дефиниция могат да бъдат сериализуеми. Разбира се, ако разширите клас, който е сериализуем, тогава новият клас също е сериализуем. С всички тези сериализуеми класове, които се движат наоколо, има вероятност единствените, които може да се наложи да сериализирате, да са вашите собствени и това е парче торта в сравнение с преминаването през всеки клас и презаписванетоclone() да направите дълбоко копие.

Един лесен начин да разберете дали имате някакви nonserializable класове в графика на даден обект е да се предположи, че всички те са Serializable и бягай ObjectClonerdeepCopy()метод на него. Ако има обект, чийто клас не е сериализуем, тогава java.io.NotSerializableExceptionще бъде хвърлен a , който ви казва кой клас е причинил проблема.

Пример за бързо изпълнение е показан по-долу. Създава прост обект v1, който Vectorсъдържа a Point. След това този обект се разпечатва, за да покаже съдържанието му. След това оригиналният обект, v1се копира в нов обект vNew, който се отпечатва, за да покаже, че съдържа същата стойност като v1. На следващо място, съдържанието на v1са се променили, и в крайна сметка и двете v1и vNewсе отпечатват така, че техните стойности могат да бъдат сравнявани.

импортиране на java.util. *; импортиране на java.awt. *; публичен клас Driver1 {static public void main (String [] args) {try {// вземете метода от командния ред String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Употреба: java драйвер1 [дълбоко, плитко]"); връщане; } // създаваме оригинален обект Vector v1 = new Vector (); Точка p1 = нова точка (1,1); v1.addElement (p1); // вижте какво е System.out.println ("Original =" + v1); Вектор vNew = нула; if (meth.equals ("deep")) {// дълбоко копиране vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("плитко")) {// плитко копие vNew = (Vector) v1.clone (); // B} // проверяваме дали е същият System.out.println ("New =" + vNew);// промяна на съдържанието на оригиналния обект p1.x = 2; p1.y = 2; // вижте какво има във всеки един сега System.out.println ("Original =" + v1); System.out.println ("Ново =" + vNew); } catch (Изключение e) {System.out.println ("Изключение в main =" + e); }}}

За да извикате дълбокото копие (ред А), изпълнете java.exe Driver1 deep. Когато дълбокото копие се изпълни, получаваме следната разпечатка:

Оригинал = [java.awt.Point [x = 1, y = 1]] Ново = [java.awt.Point [x = 1, y = 1]] Оригинал = [java.awt.Point [x = 2, y = 2]] Ново = [java.awt.Point [x = 1, y = 1]] 

Това показва, че когато оригиналът Point,, p1е променен, новият, Pointсъздаден в резултат на дълбокото копие, остава незасегнат, тъй като цялата графика е копирана. За сравнение извикайте плиткото копие (ред Б), като изпълните java.exe Driver1 shallow. Когато се изпълни плиткото копие, получаваме следната разпечатка:

Оригинал = [java.awt.Point [x = 1, y = 1]] Ново = [java.awt.Point [x = 1, y = 1]] Оригинал = [java.awt.Point [x = 2, y = 2]] Ново = [java.awt.Point [x = 2, y = 2]] 

This shows that when the original Point was changed, the new Point was changed as well. This is due to the fact that the shallow copy makes copies only of the references, and not of the objects to which they refer. This is a very simple example, but I think it illustrates the, um, point.

Implementation issues

Now that I've preached about all of the virtues of deep copy using serialization, let's look at some things to watch out for.

The first problematic case is a class that is not serializable and that cannot be edited. This could happen, for example, if you're using a third-party class that doesn't come with the source code. In this case you can extend it, make the extended class implement Serializable, add any (or all) necessary constructors that just call the associated superconstructor, and use this new class everywhere you did the old one (here is an example of this).

This may seem like a lot of work, but, unless the original class's clone() method implements deep copy, you will be doing something similar in order to override its clone() method anyway.

The next issue is the runtime speed of this technique. As you can imagine, creating a socket, serializing an object, passing it through the socket, and then deserializing it is slow compared to calling methods in existing objects. Here is some source code that measures the time it takes to do both deep copy methods (via serialization and clone()) on some simple classes, and produces benchmarks for different numbers of iterations. The results, shown in milliseconds, are in the table below:

Milliseconds to deep copy a simple class graph n times
Procedure\Iterations(n) 1000 10000 100000
clone 10 101 791
serialization 1832 11346 107725

As you can see, there is a large difference in performance. If the code you are writing is performance-critical, then you may have to bite the bullet and hand-code a deep copy. If you have a complex graph and are given one day to implement a deep copy, and the code will be run as a batch job at one in the morning on Sundays, then this technique gives you another option to consider.

Another issue is dealing with the case of a class whose objects' instances within a virtual machine must be controlled. This is a special case of the Singleton pattern, in which a class has only one object within a VM. As discussed above, when you serialize an object, you create a totally new object that will not be unique. To get around this default behavior you can use the readResolve() method to force the stream to return an appropriate object rather than the one that was serialized. In this particular case, the appropriate object is the same one that was serialized. Here is an example of how to implement the readResolve() method. You can find out more about readResolve() as well as other serialization details at Sun's Web site dedicated to the Java Object Serialization Specification (see Resources).

One last gotcha to watch out for is the case of transient variables. If a variable is marked as transient, then it will not be serialized, and therefore it and its graph will not be copied. Instead, the value of the transient variable in the new object will be the Java language defaults (null, false, and zero). There will be no compiletime or runtime errors, which can result in behavior that is hard to debug. Just being aware of this can save a lot of time.

The deep copy technique can save a programmer many hours of work but can cause the problems described above. As always, be sure to weigh the advantages and disadvantages before deciding which method to use.

Conclusion

Внедряването на дълбоко копие на сложна обектна графика може да бъде трудна задача. Показаната по-горе техника е проста алтернатива на конвенционалната процедура за презаписване на clone()метода за всеки обект в графиката.

Дейв Милър е старши архитект в консултантската фирма Javelin Technology, където работи върху Java и интернет приложения. Работил е за компании като Хюз, IBM, Nortel и MCIWorldcom по обектно-ориентирани проекти и е работил изключително с Java през последните три години.

Научете повече за тази тема

  • Уеб сайтът на Sun на Java има раздел, посветен на Спецификацията за сериализация на обекти на Java

    //www.javasoft.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html

Тази история „Java Tip 76: Алтернатива на техниката за дълбоко копиране“ първоначално е публикувана от JavaWorld.