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

Προσοχή στους κινδύνους των γενικών εξαιρέσεων

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

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

Λίστα 1. Αρχικός κωδικός καθαρισμού

private void cleanupConnections () ρίχνει ExceptionOne, ExceptionTwo {for (int i = 0; i <συνδέσεις.length; i ++) {σύνδεση [i] .release (); // Ρίχνει ExceptionOne, ExceptionTwo σύνδεση [i] = null; } συνδέσεις = null; } προστατευμένο abstract void cleanupFiles () ρίχνει ExceptionThree, ExceptionFour; προστατευμένο abstract void removeListeners () ρίχνει ExceptionFive, ExceptionSix; public void cleanupEverything () ρίχνει την Εξαίρεση {cleanupConnections (); cleanupFiles (); αφαίρεσηListeners (); } δημόσιο κενό () {δοκιμάστε {doStuff (); cleanupEverything (); doMoreStuff (); } αλίευση (Εξαίρεση ε) {}} 

Σε ένα άλλο μέρος του κώδικα, το συνδέσεις Ο πίνακας δεν αρχικοποιείται έως ότου δημιουργηθεί η πρώτη σύνδεση. Αλλά αν δεν δημιουργηθεί ποτέ σύνδεση, τότε ο πίνακας συνδέσεων είναι μηδενικός. Έτσι, σε ορισμένες περιπτώσεις, η κλήση προς συνδέσεις [i] .release () καταλήγει σε ένα NullPointerException. Αυτό είναι ένα σχετικά εύκολο πρόβλημα να επιλυθεί. Απλώς προσθέστε μια επιταγή για συνδέσεις! = μηδέν.

Ωστόσο, η εξαίρεση δεν αναφέρεται ποτέ. Πετάγεται από cleanupConnections (), ρίχτηκαν ξανά από Καθαρισμός Όλα (), και τελικά μπήκα Ολοκληρώθηκε(). ο Ολοκληρώθηκε() Η μέθοδος δεν κάνει τίποτα με την εξαίρεση, δεν το καταγράφει καν. Και επειδή Καθαρισμός Όλα () καλείται μόνο μέσω Ολοκληρώθηκε(), η εξαίρεση δεν φαίνεται ποτέ. Έτσι, ο κώδικας δεν διορθώνεται ποτέ.

Έτσι, στο σενάριο αποτυχίας, το cleanupFiles () και αφαίρεση ακροατών () Οι μέθοδοι δεν καλούνται ποτέ (έτσι οι πόροι τους δεν κυκλοφορούν ποτέ), και doMoreStuff () δεν καλείται ποτέ, έτσι, η τελική επεξεργασία στο Ολοκληρώθηκε() ποτέ δεν ολοκληρώνεται. Για να κάνουν τα πράγματα χειρότερα, Ολοκληρώθηκε() δεν καλείται όταν κλείσει το σύστημα. Αντ 'αυτού καλείται να ολοκληρώσει κάθε συναλλαγή. Έτσι διαρροές πόρων σε κάθε συναλλαγή.

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

  • Μην αγνοείτε τις εξαιρέσεις
  • Μην πιάσετε γενικές Εξαίρεσημικρό
  • Μην πετάτε γενικά Εξαίρεσημικρό

Μην αγνοείτε τις εξαιρέσεις

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

Στις περισσότερες περιπτώσεις, μια εξαίρεση θα πρέπει, τουλάχιστον, να καταγράφεται. Πολλά πακέτα καταγραφής (δείτε την πλευρική γραμμή "Εξαιρέσεις καταγραφής") μπορούν να καταγράψουν σφάλματα συστήματος και εξαιρέσεις χωρίς να επηρεάσουν σημαντικά την απόδοση του συστήματος. Τα περισσότερα συστήματα καταγραφής επιτρέπουν επίσης την εκτύπωση ιχνών στοίβας, παρέχοντας έτσι πολύτιμες πληροφορίες για το πού και γιατί συνέβη η εξαίρεση. Τέλος, επειδή τα αρχεία καταγραφής είναι συνήθως γραμμένα σε αρχεία, ένα αρχείο εξαιρέσεων μπορεί να αναθεωρηθεί και να αναλυθεί. Δείτε την καταχώριση 11 στην πλαϊνή γραμμή για ένα παράδειγμα καταγραφής ιχνών στοίβας.

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

Εξαιρέσεις τελικά

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

Λίστα 2

public void loadFile (String fileName) ρίχνει το IOException {InputStream in = null; δοκιμάστε το {in = new FileInputStream (όνομα αρχείου); readSomeData (σε); } τέλος {if (in! = null) {δοκιμάστε {in.close (); } catch (IOException)) {// Αγνοήθηκε}}}} 

Σημειώστε ότι loadFile () εξακολουθεί να αναφέρει ένα IOException στη μέθοδο κλήσης εάν η πραγματική φόρτωση δεδομένων αποτύχει λόγω ενός προβλήματος εισόδου / εξόδου (είσοδος / έξοδος). Σημειώστε επίσης ότι παρόλο που μια εξαίρεση από Κλείσε() αγνοείται, ο κώδικας δηλώνει ότι ρητά σε ένα σχόλιο για να το καταστήσει σαφές σε όποιον εργάζεται στον κώδικα. Μπορείτε να εφαρμόσετε αυτήν την ίδια διαδικασία για τον καθαρισμό όλων των ροών I / O, των πριζών κλεισίματος και των συνδέσεων JDBC και ούτω καθεξής.

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

Μην πιάσετε γενικές εξαιρέσεις

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

Αντί να προσθέσετε τα τέσσερα διαφορετικά μπλοκ catch στο μπλοκ δοκιμών, ένας πολυάσχολος προγραμματιστής μπορεί απλώς να τυλίξει τις κλήσεις μεθόδου σε ένα μπλοκ δοκιμής / catch που πιάνει γενικά Εξαίρεσηs (βλ. λίστα 3 παρακάτω). Αν και αυτό φαίνεται αβλαβές, θα μπορούσαν να προκύψουν κάποιες ανεπιθύμητες παρενέργειες. Για παράδειγμα, εάν όνομα τάξης() είναι μηδενικό, Class.forName () θα ρίξει ένα NullPointerException, η οποία θα παγιδευτεί στη μέθοδο.

Σε αυτήν την περίπτωση, το μπλοκ αλιευμάτων συλλαμβάνει εξαιρέσεις που δεν είχε ποτέ σκοπό να πιάσει επειδή NullPointerException είναι μια υποκατηγορία του RuntimeException, το οποίο, με τη σειρά του, είναι μια υποκατηγορία του Εξαίρεση. Έτσι το γενικό αλίευση (εξαίρεση ε) πιάνει όλες τις υποκατηγορίες του RuntimeException, συμπεριλαμβανομένου NullPointerException, IndexOutOfBoundsException, και ArrayStoreException. Συνήθως, ένας προγραμματιστής δεν προτίθεται να χρησιμοποιήσει αυτές τις εξαιρέσεις.

Στην λίστα 3, το null className καταλήγει σε ένα NullPointerException, που υποδεικνύει στη μέθοδο κλήσης ότι το όνομα κλάσης δεν είναι έγκυρο:

Λίστα 3

δημόσιο SomeInterface buildInstance (String className) {SomeInterface impl = null; δοκιμάστε το {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Εξαίρεση e) {log.error ("Σφάλμα δημιουργίας κλάσης:" + className); } επιστροφή impl; } 

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

Λίστα 4

catch (Εξαίρεση e) {if (e instanceof ClassNotFoundException) {log.error ("Μη έγκυρο όνομα κλάσης:" + className + "," + e.toString ()); } αλλιώς {log.error ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className + "," + e.toString ()); }} 

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

Λίστα 5

δημόσιο SomeInterface buildInstance (String className) {SomeInterface impl = null; δοκιμάστε το {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Μη έγκυρο όνομα κλάσης:" + className + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Μη έγκυρος τύπος κλάσης," + className + "δεν υλοποιεί" + SomeInterface.class.getName ()); } επιστροφή impl; } 

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

Η λίστα 6 παρακάτω παρέχει μια εναλλακτική έκδοση του buildInterface () μέθοδος, που ρίχνει ένα ClassNotFoundException εάν παρουσιαστεί πρόβλημα κατά τη φόρτωση και την παρουσίαση της κλάσης. Σε αυτό το παράδειγμα, η μέθοδος κλήσης διασφαλίζεται ότι λαμβάνει είτε ένα σωστά τεκμηριωμένο αντικείμενο είτε μια εξαίρεση. Έτσι, η μέθοδος κλήσης δεν χρειάζεται να ελέγξει εάν το αντικείμενο που επιστρέφεται είναι μηδενικό.

Σημειώστε ότι αυτό το παράδειγμα χρησιμοποιεί τη μέθοδο Java 1.4 για τη δημιουργία μιας νέας εξαίρεσης που περιβάλλεται από μια άλλη εξαίρεση για τη διατήρηση των αρχικών πληροφοριών ιχνών στοίβας. Διαφορετικά, το ίχνος στοίβας θα υποδείξει τη μέθοδο buildInstance () ως η μέθοδος από την οποία προέκυψε η εξαίρεση, αντί της υποκείμενης εξαίρεσης που newInstance ():

Λίστα 6

δημόσιο SomeInterface buildInstance (String className) ρίχνει ClassNotFoundException {δοκιμάστε {Class clazz = Class.forName (className); επιστροφή (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Μη έγκυρο όνομα κλάσης:" + className + "," + e.toString ()); ρίξτε e; } catch (InstantiationException e) {ρίξτε νέο ClassNotFoundException ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className, e); } catch (IllegalAccessException e) {ρίξτε νέο ClassNotFoundException ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className, e); } catch (ClassCastException e) {ρίξτε νέο ClassNotFoundException (το className + "δεν υλοποιεί" + SomeInterface.class.getName (), e); }} 

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

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

Σημείωση:IlegalClassException είναι μια κατηγορία εξαίρεσης τομέα που αναφέρεται εδώ για σκοπούς επίδειξης.

Λίστα 7

δημόσιο SomeInterface buildInstance (String className) ρίχνει IllegalClassException {SomeInterface impl = null; δοκιμάστε το {Class clazz = Class.forName (className); επιστροφή (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Μη έγκυρο όνομα κλάσης:" + className + ", χρησιμοποιώντας προεπιλογή"); } catch (InstantiationException e) {log.warn ("Μη έγκυρο όνομα κλάσης:" + className + ", χρησιμοποιώντας προεπιλογή"); } catch (IllegalAccessException e) {ρίξτε νέο IllegalClassException ("Δεν είναι δυνατή η δημιουργία κλάσης:" + className, e); } catch (ClassCastException e) {ρίξτε νέο IllegalClassException (το className + "δεν υλοποιεί" + SomeInterface.class.getName (), e); } if (impl == null) {impl = new DefaultImplemantation (); } επιστροφή impl; } 

Όταν γενικές εξαιρέσεις πρέπει να ληφθούν

Ορισμένες περιπτώσεις δικαιολογούν όταν είναι βολικό και απαιτείται για να πιάσετε γενικές Εξαίρεσημικρό. Αυτές οι περιπτώσεις είναι πολύ συγκεκριμένες, αλλά σημαντικές για μεγάλα, ανεκτά σε αστοχία συστήματα. Στην Λίστα 8, τα αιτήματα διαβάζονται από μια σειρά αιτημάτων και υποβάλλονται σε επεξεργασία με τη σειρά. Αλλά εάν προκύψουν εξαιρέσεις κατά την επεξεργασία του αιτήματος (είτε a BadRequestException ή όποιος υποκατηγορία του RuntimeException, συμπεριλαμβανομένου NullPointerException), τότε θα εξαχθεί αυτή η εξαίρεση εξω απο η επεξεργασία ενώ βρόχος. Έτσι, οποιοδήποτε σφάλμα προκαλεί τη διακοπή του βρόχου επεξεργασίας και τυχόν υπολειπόμενα αιτήματα δεν θα να υποβληθεί σε επεξεργασία. Αυτό αντιπροσωπεύει έναν κακό τρόπο αντιμετώπισης ενός σφάλματος κατά την επεξεργασία αιτημάτων:

Λίστα 8

public void processAllRequests () {Αίτημα req = null; δοκιμάστε το {while (true) {req = getNextRequest (); εάν (req! = null) {processRequest (req); // ρίχνει BadRequestException} άλλο {// Η ουρά αιτήσεων είναι κενή, πρέπει να γίνει διάλειμμα. }}} catch (BadRequestException e) {log.error ("Μη έγκυρο αίτημα:" + req, e); }}