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

Βασικός κωδικός hash Java και ισούται με επιδείξεις

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

Επειδή όλα τα αντικείμενα Java τελικά κληρονομούν εφαρμογές για ισούται με (αντικείμενο) και hashCode (), ο μεταγλωττιστής Java και ο εκκινητής χρόνου εκτέλεσης Java δεν θα αναφέρουν κανένα πρόβλημα κατά την επίκληση αυτών των "προεπιλεγμένων εφαρμογών" αυτών των μεθόδων. Δυστυχώς, όταν απαιτούνται αυτές οι μέθοδοι, οι προεπιλεγμένες υλοποιήσεις αυτών των μεθόδων (όπως ο ξάδερφος τους η μέθοδος toString) σπάνια είναι οι επιθυμητές. Η τεκμηρίωση API που βασίζεται σε Javadoc για την κλάση Object συζητά το "συμβόλαιο" που αναμένεται για οποιαδήποτε εφαρμογή του ισούται με (αντικείμενο) και hashCode () μεθόδους και συζητά επίσης την πιθανή προεπιλεγμένη εφαρμογή καθενός εάν δεν παρακαμφθεί από τις τάξεις παιδιών.

Για τα παραδείγματα σε αυτήν την ανάρτηση, θα χρησιμοποιήσω την κλάση HashAndEquals της οποίας η λίστα κωδικών εμφανίζεται δίπλα σε επεξεργασία αντικειμένων από διάφορες κλάσεις ατόμων με διαφορετικά επίπεδα υποστήριξης για hashCode και ισούται μεθόδους.

HashAndEquals.java

πακέτο dustin.example; εισαγωγή java.util.HashSet; εισαγωγή java.util.Set; εισαγωγή στατικού java.lang.System.out; δημόσια τάξη HashAndEquals {private static final String HEADER_SEPARATOR = "======================================= =============================== "; ιδιωτικό στατικό τελικό int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length (); ιδιωτικό στατικό τελικό String NEW_LINE = System.getProperty ("line.separator"); ιδιωτικό τελικό Person1 = νέο άτομο ("Flintstone", "Fred"); ιδιωτικό τελικό Person2 = νέο άτομο ("Rubble", "Barney"); ιδιωτικό τελικό Person3 = νέο άτομο ("Flintstone", "Fred"); ιδιωτικό τελικό Person4 = νέο άτομο ("Rubble", "Barney"); public void displayContents () {printHeader ("ΤΟ ΠΕΡΙΕΧΟΜΕΝΟ ΤΩΝ ΑΝΤΙΚΕΙΜΕΝΩΝ"); out.println ("Πρόσωπο 1:" + άτομο1); out.println ("Πρόσωπο 2:" + άτομο2); out.println ("Πρόσωπο 3:" + άτομο3); out.println ("Άτομο 4:" + άτομο4); } public void membandingkanEquality () {printHeader ("ΣΥΓΚΡΙΣΕΙΣ ΙΣΟΤΗΤΑΣ"); out.println ("Person1.equals (Person2):" + person1.equals (άτομο2)); out.println ("Person1.equals (Person3):" + person1.equals (άτομο3)); out.println ("Person2.equals (Person4):" + person2.equals (άτομο4)); } public void membandingkanHashCodes () {printHeader ("ΣΥΓΚΡΙΣΗ ΚΩΔΙΚΩΝ ΚΑΘΑΡΙΣΜΟΥ"); out.println ("Person1.hashCode ():" + person1.hashCode ()); out.println ("Person2.hashCode ():" + person2.hashCode ()); out.println ("Person3.hashCode ():" + person3.hashCode ()); out.println ("Person4.hashCode ():" + person4.hashCode ()); } Δημόσιο σύνολο addToHashSet () {printHeader ("ΠΡΟΣΘΗΚΗ ΣΤΟΙΧΕΙΩΝ ΣΕ ΡΥΘΜΙΣΗ - ΠΡΟΣΘΗΚΗ Ή ΕΙΝΑΙ Ο ίδιος;"); final Set set = νέο HashSet (); out.println ("Set.add (Person1):" + set.add (person1)); out.println ("Set.add (Person2):" + set.add (person2)); out.println ("Set.add (Person3):" + set.add (person3)); out.println ("Set.add (Person4):" + set.add (person4)); σύνολο επιστροφής; } public void removeFromHashSet (final Set sourceSet) {printHeader ("ΑΦΑΙΡΕΣΤΕ ΤΑ ΣΤΟΙΧΕΙΑ ΑΠΟ ΤΟ ΣΕΤ - ΜΠΟΡΟΥΝ ΝΑ ΒΡΕΧΟΝΤΑΙ ΓΙΑ ΝΑ ΑΦΑΙΡΟΥΝΤΑΙ;"); out.println ("Set.remove (Person1):" + sourceSet.remove (άτομο1)); out.println ("Set.remove (Person2):" + sourceSet.remove (person2)); out.println ("Set.remove (Person3):" + sourceSet.remove (person3)); out.println ("Set.remove (Person4):" + sourceSet.remove (person4)); } public static void printHeader (final String headerText) {out.println (NEW_LINE); out.println (HEADER_SEPARATOR); out.println ("=" + headerText); out.println (HEADER_SEPARATOR); } public static void main (final επιχειρήματα String []) {final HashAndEquals instance = new HashAndEquals (); instance.displayContents (); instance.compareEquality (); instance.compareHashCodes (); final Set set = instance.addToHashSet (); out.println ("Ορισμός πριν από την κατάργηση:" + set); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (σύνολο); out.println ("Set After Removals:" + set); }} 

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

Χωρίς ρητή ισούται ή hashCode Μέθοδοι

Η πρώτη έκδοση του Πρόσωπο Η κλάση δεν παρέχει μια ρητή παράκαμψη έκδοσης των δύο ισούται μέθοδος ή το hashCode μέθοδος. Αυτό θα δείξει την "προεπιλεγμένη εφαρμογή" καθεμιάς από αυτές τις μεθόδους που κληρονομήθηκαν από Αντικείμενο. Εδώ είναι ο πηγαίος κώδικας για Πρόσωπο χωρίς hashCode ή ισούται παρακάμπτεται ρητά.

Person.java (δεν υπάρχει ρητός κωδικός hashCode ή ισούται με)

πακέτο dustin.example; Πρόσωπο δημόσιας τάξης {private final String lastName; ιδιωτικό τελικό String firstName; δημόσιο πρόσωπο (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Αυτή η πρώτη έκδοση του Πρόσωπο δεν παρέχει μεθόδους get / set και δεν παρέχει ισούται ή hashCode υλοποιήσεις. Όταν η κύρια τάξη επίδειξης HashAndEquals εκτελείται με περιπτώσεις αυτού ισούται- χωρίς και hashCode-πιο λιγο Πρόσωπο τάξη, τα αποτελέσματα εμφανίζονται όπως φαίνεται στο επόμενο στιγμιότυπο οθόνης.

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

Η μέθοδος ισούται με την κατηγορία αντικειμένου εφαρμόζει την πιο διακριτική πιθανή σχέση ισοδυναμίας σε αντικείμενα. Δηλαδή, για οποιεσδήποτε μη μηδενικές τιμές αναφοράς x και y, αυτή η μέθοδος επιστρέφει true εάν και μόνο εάν x και y αναφέρονται στο ίδιο αντικείμενο (x == y έχει την τιμή true).

Μια δεύτερη παρατήρηση από αυτό το πρώτο παράδειγμα είναι ότι ο κωδικός κατακερματισμού είναι διαφορετικός για κάθε παρουσία του Πρόσωπο αντικείμενο ακόμη και όταν δύο παρουσίες μοιράζονται τις ίδιες τιμές για όλα τα χαρακτηριστικά τους. Το HashSet επιστρέφει αληθής όταν προστίθεται ένα "μοναδικό" αντικείμενο (HashSet.add) στο σύνολο ή ψευδής εάν το προστιθέμενο αντικείμενο δεν θεωρείται μοναδικό και δεν προστίθεται. Ομοίως, το HashSetεπιστρέφει μέθοδο επιστροφής αληθής εάν το παρεχόμενο αντικείμενο θεωρείται ότι βρέθηκε και αφαιρέθηκε ή ψευδής εάν το καθορισμένο αντικείμενο θεωρείται ότι δεν αποτελεί μέρος του HashSet και έτσι δεν μπορεί να αφαιρεθεί. Επειδή η ισούται και hashCode Οι κληρονομικές προεπιλεγμένες μέθοδοι αντιμετωπίζουν αυτές τις περιπτώσεις ως εντελώς διαφορετικές, δεν αποτελεί έκπληξη το γεγονός ότι όλα προστίθενται στο σύνολο και όλα αφαιρούνται με επιτυχία από το σετ.

Σαφής ισούται Μόνο μέθοδος

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

Person.java (παρέχεται ρητή ισοδύναμη μέθοδος)

πακέτο dustin.example; Πρόσωπο δημόσιας τάξης {private final String lastName; ιδιωτικό τελικό String firstName; δημόσιο πρόσωπο (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public boolean ισούται με (Object obj) {if (obj == null) {return false; } εάν (αυτό == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {επιστροφή false; } τελικό άτομο άλλο = (άτομο) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {επιστροφή false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {επιστροφή false; } επιστροφή αληθινή; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Όταν υπάρχουν τέτοιες περιπτώσεις Πρόσωπο με ισούται με (αντικείμενο) χρησιμοποιούνται ρητά, η έξοδος είναι όπως φαίνεται στο επόμενο στιγμιότυπο οθόνης.

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

Σαφής ισούται και hashCode Μέθοδοι

Ήρθε η ώρα να προσθέσετε ένα ρητό hashCode () μέθοδος στο Πρόσωπο τάξη. Πράγματι, αυτό θα έπρεπε πραγματικά να είχε γίνει όταν το ισούται εφαρμόστηκε η μέθοδος. Ο λόγος για αυτό αναφέρεται στην τεκμηρίωση για το Object.equals (Αντικείμενο) μέθοδος:

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

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

Person.java (ρητά ισούται με και hashCode υλοποιήσεις)

πακέτο dustin.example; Πρόσωπο δημόσιας τάξης {private final String lastName; ιδιωτικό τελικό String firstName; δημόσιο πρόσωπο (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } @Override public boolean ισούται με (Object obj) {if (obj == null) {return false; } εάν (αυτό == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {επιστροφή false; } τελικό άτομο άλλο = (άτομο) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {επιστροφή false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {επιστροφή false; } επιστροφή αληθινή; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Η έξοδος από την εκτέλεση με το νέο Πρόσωπο τάξη με hashCode και ισούται οι μέθοδοι εμφανίζονται στη συνέχεια.

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

Το πρόβλημα με τα μεταβλητά χαρακτηριστικά hashCode

Για το τέταρτο και τελευταίο παράδειγμα σε αυτήν την ανάρτηση, κοιτάζω τι συμβαίνει όταν το hashCode Η υλοποίηση βασίζεται σε ένα χαρακτηριστικό που αλλάζει. Για αυτό το παράδειγμα, α setFirstName προστίθεται μέθοδος στο Πρόσωπο και το τελικός ο τροποποιητής αφαιρείται από το όνομα Χαρακτηριστικό. Επιπλέον, η κύρια κατηγορία HashAndEquals πρέπει να αφαιρέσει το σχόλιο από τη γραμμή που επικαλείται αυτήν τη νέα μέθοδο καθορισμού. Η νέα έκδοση του Πρόσωπο εμφανίζεται στη συνέχεια.

πακέτο dustin.example; Πρόσωπο δημόσιας τάξης {private final String lastName; ιδιωτικό όνομα συμβολοσειράς; δημόσιο πρόσωπο (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } public void setFirstName (final String newFirstName) {this.firstName = newFirstName; } @Override public boolean ισούται με (Object obj) {if (obj == null) {return false; } εάν (αυτό == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {επιστροφή false; } τελικό άτομο άλλο = (άτομο) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {επιστροφή false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {επιστροφή false; } επιστροφή αληθινή; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

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