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

Επανάληψη συλλογών στην Java

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

Το μοτίβο επανάληψης

Ενα επαναληπτικό είναι ένας μηχανισμός που επιτρέπει σε όλα τα στοιχεία μιας συλλογής να έχουν διαδοχική πρόσβαση, με κάποια λειτουργία να εκτελείται σε κάθε στοιχείο. Ουσιαστικά, ένας επαναληπτής παρέχει ένα μέσο «βρόχου» πάνω σε μια ενθυλακωμένη συλλογή αντικειμένων. Παραδείγματα χρήσης επαναληπτών περιλαμβάνουν

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

Ορισμένες αρχές ισχύουν για τη χρήση των επαναληπτών: Σε γενικές γραμμές, θα πρέπει να μπορείτε να έχετε πολλαπλές διαβάσεις σε εξέλιξη ταυτόχρονα. δηλαδή, ένας επαναληπτής πρέπει να επιτρέπει την έννοια του ένθετο βρόχο. Ένας επαναληπτής θα πρέπει επίσης να μην είναι καταστρεπτικός υπό την έννοια ότι η πράξη επανάληψης δεν πρέπει, από μόνη της, να αλλάξει τη συλλογή. Φυσικά η λειτουργία που εκτελείται στα στοιχεία μιας συλλογής θα μπορούσε ενδεχομένως να αλλάξει μερικά από τα στοιχεία. Μπορεί επίσης να είναι δυνατό για έναν επαναληπτικό να υποστηρίζει την αφαίρεση ενός στοιχείου από μια συλλογή ή την εισαγωγή ενός νέου στοιχείου σε ένα συγκεκριμένο σημείο της συλλογής, αλλά τέτοιες αλλαγές θα πρέπει να είναι σαφείς εντός του προγράμματος και όχι υποπροϊόν της επανάληψης. Σε ορισμένες περιπτώσεις, θα πρέπει επίσης να έχετε επαναλήψεις με διαφορετικές μεθόδους διασταύρωσης. για παράδειγμα, διατομή προ-παραγγελίας και μετα-παραγγελία ενός δέντρου, ή διασταύρωση πρώτου βάθους και πρώτου πλάτους ενός γραφήματος.

Επανάληψη σύνθετων δομών δεδομένων

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

Τα σχέδια του Iterators and the Gang of Four

Σύμφωνα με τη συμμορία των τεσσάρων (βλ. Παρακάτω), το Πρότυπο σχεδίασης επανάληψης είναι ένα μοτίβο συμπεριφοράς, του οποίου η βασική ιδέα είναι "να αναλάβει την ευθύνη για την πρόσβαση και τη διέλευση από τη λίστα [εκδ. σκέψη συλλογή] αντικείμενο και βάλτε το σε ένα αντικείμενο επανάληψης. "Αυτό το άρθρο δεν αφορά τόσο πολύ το μοτίβο Iterator όσο αφορά τον τρόπο με τον οποίο χρησιμοποιούνται στην πράξη οι επαναληπτές. Για να καλύψετε πλήρως το μοτίβο θα απαιτούσε να συζητήσετε πώς θα σχεδιαζόταν ένας επαναληπτής, συμμετέχοντες ( αντικείμενα και τάξεις) στο σχεδιασμό, πιθανά εναλλακτικά σχέδια και ανταλλαγές διαφορετικών εναλλακτικών σχεδιασμών. Θα προτιμούσα να επικεντρωθώ στον τρόπο με τον οποίο χρησιμοποιούνται οι επαναληπτές στην πράξη, αλλά θα σας δείξω μερικούς πόρους για τη διερεύνηση του μοτίβου Iterator και των σχεδίων γενικά:

  • Σχέδια σχεδίασης: Στοιχεία επαναχρησιμοποιήσιμου αντικειμενοστραφούς λογισμικού (Addison-Wesley Professional, 1994) που γράφτηκε από τους Erich Gamma, Richard Helm, Ralph Johnson και John Vlissides (επίσης γνωστοί ως η συμμορία των τεσσάρων ή απλά GoF) είναι ο καθοριστικός πόρος για τη μάθηση σχετικά με τα σχέδια σχεδίασης. Αν και το βιβλίο εκδόθηκε για πρώτη φορά το 1994, παραμένει κλασικό, όπως αποδεικνύεται από το γεγονός ότι υπήρξαν περισσότερες από 40 εκτυπώσεις.
  • Ο Μπομπ Ταρ, λέκτορας στο Πανεπιστήμιο του Μέριλαντ της Κομητείας της Βαλτιμόρης, έχει μια εξαιρετική σειρά διαφανειών για το μάθημά του σχετικά με τα σχέδια σχεδίασης, συμπεριλαμβανομένης της εισαγωγής του στο πρότυπο Iterator
  • Η σειρά JavaWorld του David Geary Μοτίβα σχεδίασης Java παρουσιάζει πολλά από τα σχέδια του Gang of Four, συμπεριλαμβανομένων των μοτίβων Singleton, Observer και Composite. Επίσης στο JavaWorld, η πιο πρόσφατη επισκόπηση τριών μερών του Jeff Friesen σχετικά με τα μοτίβα σχεδίασης περιλαμβάνει έναν οδηγό για τα μοτίβα GoF

Ενεργά επαναληπτικά έναντι παθητικών επαναληπτών

Υπάρχουν δύο γενικές προσεγγίσεις για την εφαρμογή ενός επαναληπτικού προγράμματος ανάλογα με το ποιος ελέγχει την επανάληψη. Για ένα ενεργός επαναληπτικός (επίσης γνωστός ως ρητή επανάληψη ή εξωτερικός επαναληπτής), ο πελάτης ελέγχει την επανάληψη με την έννοια ότι ο πελάτης δημιουργεί τον επαναλήπτη, του λέει πότε πρέπει να προχωρήσει στο επόμενο στοιχείο, ελέγχει για να δει εάν κάθε στοιχείο έχει επισκεφθεί και ούτω καθεξής. Αυτή η προσέγγιση είναι κοινή σε γλώσσες όπως το C ++ και είναι η προσέγγιση που λαμβάνει τη μεγαλύτερη προσοχή στο βιβλίο GoF. Παρόλο που οι επαναληπτές στην Java έχουν λάβει διαφορετικές μορφές, η χρήση ενός ενεργού επαναληπτικού ήταν ουσιαστικά η μόνη βιώσιμη επιλογή πριν από την Java 8.

Για ένα παθητικός επαναληπτικός (επίσης γνωστό ως σιωπηρή επανάληψη, εσωτερικός επαναληπτής, ή επανάληψη επανάκλησης), ο ίδιος ο επαναληπτής ελέγχει την επανάληψη. Ο πελάτης ουσιαστικά λέει στον επαναληπτή, "εκτελέστε αυτήν τη λειτουργία στα στοιχεία της συλλογής." Αυτή η προσέγγιση είναι κοινή σε γλώσσες όπως το LISP που παρέχουν ανώνυμες λειτουργίες ή κλείσιμο. Με την κυκλοφορία του Java 8, αυτή η προσέγγιση επανάληψης είναι τώρα μια λογική εναλλακτική λύση για προγραμματιστές Java.

Σχέδια ονομασίας Java 8

Αν και δεν είναι τόσο άσχημα όσο τα Windows (NT, 2000, XP, VISTA, 7, 8, ...) Το ιστορικό εκδόσεων της Java περιλαμβάνει πολλά σχήματα ονομάτων. Αρχικά, πρέπει να αναφέρουμε την τυπική έκδοση Java ως "JDK", "J2SE" ή "Java SE"; Οι αριθμοί έκδοσης της Java ξεκίνησαν αρκετά απλοί - 1.0, 1.1 κ.λπ. - αλλά όλα άλλαξαν με την έκδοση 1.5, η οποία είχε την επωνυμία Java (ή JDK) 5. Όταν αναφέρομαι σε πρώιμες εκδόσεις της Java, χρησιμοποιώ φράσεις όπως "Java 1.0" ή "Java 1.1, "αλλά μετά την πέμπτη έκδοση του Java χρησιμοποιώ φράσεις όπως" Java 5 "ή" Java 8. "

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

Άλλες μορφές επανάληψης στην Java 8

Εστιάζω στην επανάληψη των συλλογών, αλλά υπάρχουν και άλλες, πιο εξειδικευμένες μορφές επανάληψης στην Java. Για παράδειγμα, μπορείτε να χρησιμοποιήσετε ένα JDBC Αποτέλεσμα για να επαναλάβετε τις σειρές που επιστρέφονται από ένα ερώτημα SELECT σε μια σχεσιακή βάση δεδομένων ή χρησιμοποιήστε ένα Ερευνητής για να επαναλάβετε μια πηγή εισόδου.

Επανάληψη με την κατηγορία απαρίθμησης

Στην Java 1.0 και 1.1, οι δύο κύριες κατηγορίες συλλογής ήταν Διάνυσμα και Hashtable, και το σχέδιο σχεδίασης Iterator εφαρμόστηκε σε μια κατηγορία που ονομάζεται Απαρίθμηση. Αναδρομικά, αυτό ήταν κακό όνομα για την τάξη. Μην συγχέετε την τάξη Απαρίθμηση με την έννοια του τύποι enum, που δεν εμφανίστηκε μέχρι την Java 5. Σήμερα και τα δύο Διάνυσμα και Hashtable είναι γενικές τάξεις, αλλά τότε τα γενικά δεν ήταν μέρος της γλώσσας Java. Ο κώδικας για την επεξεργασία ενός διανύσματος συμβολοσειρών χρησιμοποιώντας Απαρίθμηση θα μοιάζει με την Καταχώριση 1.

Λίστα 1. Χρησιμοποιώντας απαρίθμηση για να επαναλάβετε ένα διάνυσμα συμβολοσειρών

 Διανυσματικά ονόματα = νέο διάνυσμα (); // ... προσθέστε μερικά ονόματα στη συλλογή Enumeration e = names.elements (); ενώ (e.hasMoreElements ()) {String name = (String) e.nextElement (); System.out.println (όνομα); } 

Επανάληψη με την τάξη Iterator

Το Java 1.2 παρουσίασε τις κατηγορίες συλλογής που όλοι γνωρίζουμε και αγαπάμε και το σχέδιο σχεδίασης Iterator εφαρμόστηκε σε μια τάξη που ονομάστηκε κατάλληλα Επαναληπτής. Επειδή δεν είχαμε ακόμα γενικά στο Java 1.2, η μετάδοση ενός αντικειμένου επέστρεψε από ένα Επαναληπτής ήταν ακόμη απαραίτητο. Για εκδόσεις Java 1.2 έως 1.4, η επανάληψη μιας λίστας συμβολοσειρών ενδέχεται να μοιάζει με την Καταχώριση 2.

Λίστα 2. Χρήση Iterator για επανάληψη μιας λίστας συμβολοσειρών

 Λίστα ονομάτων = νέα LinkedList (); // ... προσθέστε μερικά ονόματα στη συλλογή Iterator i = names.iterator (); ενώ (i.hasNext ()) {String name = (String) i.next (); System.out.println (όνομα); } 

Επανάληψη με γενικά και το βελτιωμένο βρόχο

Η Java 5 μας έδωσε γενικά, τη διεπαφή Επαληθεύσιμοκαι το βελτιωμένο for-loop. Το βελτιωμένο for-loop είναι μία από τις αγαπημένες μου μικρές προσθήκες στην Java. Η δημιουργία του επαναληπτικού και καλεί σε αυτό hasNext () και Επόμενο() Οι μέθοδοι δεν εκφράζονται ρητά στον κώδικα, αλλά εξακολουθούν να πραγματοποιούνται πίσω από τα παρασκήνια. Έτσι, παρόλο που ο κώδικας είναι πιο συμπαγής, εξακολουθούμε να χρησιμοποιούμε έναν ενεργό επαναληπτικό. Χρησιμοποιώντας το Java 5, το παράδειγμά μας θα μοιάζει με αυτό που βλέπετε στην Καταχώριση 3.

Λίστα 3. Χρησιμοποιώντας γενικά και το βελτιωμένο for-loop για να επαναλάβετε μια λίστα συμβολοσειρών

 Λίστα ονομάτων = νέα LinkedList (); // ... προσθέστε μερικά ονόματα στη συλλογή για το (String name: names) System.out.println (name); 

Η Java 7 μας έδωσε τον τελεστή διαμαντιών, γεγονός που μειώνει την ευγένεια των γενόσημων. Πέρασαν οι μέρες που έπρεπε να επαναλάβω τον τύπο που χρησιμοποιήθηκε για να δημιουργηθεί η γενική τάξη μετά την επίκληση του νέος χειριστής! Στην Java 7 θα μπορούσαμε να απλοποιήσουμε την πρώτη γραμμή στην λίστα 3 παραπάνω ως εξής:

 Λίστα ονομάτων = νέα LinkedList (); 

Ένα ήπιο χτύπημα κατά των γενόσημων

Ο σχεδιασμός μιας γλώσσας προγραμματισμού συνεπάγεται ανταλλαγές μεταξύ των πλεονεκτημάτων των γλωσσικών χαρακτηριστικών έναντι της πολυπλοκότητας που επιβάλλουν στη σύνταξη και τη σημασιολογία της γλώσσας. Για τα γενόσημα, δεν είμαι πεπεισμένος ότι τα οφέλη υπερτερούν της πολυπλοκότητας. Το Generics έλυσε ένα πρόβλημα που δεν είχα με την Java. Συμφωνώ γενικά με τη γνώμη του Ken Arnold όταν δηλώνει: "Τα γενικά είναι ένα λάθος. Αυτό δεν είναι ένα πρόβλημα που βασίζεται σε τεχνικές διαφωνίες. Είναι ένα θεμελιώδες πρόβλημα σχεδιασμού γλώσσας [...] Η πολυπλοκότητα της Java έχει υπερτροφοδοτήσει αυτό που μου φαίνεται σχετικά μικρό όφελος. "

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

Επανάληψη με τη μέθοδο forEach ()

Πριν εξερευνήσουμε τις δυνατότητες επανάληψης Java 8, ας αναλογιστούμε τι συμβαίνει με τον κώδικα που εμφανίζεται στις προηγούμενες λίστες - πράγμα που δεν είναι τίποτα πραγματικά. Υπάρχουν εκατομμύρια γραμμές κώδικα Java σε εφαρμογές που αναπτύσσονται αυτήν τη στιγμή και χρησιμοποιούν ενεργούς επαναληπτικούς παρόμοιους με αυτούς που εμφανίζονται στις λίστες μου. Το Java 8 παρέχει απλώς πρόσθετες δυνατότητες και νέους τρόπους εκτέλεσης επαναλήψεων. Για ορισμένα σενάρια, οι νέοι τρόποι μπορεί να είναι καλύτεροι.

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

ΕΝΑ προεπιλεγμένη μέθοδος, μια άλλη νέα δυνατότητα στο Java 8, είναι μια μέθοδος σε μια διεπαφή με μια προεπιλεγμένη εφαρμογή. Σε αυτήν την περίπτωση, το για κάθε() Η μέθοδος εφαρμόζεται πραγματικά χρησιμοποιώντας έναν ενεργό επαναληπτικό με τρόπο παρόμοιο με αυτό που είδατε στην Καταχώριση 3.

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

Λίστα 4. Επανάληψη σε Java 8 χρησιμοποιώντας τη μέθοδο forEach ()

 Λίστα ονομάτων = νέα LinkedList (); // ... προσθέστε μερικά ονόματα στα ονόματα συλλογής.forEach (όνομα -> System.out.println (όνομα)); 

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

Επανάληψη με ροές Java

Τώρα ας εξετάσουμε το ενδεχόμενο να κάνουμε κάτι λίγο πιο εμπλεκόμενο από την απλή εκτύπωση των ονομάτων στη λίστα μας. Ας υποθέσουμε, για παράδειγμα, ότι θέλουμε να μετρήσουμε τον αριθμό των ονομάτων που ξεκινούν με το γράμμα ΕΝΑ. Θα μπορούσαμε να εφαρμόσουμε την πιο περίπλοκη λογική ως μέρος της έκφρασης λάμδα ή θα μπορούσαμε να χρησιμοποιήσουμε το νέο Stream API του Java 8. Ας ακολουθήσουμε την τελευταία προσέγγιση.