טיפ Java 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האובייקט. זה כותב את סוגי הנתונים הפרימיטיביים של האובייקט, מערכים, מחרוזות והפניות אובייקט אחרות. הwriteObject()לאחר מכן נקראת השיטה על האובייקטים המופנים כדי לסדר אותם גם כן. יתר על כן, לכל אחד מהאובייקטים יש הפניות ואובייקטים שלהם בסידרה; תהליך זה נמשך ונמשך עד שהגרף כולו חוצה את סדרתו. זה נשמע מוכר? בעזרת פונקציונליות זו ניתן להשיג עותק עמוק.

העתקה עמוקה באמצעות סידור

השלבים ליצירת עותק עמוק באמצעות סידור הם:

  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 פרטי ObjectCloner () {} // מחזיר עותק עמוק של אובייקט סטטי ציבורי אובייקט deepCopy (Object oldObj) זורק Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; נסה את {ByteArrayOutputStream bos = ByteArrayOutputStream חדש (); // Aos = ObjectOutputStream חדש (bos); // B // סדרתי והעביר את האובייקט oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = ByteArrayInputStream חדש (bos.toByteArray ()); // E ois = חדש ObjectInputStream (bin); // F // להחזיר את האובייקט החדש להחזיר ois.readObject (); // G} לתפוס (Exception e) {System.out.println ("Exception in ObjectCloner =" + e); לזרוק (ה); } סוף סוף {oos.close (); ois.close (); }}}

כל ObjectClonerשנותר למפתח שיש לו גישה לעשות לפני הפעלת קוד זה הוא להבטיח כי כל המחלקות בגרף האובייקט ניתנות לסידור. ברוב המקרים, זה היה צריך להיעשות כבר; אם לא, זה צריך להיות קל יחסית לגישה לקוד המקור. רוב השיעורים ב- JDK ניתנים לסידור; רק אלה שתלויים בפלטפורמה, כגון FileDescriptor, אינם. כמו כן, כל השיעורים שתקבל מספק צד שלישי התואמים ל- JavaBean הם בהגדרה מסודרים. כמובן שאם אתה מאריך כיתה שניתנת לסידור, אז ניתן למחלק את המחלקה החדשה. כשכל השיעורים האלה מסודרים צפים, רוב הסיכויים שהיחידים שתצטרכו לבצע סדרת סדר הם שלך, וזו חתיכת עוגה בהשוואה לעוברים בכל שיעור ומחליפיםclone() לעשות העתק עמוק.

דרך קלה לגלות אם יש לך מחלקות שאינן ניתנות לשינוי בגרף של אובייקט היא להניח שכולן ניתנות לסידור ולהפעיל ObjectClonerאת deepCopy()השיטה. אם יש אובייקט שהמחלקה שלו אינה ניתנת לסידרה, אזי java.io.NotSerializableExceptionייזרק ויגיד לך איזה מחלקה גרמה לבעיה.

דוגמה ליישום מהיר מוצגת להלן. זה יוצר אובייקט פשוט v1, שהוא Vectorמכיל a Point. אובייקט זה מודפס לאחר מכן כדי להציג את תוכנו. האובייקט המקורי,, v1מועתק לאחר מכן לאובייקט חדש vNew, אשר מודפס כדי להראות שהוא מכיל את אותו הערך כמו v1. הבא, את תוכנו של v1משתנים, ולבסוף הן v1ו vNewמודפסים כך ניתן להשוות את הערכים שלהם.

ייבא java.util. *; ייבא java.awt. *; class class 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]; } אחר {System.out.println ("שימוש: java Driver1 [עמוק, רדוד]"); לַחֲזוֹר; } // צור אובייקט מקורי וקטור v1 = וקטור חדש (); נקודה p1 = נקודה חדשה (1,1); v1.addElement (p1); // ראה מה זה System.out.println ("Original =" + v1); וקטור vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // א} אחר אם (meth.equals ("רדוד")) {// עותק רדוד vNew = (וקטור) v1.clone (); // B} // ודא שזה אותו System.out.println ("חדש =" + vNew);// שנה את תוכן האובייקט המקורי p1.x = 2; p1.y = 2; // ראה מה יש בכל אחד עכשיו System.out.println ("Original =" + v1); System.out.println ("חדש =" + vNew); } לתפוס (Exception e) {System.out.println ("Exception in main =" + e); }}}

כדי להפעיל את העותק העמוק (שורה A), בצע 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שנוצר כתוצאה העותק עמוק נותרה בעינה, שכן הגרף כולו הועתק. לשם השוואה, הפעל את העותק הרדוד (שורה B) על ידי ביצוע 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]] 

זה מראה שכאשר שונה המקור Point, גם החדש Pointשונה. זאת בשל העובדה שהעותק הרדוד מייצר העתקים רק של ההפניות, ולא של האובייקטים אליהם הם מתייחסים. זו דוגמה מאוד פשוטה, אבל אני חושב שהיא ממחישה את הנקודה.

בעיות יישום

עכשיו, לאחר שהטיפתי על כל המעלות של העתקה עמוקה באמצעות סדרת סדר, בואו נסתכל על כמה דברים שיש להיזהר מהם.

המקרה הבעייתי הראשון הוא מחלקה שאינה ניתנת לסידור ואינה ניתנת לעריכה. זה יכול לקרות, למשל, אם אתה משתמש בכיתה של צד שלישי שאינה מצורפת עם קוד המקור. במקרה זה תוכלו להרחיב אותו, לבצע את המימוש המעמד המורחב Serializable, להוסיף כל (או את כל) הבונים הדרושים שפשוט קוראים לבניין העל המשויך, ולהשתמש במחלקה החדשה הזו בכל מקום שעשיתם את הישן (הנה דוגמה לכך).

זה אולי נראה כמו הרבה עבודה, אבל אלא אם כן השיטה המקורית של המחלקה clone()מיישמת עותק עמוק, בכל מקרה תעשה משהו דומה כדי לעקוף את clone()השיטה שלה .

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 ואינטרנט. הוא עבד עבור חברות כמו יוז, יבמ, נורטל ו- MCIWorldcom בפרויקטים מונחים עצמים, ועבד אך ורק עם Java בשלוש השנים האחרונות.

למידע נוסף על נושא זה

  • באתר Java של Sun יש קטע המוקדש למפרט Java Object Serialization

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

סיפור זה, "טיפ ג'אווה 76: אלטרנטיבה לטכניקת העתקה עמוקה" פורסם במקור על ידי JavaWorld.