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

Οι ελεγμένες εξαιρέσεις είναι καλές ή κακές;

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

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

Τι είναι οι εξαιρέσεις;

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

Οι πρώτες προσπάθειες αναγνώρισης εξαιρέσεων περιελάμβαναν επιστροφή ειδικών τιμών που υποδηλώνουν αποτυχία. Για παράδειγμα, η γλώσσα Γ fopen () επιστρέφει η συνάρτηση ΜΗΔΕΝΙΚΟ όταν δεν μπορεί να ανοίξει ένα αρχείο. Επίσης, PHP's mysql_query () επιστρέφει η συνάρτηση ΨΕΥΔΗΣ όταν παρουσιάζεται μια αποτυχία SQL. Πρέπει να ψάξετε αλλού για τον πραγματικό κωδικό αποτυχίας. Παρόλο που είναι εύκολο να εφαρμοστεί, υπάρχουν δύο προβλήματα με αυτήν την προσέγγιση "επιστροφής ειδικής αξίας" για την αναγνώριση εξαιρέσεων:

  • Οι ειδικές τιμές δεν περιγράφουν την εξαίρεση. Τι κάνει ΜΗΔΕΝΙΚΟ ή ΨΕΥΔΗΣ πραγματικά εννοεί; Όλα εξαρτώνται από τον συντάκτη της λειτουργικότητας που επιστρέφει την ειδική τιμή. Επιπλέον, πώς συσχετίζετε μια ειδική τιμή με το περιβάλλον του προγράμματος όταν έγινε η εξαίρεση, ώστε να μπορείτε να παρουσιάσετε ένα σημαντικό μήνυμα στον χρήστη;
  • Είναι πολύ εύκολο να αγνοήσετε μια ειδική τιμή. Για παράδειγμα, int γ; FILE * fp = fopen ("data.txt", "r"); c = fgetc (fp); είναι προβληματική επειδή εκτελείται αυτό το τμήμα κώδικα C fgetc () για να διαβάσετε έναν χαρακτήρα από το αρχείο ακόμα και όταν fopen () επιστρέφει ΜΗΔΕΝΙΚΟ. Σε αυτήν την περίπτωση, fgetc () δεν θα πετύχει: έχουμε ένα σφάλμα που μπορεί να είναι δύσκολο να βρεθεί.

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

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

Εξαιρέσεις και Java

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

Για παράδειγμα, η βιβλιοθήκη Java περιλαμβάνει java.net.URISyntaxException, το οποίο επεκτείνεται Εξαίρεση και υποδεικνύει ότι μια συμβολοσειρά δεν ήταν δυνατό να αναλυθεί ως αναφορά Uniform Resource Identifier. Σημειώστε ότι URISyntaxException ακολουθεί μια σύμβαση ονομασίας στην οποία ένα όνομα κλάσης εξαίρεσης τελειώνει με τη λέξη Εξαίρεση. Μια παρόμοια σύμβαση ισχύει για ονόματα τάξεων σφάλματος, όπως java.lang.OutOfMemoryError.

Εξαίρεση υποκατηγοριοποιείται από java.lang.RuntimeException, που είναι το superclass αυτών των εξαιρέσεων που μπορούν να απορριφθούν κατά την κανονική λειτουργία της Java Virtual Machine (JVM). Για παράδειγμα, java.lang.ArithmeticException περιγράφει αριθμητικές αστοχίες όπως προσπάθειες διαίρεσης ακέραιων με ακέραιο 0. Επίσης, java.lang.NullPointerException περιγράφει τις προσπάθειες πρόσβασης σε μέλη αντικειμένων μέσω της μηδενικής αναφοράς.

Ένας άλλος τρόπος να δούμε RuntimeException

Το τμήμα 11.1.1 της Java 8 Language Specification ορίζει: RuntimeException είναι το superclass όλων των εξαιρέσεων που μπορεί να απορριφθούν για πολλούς λόγους κατά την αξιολόγηση της έκφρασης, αλλά από τις οποίες η ανάκτηση μπορεί να είναι ακόμα δυνατή.

Όταν προκύπτει εξαίρεση ή σφάλμα, ένα αντικείμενο από το κατάλληλο Εξαίρεση ή Λάθος Η υποκατηγορία δημιουργείται και μεταβιβάζεται στο JVM. Η πράξη της διέλευσης του αντικειμένου είναι γνωστή ως ρίχνοντας την εξαίρεση. Η Java παρέχει το βολή δήλωση για το σκοπό αυτό. Για παράδειγμα, ρίξτε νέο IOException ("αδυναμία ανάγνωσης αρχείου"). δημιουργεί ένα νέο java.io.IOException αντικείμενο που έχει αρχικοποιηθεί στο καθορισμένο κείμενο. Αυτό το αντικείμενο στη συνέχεια ρίχνεται στο JVM.

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

δοκιμάστε το {method (); } // ... άκυρη μέθοδος () {ρίξτε νέο NullPointerException ("κάποιο κείμενο"); }

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

Η JVM λαμβάνει το ρίξιμο και αναζητά τη στοίβα μεθόδου-κλήσης για ένα χειριστής να χειριστεί την εξαίρεση. Εξαιρέσεις που δεν προέρχονται από RuntimeException αντιμετωπίζονται συχνά. Οι εξαιρέσεις και τα σφάλματα εκτέλεσης σπάνια αντιμετωπίζονται.

Γιατί σπάνια αντιμετωπίζονται σφάλματα

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

Ένας χειριστής περιγράφεται από το a σύλληψη μπλοκ που ακολουθεί το δοκιμάστε ΟΙΚΟΔΟΜΙΚΟ ΤΕΤΡΑΓΩΝΟ. ο σύλληψη Το μπλοκ παρέχει μια κεφαλίδα που παραθέτει τους τύπους εξαιρέσεων που είναι διατεθειμένος να χειριστεί. Εάν ο τύπος του ρίχνου περιλαμβάνεται στη λίστα, το ρίξιμο μεταφέρεται στο σύλληψη μπλοκ του οποίου εκτελείται ο κωδικός. Ο κώδικας ανταποκρίνεται στην αιτία της αποτυχίας με τέτοιο τρόπο ώστε το πρόγραμμα να προχωρήσει ή πιθανώς να τερματιστεί:

δοκιμάστε το {method (); } catch (NullPointerException npe) {System.out.println ("απόπειρα πρόσβασης σε μέλος αντικειμένου μέσω μηδενικής αναφοράς"); } // ... άκυρη μέθοδος () {ρίξτε νέο NullPointerException ("κάποιο κείμενο"); }

Σε αυτό το τμήμα κώδικα, έχω επισυνάψει ένα σύλληψη μπλοκ στο δοκιμάστε ΟΙΚΟΔΟΜΙΚΟ ΤΕΤΡΑΓΩΝΟ. Οταν ο NullPointerException αντικείμενο ρίχνεται από μέθοδος(), το JVM εντοπίζει και περνά την εκτέλεση στο σύλληψη μπλοκ, το οποίο εξάγει ένα μήνυμα.

Τέλος μπλοκ

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

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

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

δοκιμάστε το {method (); } catch (IOException πρόσβαση) {System.out.println ("αποτυχία I / O"); } // ... άκυρη μέθοδος () ρίχνει το IOException {ρίξτε νέο IOException ("κάποιο κείμενο"); }

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

Ισχυρισμός υπέρ και κατά των ελεγμένων εξαιρέσεων

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

Οι ελεγμένες εξαιρέσεις είναι καλές

Ο James Gosling δημιούργησε τη γλώσσα Java. Περιέλαβε ελεγμένες εξαιρέσεις για να ενθαρρύνει τη δημιουργία ενός πιο ισχυρού λογισμικού. Σε μια συνομιλία του 2003 με τον Bill Venners, ο Gosling επεσήμανε πόσο εύκολο είναι να δημιουργηθεί buggy code στη γλώσσα C, αγνοώντας τις ειδικές τιμές που επιστρέφονται από τις λειτουργίες που προσανατολίζονται στο C. Για παράδειγμα, ένα πρόγραμμα προσπαθεί να διαβάσει από ένα αρχείο που δεν άνοιξε με επιτυχία για ανάγνωση.

Η σοβαρότητα του μη ελέγχου των τιμών επιστροφής

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

Ο Gosling επεσήμανε επίσης ότι τα μαθήματα προγραμματισμού κολλεγίων δεν συζητούν επαρκώς τον χειρισμό σφαλμάτων (αν και αυτό μπορεί να έχει αλλάξει από το 2003). Όταν πηγαίνετε στο κολέγιο και κάνετε εργασίες, σας ζητούν απλώς να κωδικοποιήσετε την πραγματική πορεία [της εκτέλεσης όπου η αποτυχία δεν είναι ζήτημα]. Σίγουρα δεν βίωσα ποτέ ένα κολέγιο όπου συζητήθηκε ο χειρισμός σφαλμάτων. Βγαίνετε από το κολέγιο και το μόνο πράγμα που είχατε να αντιμετωπίσετε είναι το μοναδικό μονοπάτι.

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

Οι επιλεγμένες εξαιρέσεις είναι κακές

Πολλοί προγραμματιστές μισούν τις ελεγμένες εξαιρέσεις επειδή είναι αναγκασμένοι να αντιμετωπίσουν API που τα χρησιμοποιούν υπερβολικά ή καθορίζουν εσφαλμένα ελεγμένες εξαιρέσεις αντί για μη ελεγμένες εξαιρέσεις ως μέρος των συμβολαίων τους. Για παράδειγμα, μια μέθοδος που ορίζει την τιμή ενός αισθητήρα περνά έναν μη έγκυρο αριθμό και ρίχνει μια επιλεγμένη εξαίρεση αντί για μια παρουσία του μη επιλεγμένου java.lang.IllegalArgumentException τάξη.

Ακολουθούν μερικοί άλλοι λόγοι για να μην σας αρέσουν οι επιλεγμένες εξαιρέσεις. Τους έχω αποσπάσει από τις συνεντεύξεις του Slashdot: Ρωτήστε τον Τζέιμς Γκόσλινγκ για τη συζήτηση Java και Ocean Exploring Robots:

  • Οι ελεγμένες εξαιρέσεις είναι εύκολο να αγνοηθούν με την επανεξέτασή τους ως RuntimeException περιπτώσεις, οπότε τι νόημα έχεις; Έχω χάσει τον αριθμό των φορών που έχω γράψει αυτό το μπλοκ κώδικα:
    δοκιμάστε {// do stuff} catch (AnnoyingcheckedException e) {ρίξτε νέο RuntimeException (e); }

    99% του χρόνου δεν μπορώ να κάνω τίποτα γι 'αυτό. Τέλος, τα μπλοκ κάνουν τον απαραίτητο καθαρισμό (ή τουλάχιστον θα έπρεπε).

  • Οι ελεγμένες εξαιρέσεις μπορούν να αγνοηθούν με την κατάποση τους, οπότε τι νόημα έχεις; Έχω χάσει επίσης τον αριθμό των φορών που το έχω δει:
    δοκιμάστε {// do stuff} catch (AnnoyingCheckedException e) {// μην κάνετε τίποτα}

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

  • Οι ελεγμένες εξαιρέσεις έχουν ως αποτέλεσμα πολλαπλές ρίχνει δηλώσεις ρήτρας. Το πρόβλημα με ελεγμένες εξαιρέσεις είναι ότι ενθαρρύνουν τους ανθρώπους να καταπιούν σημαντικές λεπτομέρειες (δηλαδή, την κατηγορία εξαίρεσης). Εάν επιλέξετε να μην καταπιείτε αυτήν τη λεπτομέρεια, τότε πρέπει να συνεχίσετε να προσθέτετε ρίχνει δηλώσεις σε ολόκληρη την εφαρμογή σας. Αυτό σημαίνει 1) ότι ένας νέος τύπος εξαίρεσης θα επηρεάσει πολλές υπογραφές λειτουργιών και 2) μπορείτε να χάσετε μια συγκεκριμένη παρουσία της εξαίρεσης που πραγματικά θέλετε να πιάσετε (ας πούμε ότι ανοίγετε ένα δευτερεύον αρχείο για μια συνάρτηση που γράφει δεδομένα σε ένα Το δευτερεύον αρχείο είναι προαιρετικό, επομένως μπορείτε να αγνοήσετε τα λάθη του, αλλά επειδή η υπογραφή ρίχνει IOException, είναι εύκολο να το παραβλέψεις αυτό).
  • Οι ελεγμένες εξαιρέσεις δεν είναι πραγματικά εξαιρέσεις. Το θέμα των ελεγχόμενων εξαιρέσεων είναι ότι δεν είναι πραγματικά εξαιρέσεις από τη συνήθη κατανόηση της έννοιας. Αντ 'αυτού, είναι εναλλακτικές τιμές επιστροφής API.

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

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

συμπέρασμα

Οι ελεγμένες εξαιρέσεις είναι καλές ή είναι κακές; Με άλλα λόγια, πρέπει οι προγραμματιστές να υποχρεωθούν να χειριστούν ελεγμένες εξαιρέσεις ή να τους δοθεί η ευκαιρία να τις αγνοήσουν; Μου αρέσει η ιδέα της εφαρμογής πιο ισχυρού λογισμικού. Ωστόσο, πιστεύω επίσης ότι ο μηχανισμός χειρισμού εξαιρέσεων της Java πρέπει να εξελιχθεί για να γίνει πιο φιλικός προς τον προγραμματιστή. Ακολουθούν μερικοί τρόποι βελτίωσης αυτού του μηχανισμού: