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

Διάγνωση και επίλυση του StackOverflowError

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

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

Η σχέση της αναδρομής έπεσε άσχημα Σφάλμα StackOverflow σημειώνεται στην περιγραφή Javadoc για StackOverflowError που δηλώνει ότι αυτό το σφάλμα είναι "Ρίχνεται όταν προκύπτει υπερχείλιση στοίβας επειδή μια εφαρμογή επαναλαμβάνεται πολύ βαθιά." Είναι σημαντικό αυτό Σφάλμα StackOverflow τελειώνει με τη λέξη Λάθος και είναι ένα σφάλμα (επεκτείνει το java.lang.Error μέσω του java.lang.VirtualMachineError) αντί για μια εξαίρεση που έχει επιλεγεί ή κατά την εκτέλεση. Η διαφορά είναι σημαντική. ο Λάθος και Εξαίρεση το καθένα είναι ένα εξειδικευμένο Throwable, αλλά ο προβλεπόμενος χειρισμός τους είναι αρκετά διαφορετικός. Το Java Tutorial επισημαίνει ότι τα σφάλματα είναι συνήθως εξωτερικά της εφαρμογής Java και ως εκ τούτου κανονικά δεν μπορούν και δεν πρέπει να εντοπιστούν ή να αντιμετωπιστούν από την εφαρμογή.

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

StackOverflowErrorDemonstrator.java

πακέτο dustin.examples.stackoverflow; εισαγωγή java.io.IOException; εισαγωγή java.io.OutputStream; / ** * Αυτή η τάξη δείχνει διαφορετικούς τρόπους με τους οποίους ενδέχεται να προκύψει * StackOverflowError *. * / δημόσια τάξη StackOverflowErrorDemonstrator {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Μέλος δεδομένων με αυθαίρετη συμβολοσειρά. * / private String stringVar = ""; / ** * Ο απλός αξεσουάρ που θα εμφανίσει ακούσια αναδρομή δεν πήγε καλά. Μόλις καλείται *, αυτή η μέθοδος θα αποκαλείται επανειλημμένα. Επειδή δεν υπάρχει * καθορισμένη συνθήκη τερματισμού για τον τερματισμό της επανάληψης, πρέπει να αναμένεται ένα * StackOverflowError. * * μεταβλητή @return String. * / public String getStringVar () {// // ΠΡΟΕΙΔΟΠΟΙΗΣΗ: // // Αυτό είναι κακό! Αυτό θα αναφερθεί αναδρομικά έως ότου η στοίβα // υπερχειλίσει και ένα StackOverflowError ρίξει. Η προβλεπόμενη γραμμή σε // αυτή την περίπτωση θα έπρεπε να ήταν: // return this.stringVar; επιστροφή getStringVar (); } / ** * Υπολογισμός παραγοντικών του παρεχόμενου ακέραιου. Αυτή η μέθοδος βασίζεται σε * αναδρομή. * * @param number Ο αριθμός του οποίου το παραγοντικό είναι επιθυμητό. * @return Η παραγοντική τιμή του παρεχόμενου αριθμού. * / public int calculatorFactorial (τελικός αριθμός int) {// ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα τελειώσει άσχημα εάν παρέχεται αριθμός μικρότερος από το μηδέν. // Ένας καλύτερος τρόπος για να γίνει αυτό φαίνεται εδώ, αλλά σχολίασε. // αριθμός επιστροφής <= 1; 1: number * calculFactorial (αριθμός-1); αριθμός επιστροφής == 1; 1: number * calculFactorial (αριθμός-1); } / ** * Αυτή η μέθοδος δείχνει πώς η ακούσια αναδρομή οδηγεί συχνά σε * StackOverflowError επειδή δεν παρέχεται συνθήκη τερματισμού για την * ακούσια αναδρομή. * / public void runUnintentionalRecursionExample () {final String unusedString = this.getStringVar (); } / ** * Αυτή η μέθοδος δείχνει πώς η ακούσια επανάληψη ως μέρος μιας κυκλικής * εξάρτησης μπορεί να οδηγήσει σε StackOverflowError εάν δεν τηρείται προσεκτικά. * / public void runUnintentionalCyclicRecusionExample () {final State newMexico = State.buildState ("Νέο Μεξικό", "NM", "Santa Fe"); System.out.println ("Η πρόσφατα κατασκευασμένη πολιτεία είναι:"); System.out.println (newMexico); } / ** * Δείχνει πώς ακόμη και η προβλεπόμενη αναδρομή μπορεί να οδηγήσει σε StackOverflowError * όταν η κατάσταση τερματισμού της αναδρομικής λειτουργικότητας δεν είναι ποτέ * ικανοποιημένη. * / public void runIntentionalRecursiveWithDysfunctionalTermination () {final int numberForFactorial = -1; System.out.print ("Το παραγοντικό του" + numberForFactorial + "είναι:"); System.out.println (calculatorFactorial (numberForFactorial)); } / ** * Γράψτε τις κύριες επιλογές αυτής της τάξης στο παρεχόμενο OutputStream. * * @param out OutputStream για να γράψετε τις επιλογές αυτής της δοκιμαστικής εφαρμογής. * / public static void writeOptionsToStream (final OutputStream out) {final String option1 = "1. Αθέλητη (χωρίς συνθήκη τερματισμού) επανάληψη μιας μεθόδου"; τελική επιλογή συμβολοσειράς2 = "2. Ακούσια (χωρίς συνθήκη τερματισμού) κυκλική επανάληψη"; τελική επιλογή συμβολοσειράς3 = "3. Λανθασμένη επανάληψη τερματισμού"; δοκιμάστε το {out.write ((option1 + NEW_LINE) .getBytes ()); out.write ((option2 + NEW_LINE) .getBytes ()); out.write ((option3 + NEW_LINE) .getBytes ()); } catch (IOException ioEx) {System.err.println ("(Δεν είναι δυνατή η εγγραφή στο παρεχόμενο OutputStream)"); System.out.println (επιλογή1); System.out.println (επιλογή2); System.out.println (επιλογή3); }} / ** * Κύρια λειτουργία για την εκτέλεση του StackOverflowErrorDemonstrator. * / public static void main (final επιχειρήματα String []) {if (argument.length <1) {System.err.println ("Πρέπει να δώσετε ένα όρισμα και αυτό το μόνο όρισμα πρέπει να είναι") System.err.println ("μία από τις ακόλουθες επιλογές:"); writeOptionsToStream (System.err); System.exit (-1); } int επιλογή = 0; δοκιμάστε το {option = Integer.valueOf (ορίσματα [0]); } catch (NumberFormatException notNumericFormat) {System.err.println ("Εισαγάγατε μια μη αριθμητική (μη έγκυρη) επιλογή [" + επιχειρήματα [0] + "]"); writeOptionsToStream (System.err); System.exit (-2); } τελικό StackOverflowErrorDemonstrator me = νέο StackOverflowErrorDemonstrator (); διακόπτης (επιλογή) {case 1: me.runUnintentionalRecursionExample (); Διακοπή; περίπτωση 2: me.runUnintentionalCyclicRecusionExample (); Διακοπή; υπόθεση 3: me.runIntentionalRecursiveWithDysfunctionalTermination (); Διακοπή; προεπιλογή: System.err.println ("Παρέχετε μια απροσδόκητη επιλογή [" + επιλογή + "]"); }}} 

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

Εντελώς ανεπιθύμητη επανάληψη

Μπορεί να υπάρχουν στιγμές που η αναδρομή συμβαίνει χωρίς καμία πρόθεση. Μια συνηθισμένη αιτία μπορεί να είναι μια μέθοδος που ονομάζεται κατά λάθος. Για παράδειγμα, δεν είναι πολύ δύσκολο να πάρετε λίγο απρόσεκτοι και να επιλέξετε την πρώτη πρόταση ενός IDE σχετικά με μια τιμή επιστροφής για μια μέθοδο "get" που μπορεί να καταλήξει να είναι μια κλήση για την ίδια μέθοδο! Αυτό είναι στην πραγματικότητα το παράδειγμα που εμφανίζεται στην παραπάνω τάξη. ο getStringVar () Η μέθοδος επαναλαμβάνεται επανειλημμένα μέχρι το Σφάλμα StackOverflow συναντάται. Η έξοδος θα εμφανίζεται ως εξής:

Εξαίρεση στο νήμα "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στο dustin.examples.stackoverflow.StackOverflowError. stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dustin.examples .stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στους dusti n.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) στο 

Το ίχνος στοίβας που φαίνεται παραπάνω είναι στην πραγματικότητα πολλές φορές μεγαλύτερο από αυτό που έβαλα παραπάνω, αλλά είναι απλώς το ίδιο επαναλαμβανόμενο μοτίβο. Επειδή το μοτίβο επαναλαμβάνεται, είναι εύκολο να διαγνωστεί ότι η γραμμή 34 της τάξης είναι η αιτία του προβλήματος. Όταν κοιτάζουμε αυτή τη γραμμή, βλέπουμε ότι είναι πράγματι η δήλωση επιστροφή getStringVar () που καταλήγει επανειλημμένα να αποκαλείται. Σε αυτήν την περίπτωση, μπορούμε γρήγορα να συνειδητοποιήσουμε ότι η επιδιωκόμενη συμπεριφορά ήταν αντ 'αυτού επιστρέψτε αυτό.stringVar;.

Αθέλητη αναδρομή με κυκλικές σχέσεις

Υπάρχουν ορισμένοι κίνδυνοι να έχετε κυκλικές σχέσεις μεταξύ τάξεων. Ένας από αυτούς τους κινδύνους είναι η μεγαλύτερη πιθανότητα αντιμετώπισης ακούσιας αναδρομής όπου οι κυκλικές εξαρτήσεις καλούνται συνεχώς μεταξύ αντικειμένων έως ότου η στοίβα ξεχειλίσει. Για να το δείξω αυτό, χρησιμοποιώ δύο ακόμη τάξεις. ο κατάσταση τάξη και το Πόλη τάξη έχουν μια κυκλική σχέσηhiop επειδή a κατάσταση παράδειγμα έχει μια αναφορά στο κεφάλαιο της Πόλη και ένα Πόλη έχει μια αναφορά στο κατάσταση στο οποίο βρίσκεται.

State.java

πακέτο dustin.examples.stackoverflow; / ** * Μια τάξη που αντιπροσωπεύει μια πολιτεία και είναι σκόπιμα μέρος μιας κυκλικής * σχέσης μεταξύ Πόλης και Πολιτείας. * / Δημόσια τάξη State {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Όνομα της πολιτείας. * / ιδιωτικό όνομα συμβολοσειράς; / ** Συντομογραφία δύο γραμμάτων για την κατάσταση. * / συντομογραφία ιδιωτικής συμβολοσειράς; / ** Πόλη που είναι η πρωτεύουσα του κράτους. * / ιδιωτική πρωτεύουσα της πόλης / ** * Στατική μέθοδος δημιουργίας που είναι η προβλεπόμενη μέθοδος για την παρουσίαση μου. * * @param newName Όνομα της πρόσφατα δημιουργημένης κατάστασης. * @param newAbbreviation Συντομογραφία δύο γραμμάτων του κράτους. * @param newCapitalCityName Όνομα πρωτεύουσας. * / public static State buildState (final String newName, final String newAbbreviation, final String newCapitalCityName) {final State instance = new State (newName, newAbbreviation); instance.capitalCity = νέα πόλη (newCapitalCityName, instance); παράδειγμα επιστροφής; } / ** * Παραμετροποιημένος κατασκευαστής που δέχεται δεδομένα για να συμπληρώσει νέα παρουσία κατάστασης. * * @param newName Όνομα της πρόσφατα δημιουργημένης κατάστασης. * @param newAbbreviation Συντομογραφία δύο γραμμάτων του κράτους. * / ιδιωτικό κράτος (final String newName, final String newAbbreviation) {this.name = newName; this.abbreviation = new Συντομογραφία; } / ** * Παρέχετε την παράσταση συμβολοσειράς της παρουσίας κατάστασης. * * @return Αναπαράσταση My String. * / @ Override public String toString () {// ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα τελειώσει άσχημα επειδή καλεί τη μέθοδο City's toString () // σιωπηρά και η μέθοδος City toString () καλεί αυτήν τη μέθοδο // State.toString (). επιστροφή "StateName:" + this.name + NEW_LINE + "StateAbbreviation:" + this.abbreviation + NEW_LINE + "CapitalCity:" + this.capitalCity; }} 

City.java