Προγραμματισμός

Java Tip 76: Μια εναλλακτική λύση στην τεχνική deep copy

Η εφαρμογή ενός βαθιού αντιγράφου ενός αντικειμένου μπορεί να είναι μια μαθησιακή εμπειρία - μαθαίνετε ότι δεν θέλετε να το κάνετε! Εάν το εν λόγω αντικείμενο αναφέρεται σε άλλα πολύπλοκα αντικείμενα, τα οποία με τη σειρά τους αναφέρονται σε άλλα, τότε αυτή η εργασία μπορεί πράγματι να είναι τρομακτική. Παραδοσιακά, κάθε κλάση στο αντικείμενο πρέπει να επιθεωρείται ξεχωριστά και να επεξεργάζεται για να εφαρμόσει το Κλωνοποιήσιμο διασύνδεση και παράκαμψη του κλώνος () μέθοδος για να δημιουργήσετε ένα βαθύ αντίγραφο του εαυτού του καθώς και τα αντικείμενα που περιέχει. Αυτό το άρθρο περιγράφει μια απλή τεχνική στη χρήση αυτού του χρονοβόρου συμβατικού αντιγράφου.

Η έννοια του αντιγράφου σε βάθος

Για να καταλάβουμε τι α βαθύ αντίγραφο είναι, ας δούμε πρώτα την έννοια της ρηχής αντιγραφής.

Σε προηγούμενο JavaWorld άρθρο, "Πώς να αποφύγετε τις παγίδες και να παρακάμψετε σωστά τις μεθόδους από το java.lang.Object", ο Mark Roulo εξηγεί πώς να κλωνοποιήσετε αντικείμενα καθώς και πώς να επιτύχετε ρηχή αντιγραφή αντί για βαθιά αντιγραφή. Για να συνοψίσουμε εν συντομία εδώ, ένα ρηχό αντίγραφο εμφανίζεται όταν ένα αντικείμενο αντιγράφεται χωρίς τα περιεχόμενα αντικείμενα. Για παράδειγμα, το σχήμα 1 δείχνει ένα αντικείμενο, obj1, που περιέχει δύο αντικείμενα, περιέχεταιObj1 και περιέχεταιObj2.

Εάν εκτελείται ρηχό αντίγραφο στις obj1, τότε αντιγράφεται, αλλά τα περιεχόμενα αντικείμενα δεν είναι, όπως φαίνεται στο Σχήμα 2.

Ένα βαθύ αντίγραφο εμφανίζεται όταν ένα αντικείμενο αντιγράφεται μαζί με τα αντικείμενα στα οποία αναφέρεται. Το σχήμα 3 δείχνει obj1 αφού εκτελεστεί σε βάθος αντίγραφο σε αυτό. Όχι μόνο έχει obj1 έχουν αντιγραφεί, αλλά έχουν αντιγραφεί και τα αντικείμενα που περιέχονται σε αυτό.

Εάν ένα από αυτά τα αντικείμενα περιέχει αντικείμενα, τότε, σε ένα βαθύ αντίγραφο, αντιγράφονται και αυτά, και ούτω καθεξής έως ότου ολόκληρο το γράφημα διασταυρωθεί και αντιγραφεί. Κάθε αντικείμενο είναι υπεύθυνο για την κλωνοποίηση μέσω του κλώνος () μέθοδος. Η προεπιλεγμένη κλώνος () μέθοδο, κληρονομική από Αντικείμενο, δημιουργεί ένα ρηχό αντίγραφο του αντικειμένου. Για να αποκτήσετε ένα βαθύ αντίγραφο, πρέπει να προστεθεί επιπλέον λογική που καλεί ρητά όλα τα αντικείμενα που περιέχονται » κλώνος () μεθόδους, οι οποίες με τη σειρά τους αποκαλούν τα περιεχόμενα αντικείμενα τους κλώνος () μεθόδους και ούτω καθεξής. Η σωστή λήψη μπορεί να είναι δύσκολη και χρονοβόρα και σπάνια είναι διασκεδαστική. Για να κάνουμε τα πράγματα ακόμη πιο περίπλοκα, εάν ένα αντικείμενο δεν μπορεί να τροποποιηθεί άμεσα και το κλώνος () μέθοδος παράγει ένα ρηχό αντίγραφο, τότε η κλάση πρέπει να επεκταθεί, το κλώνος () η μέθοδος αντικαταστάθηκε, και αυτή η νέα τάξη χρησιμοποιήθηκε στη θέση του παλιού. (Για παράδειγμα, Διάνυσμα δεν περιέχει τη λογική που είναι απαραίτητη για ένα αντίγραφο σε βάθος.) Και αν θέλετε να γράψετε κώδικα που προστατεύει μέχρι το χρόνο εκτέλεσης το ερώτημα αν θα κάνετε ένα βαθύ ή ρηχό αντίγραφο ενός αντικειμένου, βρίσκεστε σε μια ακόμη πιο περίπλοκη κατάσταση. Σε αυτήν την περίπτωση, πρέπει να υπάρχουν δύο λειτουργίες αντιγραφής για κάθε αντικείμενο: μία για ένα βαθύ αντίγραφο και μία για ένα ρηχό. Τέλος, ακόμη και αν το αντικείμενο που αντιγράφεται σε βάθος περιέχει πολλές αναφορές σε άλλο αντικείμενο, το τελευταίο αντικείμενο θα πρέπει να αντιγραφεί μόνο μία φορά. Αυτό αποτρέπει τον πολλαπλασιασμό των αντικειμένων και ξεφεύγει από την ειδική κατάσταση στην οποία μια κυκλική αναφορά παράγει έναν άπειρο βρόχο αντιγράφων.

Σειριοποίηση

Τον Ιανουάριο του 1998, JavaWorld ξεκίνησε το JavaBeans στήλη του Mark Johnson με ένα άρθρο σχετικά με τη σειριοποίηση, "Κάνε τον τρόπο" Nescafé "- με λυοφιλιωμένα JavaBeans." Συνοψίζοντας, η σειριοποίηση είναι η δυνατότητα μετατροπής ενός γραφήματος αντικειμένων (συμπεριλαμβανομένης της εκφυλισμένης περίπτωσης ενός μεμονωμένου αντικειμένου) σε μια σειρά byte που μπορούν να μετατραπούν ξανά σε ισοδύναμο γράφημα αντικειμένων. Ένα αντικείμενο λέγεται ότι μπορεί να σειριοποιηθεί εάν το εφαρμόσει ή ένας από τους προγόνους του java.io.Serializable ή java.io. Εξαιρέσιμο. Ένα σειριοποιήσιμο αντικείμενο μπορεί να σειριοποιηθεί μεταβιβάζοντάς το στο writeObject () μέθοδος ενός ObjectOutputStream αντικείμενο. Αυτό γράφει τους πρωτόγονους τύπους δεδομένων του αντικειμένου, τους πίνακες, τις συμβολοσειρές και άλλες αναφορές αντικειμένων. ο writeObject () Στη συνέχεια καλείται μέθοδος στα αναφερόμενα αντικείμενα για να τα σειριοποιήσουν επίσης. Επιπλέον, κάθε ένα από αυτά τα αντικείμενα έχει δικα τους σειριακές αναφορές και αντικείμενα · αυτή η διαδικασία συνεχίζεται και συνεχίζεται έως ότου ολόκληρο το γράφημα διασταυρωθεί και σειριοποιηθεί. Αυτό ακούγεται οικείο; Αυτή η λειτουργικότητα μπορεί να χρησιμοποιηθεί για να επιτευχθεί ένα βαθύ αντίγραφο.

Βαθύ αντίγραφο χρησιμοποιώντας σειριοποίηση

Τα βήματα για τη δημιουργία ενός αντιγράφου σε βάθος χρησιμοποιώντας τη σειριοποίηση είναι:

  1. Βεβαιωθείτε ότι όλες οι τάξεις στο γράφημα του αντικειμένου είναι σειριοποιήσιμες.

  2. Δημιουργήστε ροές εισόδου και εξόδου.

  3. Χρησιμοποιήστε τις ροές εισόδου και εξόδου για να δημιουργήσετε ροές εισόδου αντικειμένου και εξόδου αντικειμένου.

  4. Περάστε το αντικείμενο που θέλετε να αντιγράψετε στη ροή εξόδου αντικειμένου.

  5. Διαβάστε το νέο αντικείμενο από τη ροή εισόδου αντικειμένου και μεταφέρετέ το πίσω στην κλάση του αντικειμένου που στείλατε.

Έχω γράψει μια τάξη που ονομάζεται ObjectCloner που εφαρμόζει τα βήματα δύο έως πέντε. Η γραμμή με την ένδειξη "A" δημιουργεί ένα ByteArrayOutputStream που χρησιμοποιείται για τη δημιουργία του ObjectOutputStream στη γραμμή Β. Η γραμμή Γ είναι εκεί που γίνεται η μαγεία. ο writeObject () Η μέθοδος διασχίζει αναδρομικά το γράφημα του αντικειμένου, δημιουργεί ένα νέο αντικείμενο σε μορφή byte και το στέλνει στο ByteArrayOutputStream. Η γραμμή D διασφαλίζει ότι έχει αποσταλεί ολόκληρο το αντικείμενο. Ο κωδικός στη γραμμή Ε δημιουργεί τότε ένα ByteArrayInputStream και το συμπληρώνει με τα περιεχόμενα του ByteArrayOutputStream. Η γραμμή F δημιουργεί ένα ObjectInputStream χρησιμοποιώντας το ByteArrayInputStream δημιουργήθηκε στη γραμμή Ε και το αντικείμενο αποστειρώνεται και επιστρέφεται στη μέθοδο κλήσης στη γραμμή Ζ. Εδώ είναι ο κωδικός:

εισαγωγή java.io. *; εισαγωγή java.util. *; εισαγωγή java.awt. *; δημόσια κλάση ObjectCloner {// έτσι ώστε κανείς να μην μπορεί να δημιουργήσει κατά λάθος ένα αντικείμενο ObjectCloner ιδιωτικό ObjectCloner () {} // επιστρέφει ένα βαθύ αντίγραφο ενός αντικειμένου στατικού δημόσιου αντικειμένου deepCopy (Object oldObj) ρίχνει την εξαίρεση {ObjectOutputStream oos = null; ObjectInputStream ois = null; δοκιμάστε το {ByteArrayOutputStream bos = νέο ByteArrayOutputStream (); // A oos = νέο ObjectOutputStream (bos); // B // σειριοποιήστε και περάστε το αντικείμενο oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = νέο ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (κάδος); // F // επιστροφή του νέου αντικειμένου Return ois.readObject (); // G} catch (Εξαίρεση e) {System.out.println ("Exception in ObjectCloner =" + e); ρίξτε (ε); } τέλος {oos.close (); ois.close (); }}} 

Όλοι οι προγραμματιστές με πρόσβαση σε ObjectCloner μένει να κάνουμε προτού εκτελέσετε αυτόν τον κώδικα, βεβαιωθείτε ότι όλες οι κλάσεις στο γράφημα του αντικειμένου είναι σειριοποιήσιμες. Στις περισσότερες περιπτώσεις, αυτό θα έπρεπε να έχει ήδη γίνει. Αν όχι, θα πρέπει να είναι σχετικά εύκολο με την πρόσβαση στον πηγαίο κώδικα. Τα περισσότερα από τα μαθήματα στο JDK είναι σειριοποιήσιμα. μόνο αυτά που εξαρτώνται από την πλατφόρμα, όπως Περιγραφέας αρχείων, δεν είναι. Επίσης, οποιεσδήποτε τάξεις που λαμβάνετε από έναν τρίτο προμηθευτή που είναι συμβατές με JavaBean είναι εξ ορισμού σειριοποιήσιμες. Φυσικά, εάν επεκτείνετε μια κλάση που είναι σειριοποιήσιμη, τότε η νέα τάξη είναι επίσης σειριοποιήσιμη. Με όλες αυτές τις σειρές που μπορούν να σειριοποιηθούν, οι πιθανότητες είναι ότι οι μόνες που ίσως χρειαστεί να κάνετε σειριοποίηση είναι οι δικές σας και αυτό είναι ένα κομμάτι κέικ σε σύγκριση με το να περάσετε σε κάθε τάξη και να αντικαταστήσετε κλώνος () για να κάνετε ένα βαθύ αντίγραφο.

Ένας εύκολος τρόπος για να μάθετε εάν έχετε οποιεσδήποτε μη εναέριες κατηγορίες στο γράφημα ενός αντικειμένου είναι να υποθέσετε ότι όλες είναι σειριοποιήσιμες και εκτελούνται ObjectCloner'μικρό deepCopy () μέθοδο σε αυτό. Εάν υπάρχει ένα αντικείμενο του οποίου η κλάση δεν είναι σειριοποιήσιμη, τότε a java.io.NotSerializableException θα πεταχτεί, θα σας πει ποια τάξη προκάλεσε το πρόβλημα.

Ένα γρήγορο παράδειγμα εφαρμογής φαίνεται παρακάτω. Δημιουργεί ένα απλό αντικείμενο, v1, το οποίο είναι ένα Διάνυσμα που περιέχει ένα Σημείο. Αυτό το αντικείμενο εκτυπώνεται στη συνέχεια για να δείξει το περιεχόμενό του. Το αρχικό αντικείμενο, v1, στη συνέχεια αντιγράφεται σε ένα νέο αντικείμενο, vΝέα, το οποίο εκτυπώνεται για να δείξει ότι περιέχει την ίδια τιμή με v1. Στη συνέχεια, το περιεχόμενο του v1 αλλάζουν και τελικά και τα δύο v1 και vΝέα εκτυπώνονται έτσι ώστε να μπορούν να συγκριθούν οι τιμές τους.

εισαγωγή java.util. *; εισαγωγή java.awt. *; δημόσια κλάση Driver1 {static public void main (String [] args) {try {// get the method from the command line String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("ρηχό")))) {meth = args [0]; } αλλιώς {System.out.println ("Χρήση: java Driver1 [deep, ρηχό]"); ΕΠΙΣΤΡΟΦΗ; } // δημιουργία αρχικού αντικειμένου Vector v1 = νέο διάνυσμα (); Σημείο p1 = νέο σημείο (1,1) · v1.addElement (σελ1); // δείτε τι είναι το System.out.println ("Original =" + v1); Διάνυσμα vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} αλλιώς εάν (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 ("Νέο =" + vΝέα); } catch (Εξαίρεση e) {System.out.println ("Exception in main =" + e); }}} 

Για να καλέσετε το αντίγραφο σε βάθος (γραμμή Α), εκτελέστε java.exe Πρόγραμμα οδήγησης1 βαθιά. Όταν εκτελείται το deep copy, λαμβάνουμε την ακόλουθη εκτύπωση:

Αρχικό = [java.awt.Point [x = 1, y = 1]] Νέο = [java.awt.Point [x = 1, y = 1]] Original = [java.awt.Point [x = 2, y = 2]] Νέο = [java.awt.Point [x = 1, y = 1]] 

Αυτό δείχνει ότι όταν το πρωτότυπο Σημείο, σελ. 1, άλλαξε, το νέο Σημείο δημιουργήθηκε ως αποτέλεσμα του αντιγράφου σε βάθος παρέμεινε ανεπηρέαστο, καθώς αντιγράφηκε ολόκληρο το γράφημα. Για σύγκριση, χρησιμοποιήστε το ρηχό αντίγραφο (γραμμή Β) εκτελώντας java.exe Πρόγραμμα οδήγησης1 ρηχό. Όταν εκτελείται το ρηχό αντίγραφο, λαμβάνουμε την ακόλουθη εκτύπωση:

Αρχικό = [java.awt.Point [x = 1, y = 1]] Νέο = [java.awt.Point [x = 1, y = 1]] Original = [java.awt.Point [x = 2, y = 2]] Νέο = [java.awt.Point [x = 2, y = 2]] 

Αυτό δείχνει ότι όταν το πρωτότυπο Σημείο άλλαξε, το νέο Σημείο άλλαξε επίσης. Αυτό οφείλεται στο γεγονός ότι το ρηχό αντίγραφο δημιουργεί αντίγραφα μόνο των αναφορών και όχι των αντικειμένων στα οποία αναφέρονται. Αυτό είναι ένα πολύ απλό παράδειγμα, αλλά νομίζω ότι απεικονίζει το, um, σημείο.

Θέματα εφαρμογής

Τώρα που έχω κηρύξει για όλες τις αρετές του deep copy χρησιμοποιώντας τη σειριοποίηση, ας δούμε μερικά πράγματα που πρέπει να προσέξουμε.

Η πρώτη προβληματική περίπτωση είναι μια κλάση που δεν είναι σειριοποιήσιμη και δεν μπορεί να επεξεργαστεί. Αυτό θα μπορούσε να συμβεί, για παράδειγμα, εάν χρησιμοποιείτε τάξη τρίτου μέρους που δεν συνοδεύεται από τον πηγαίο κώδικα. Σε αυτήν την περίπτωση μπορείτε να το επεκτείνετε, να εκτελέσετε την εκτεταμένη κλάση Σειριοποιήσιμο, προσθέστε οποιονδήποτε (ή όλους) απαραίτητους κατασκευαστές που απλά καλούν τον σχετικό υπερκατασκευαστή και χρησιμοποιήστε αυτήν τη νέα τάξη οπουδήποτε κάνατε την παλιά (εδώ είναι ένα παράδειγμα αυτού).

Αυτό μπορεί να φαίνεται σαν πολλή δουλειά, αλλά, εκτός αν η αρχική τάξη κλώνος () Η μέθοδος εφαρμόζει σε βάθος αντίγραφο, θα κάνετε κάτι παρόμοιο για να το παρακάμψετε κλώνος () ούτως ή άλλως.

Το επόμενο ζήτημα είναι η ταχύτητα εκτέλεσης αυτής της τεχνικής. Όπως μπορείτε να φανταστείτε, η δημιουργία μιας πρίζας, η σειριοποίηση ενός αντικειμένου, η διέλευσή του από την πρίζα και, στη συνέχεια, η αφαλάτωση είναι αργή σε σύγκριση με τις μεθόδους κλήσης σε υπάρχοντα αντικείμενα. Ακολουθεί ορισμένος πηγαίος κώδικας που μετρά το χρόνο που απαιτείται για να κάνει και τις δύο μεθόδους αντιγραφής (μέσω σειριοποίησης και κλώνος ()) σε ορισμένες απλές τάξεις και παράγει σημεία αναφοράς για διαφορετικούς αριθμούς επαναλήψεων. Τα αποτελέσματα, που εμφανίζονται σε χιλιοστά του δευτερολέπτου, βρίσκονται στον παρακάτω πίνακα:

Χιλιοστά του δευτερολέπτου για αντιγραφή σε βάθος ενός απλού γραφήματος κλάσης n φορές
Διαδικασία \ Επαναλήψεις (n)100010000100000
κλώνος10101791
σειριοποίηση183211346107725

Όπως μπορείτε να δείτε, υπάρχει μεγάλη διαφορά στην απόδοση. Εάν ο κώδικας που γράφετε είναι κρίσιμος για την απόδοση, τότε ίσως χρειαστεί να δαγκώσετε τη σφαίρα και να γράψετε με το χέρι ένα βαθύ αντίγραφο. Εάν έχετε ένα πολύπλοκο γράφημα και σας δοθεί μια μέρα για να εφαρμόσετε ένα αντίγραφο σε βάθος και ο κώδικας θα εκτελεστεί ως εργασία παρτίδας μία το πρωί τις Κυριακές, τότε αυτή η τεχνική σας δίνει μια άλλη επιλογή να λάβετε υπόψη.

Ένα άλλο ζήτημα αφορά την περίπτωση μιας κλάσης της οποίας οι παρουσίες των αντικειμένων μέσα σε μια εικονική μηχανή πρέπει να ελέγχονται. Αυτή είναι μια ειδική περίπτωση του μοτίβου Singleton, στην οποία μια τάξη έχει μόνο ένα αντικείμενο μέσα σε ένα VM. Όπως συζητήθηκε παραπάνω, όταν κάνετε σειριοποίηση ενός αντικειμένου, δημιουργείτε ένα εντελώς νέο αντικείμενο που δεν θα είναι μοναδικό. Για να αντιμετωπίσετε αυτήν την προεπιλεγμένη συμπεριφορά μπορείτε να χρησιμοποιήσετε το readResolve () μέθοδος για να αναγκάσει τη ροή να επιστρέψει ένα κατάλληλο αντικείμενο και όχι αυτό που είχε σειριοποιηθεί. Σε αυτό ιδιαιτερος περίπτωση, το κατάλληλο αντικείμενο είναι το ίδιο με το σειριακό. Εδώ είναι ένα παράδειγμα του τρόπου εφαρμογής του readResolve () μέθοδος. Μπορείτε να μάθετε περισσότερα για readResolve () καθώς και άλλες λεπτομέρειες σειριοποίησης στον ιστότοπο της Sun που είναι αφιερωμένες στην προδιαγραφή Serialization Object Java (βλ. πόρους).

Ένα τελευταίο gotcha που πρέπει να προσέξετε είναι η περίπτωση μεταβατικών μεταβλητών. Εάν μια μεταβλητή έχει επισημανθεί ως παροδική, τότε δεν θα σειριοποιηθεί και επομένως δεν θα αντιγραφεί και το γράφημα της. Αντίθετα, η τιμή της μεταβατικής μεταβλητής στο νέο αντικείμενο θα είναι οι προεπιλεγμένες γλώσσες Java (null, false και zero). Δεν θα υπάρξουν σφάλματα compiletime ή runtime, τα οποία μπορεί να οδηγήσουν σε συμπεριφορά που είναι δύσκολο να εντοπιστεί. Το να γνωρίζετε αυτό μπορεί να εξοικονομήσει πολύ χρόνο.

Η τεχνική deep copy μπορεί να σώσει έναν προγραμματιστή πολλές ώρες εργασίας, αλλά μπορεί να προκαλέσει τα προβλήματα που περιγράφονται παραπάνω. Όπως πάντα, φροντίστε να σταθμίσετε τα πλεονεκτήματα και τα μειονεκτήματα πριν αποφασίσετε ποια μέθοδο θα χρησιμοποιήσετε.

συμπέρασμα

Η εφαρμογή σε βάθος αντιγράφου ενός σύνθετου γραφήματος αντικειμένων μπορεί να είναι μια δύσκολη εργασία. Η τεχνική που φαίνεται παραπάνω είναι μια απλή εναλλακτική λύση στη συμβατική διαδικασία αντικατάστασης του κλώνος () μέθοδος για κάθε αντικείμενο στο γράφημα.

Ο Dave Miller είναι ανώτερος αρχιτέκτονας της συμβουλευτικής εταιρείας Javelin Technology, όπου εργάζεται σε εφαρμογές Java και Internet. Έχει εργαστεί σε εταιρείες όπως οι Hughes, IBM, Nortel και MCIWorldcom σε αντικειμενοστραφή έργα και έχει εργαστεί αποκλειστικά με την Java τα τελευταία τρία χρόνια.

Μάθετε περισσότερα σχετικά με αυτό το θέμα

  • Ο ιστότοπος Java της Sun έχει μια ενότητα αφιερωμένη στην Προδιαγραφή σειριοποίησης αντικειμένων Java

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

Αυτή η ιστορία, "Java Tip 76: Μια εναλλακτική λύση στην τεχνική deep copy" δημοσιεύθηκε αρχικά από την JavaWorld.