Le eccezioni controllate sono buone o cattive?

Java supporta le eccezioni controllate. Questa controversa caratteristica del linguaggio è amata da alcuni e odiata da altri, al punto che la maggior parte dei linguaggi di programmazione evita le eccezioni controllate e supporta solo le loro controparti non controllate.

In questo post, esamino la controversia che circonda le eccezioni controllate. Per prima cosa introduco il concetto di eccezioni e descrivo brevemente il supporto del linguaggio Java per le eccezioni per aiutare i principianti a comprendere meglio la controversia.

Cosa sono le eccezioni?

In un mondo ideale, i programmi per computer non incontrerebbero mai alcun problema: i file esisterebbero quando dovrebbero esistere, le connessioni di rete non si chiuderebbero mai inaspettatamente, non ci sarebbe mai un tentativo di invocare un metodo tramite il riferimento nullo, intero-divisione per -zero tentativi non si verificherebbero, e così via. Tuttavia, il nostro mondo è tutt'altro che ideale; queste e altre eccezioni all'esecuzione ideale del programma sono molto diffuse.

I primi tentativi di riconoscere le eccezioni includevano la restituzione di valori speciali che indicano un errore. Ad esempio, la fopen()funzione del linguaggio C restituisce NULLquando non è possibile aprire un file. Inoltre, la mysql_query()funzione di PHP ritorna FALSEquando si verifica un errore SQL. Devi cercare altrove il codice di errore effettivo. Sebbene sia facile da implementare, ci sono due problemi con questo approccio "restituisci valore speciale" per riconoscere le eccezioni:

  • I valori speciali non descrivono l'eccezione. Cosa significa NULLo FALSEsignifica veramente? Tutto dipende dall'autore della funzionalità che restituisce il valore speciale. Inoltre, come si collega un valore speciale al contesto del programma quando si è verificata l'eccezione in modo da poter presentare un messaggio significativo all'utente?
  • È troppo facile ignorare un valore speciale. Ad esempio, int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);è problematico perché questo frammento di codice C viene eseguito fgetc()per leggere un carattere dal file anche quando fopen()ritorna NULL. In questo caso, fgetc()non avrà successo: abbiamo un bug che potrebbe essere difficile da trovare.

Il primo problema viene risolto utilizzando le classi per descrivere le eccezioni. Il nome di una classe identifica il tipo di eccezione ei suoi campi aggregano il contesto del programma appropriato per determinare (tramite chiamate di metodo) cosa è andato storto. Il secondo problema viene risolto facendo in modo che il compilatore costringa il programmatore a rispondere direttamente a un'eccezione o indichi che l'eccezione deve essere gestita altrove.

Alcune eccezioni sono molto gravi. Ad esempio, un programma potrebbe tentare di allocare una parte della memoria quando non è disponibile memoria libera. La ricorsione illimitata che esaurisce lo stack è un altro esempio. Tali eccezioni sono note come errori .

Eccezioni e Java

Java utilizza le classi per descrivere le eccezioni e gli errori. Queste classi sono organizzate in una gerarchia radicata nella java.lang.Throwableclasse. (Il motivo per cui è Throwablestato scelto di nominare questa classe speciale diventerà evidente a breve.) Subito sotto ci Throwablesono le classi java.lang.Exceptione java.lang.Error, che descrivono rispettivamente le eccezioni e gli errori.

Ad esempio, la libreria Java include java.net.URISyntaxException, che si estende Exceptione indica che una stringa non può essere analizzata come riferimento di Uniform Resource Identifier. Notare che URISyntaxExceptionsegue una convenzione di denominazione in cui il nome di una classe di eccezione termina con la parola Exception. Una convenzione simile si applica ai nomi delle classi di errore, come java.lang.OutOfMemoryError.

Exceptionè sottoclasse da java.lang.RuntimeException, che è la superclasse di quelle eccezioni che possono essere lanciate durante il normale funzionamento della Java Virtual Machine (JVM). Ad esempio, java.lang.ArithmeticExceptiondescrive i fallimenti aritmetici come i tentativi di dividere i numeri interi per il numero intero 0. Inoltre, java.lang.NullPointerExceptiondescrive i tentativi di accesso ai membri dell'oggetto tramite il riferimento null.

Un altro modo di guardare RuntimeException

La sezione 11.1.1 della specifica del linguaggio Java 8 afferma: RuntimeExceptionè la superclasse di tutte le eccezioni che possono essere lanciate per molte ragioni durante la valutazione dell'espressione, ma da cui è ancora possibile il ripristino.

Quando si verifica un'eccezione o un errore, un oggetto della sottoclasse Exceptiono appropriata Errorviene creato e trasmesso alla JVM. L'atto di passare l'oggetto è noto come lancio dell'eccezione . Java fornisce la throwdichiarazione per questo scopo. Ad esempio, throw new IOException("unable to read file");crea un nuovo java.io.IOExceptionoggetto inizializzato sul testo specificato. Questo oggetto viene successivamente lanciato alla JVM.

Java fornisce l' tryistruzione per delimitare il codice da cui può essere generata un'eccezione. Questa istruzione consiste in una parola chiave tryseguita da un blocco delimitato da parentesi graffe. Il frammento di codice seguente dimostra trye throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

In questo frammento di codice, l'esecuzione entra nel tryblocco e richiama method(), che genera un'istanza di NullPointerException.

La JVM riceve il throwable e cerca nello stack di chiamate di metodo un gestore per gestire l'eccezione. Le eccezioni non derivate da RuntimeExceptionvengono spesso gestite; le eccezioni e gli errori di runtime vengono gestiti raramente.

Perché gli errori vengono gestiti raramente

Gli errori vengono gestiti raramente perché spesso non c'è nulla che un programma Java possa fare per recuperare dall'errore. Ad esempio, quando la memoria libera è esaurita, un programma non può allocare memoria aggiuntiva. Tuttavia, se l'errore di allocazione è dovuto al trattenere molta memoria che dovrebbe essere liberata, un hander potrebbe tentare di liberare la memoria con l'aiuto della JVM. Sebbene un gestore possa sembrare utile in questo contesto di errore, il tentativo potrebbe non riuscire.

Un gestore è descritto da un catchblocco che segue il tryblocco. Il catchblocco fornisce un'intestazione che elenca i tipi di eccezioni che è pronto a gestire. Se il tipo di throwable è incluso nell'elenco, il throwable viene passato al catchblocco il cui codice viene eseguito. Il codice risponde alla causa dell'errore in modo tale da far procedere o eventualmente terminare il programma:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

In questo frammento di codice, ho aggiunto un catchblocco al tryblocco. Quando l' NullPointerExceptionoggetto viene lanciato da method(), la JVM individua e passa l'esecuzione al catchblocco, che emette un messaggio.

Infine blocchi

Un tryblocco o il suo catchblocco finale può essere seguito da un finallyblocco utilizzato per eseguire attività di pulizia, come il rilascio delle risorse acquisite. Non ho altro da dire finallyperché non è rilevante per la discussione.

Le eccezioni descritte da Exceptione le sue sottoclassi ad eccezione di RuntimeExceptione le sue sottoclassi sono note come eccezioni controllate . Per ogni throwistruzione, il compilatore esamina il tipo di oggetto eccezione. Se il tipo indica verificato, il compilatore controlla il codice sorgente per assicurarsi che l'eccezione venga gestita nel metodo in cui è stata generata o dichiarata per essere gestita più in alto nello stack di chiamate del metodo. Tutte le altre eccezioni sono note come eccezioni non controllate .

Java consente di dichiarare che un'eccezione selezionata viene gestita più in alto nello stack di chiamate al metodo aggiungendo una throwsclausola (parola chiave throwsseguita da un elenco delimitato da virgole di nomi di classi di eccezioni verificate) a un'intestazione del metodo:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Poiché IOExceptionè un tipo di eccezione controllato, le istanze generate di questa eccezione devono essere gestite nel metodo in cui vengono generate o devono essere dichiarate per essere gestite più in alto nello stack di chiamate del metodo aggiungendo una throwsclausola all'intestazione di ciascun metodo interessato. In questo caso, una throws IOExceptionclausola viene aggiunta all'intestazione di method(). L' IOExceptionoggetto generato viene passato alla JVM, che individua e trasferisce l'esecuzione al catchgestore.

Litigare a favore e contro le eccezioni verificate

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

Inoltre, ho riscontrato l'argomento sulle applicazioni che devono gestire un gran numero di eccezioni controllate generate dalle più librerie a cui accedono. Tuttavia, questo problema può essere superato attraverso una facciata progettata in modo intelligente che sfrutta la funzionalità di eccezioni concatenate di Java e il rilancio delle eccezioni per ridurre notevolmente il numero di eccezioni che devono essere gestite preservando l'eccezione originale generata.

Conclusione

Le eccezioni controllate sono buone o sono cattive? In altre parole, i programmatori dovrebbero essere costretti a gestire le eccezioni controllate o dovrebbero avere l'opportunità di ignorarle? Mi piace l'idea di applicare un software più robusto. Tuttavia, penso anche che il meccanismo di gestione delle eccezioni di Java debba evolversi per renderlo più intuitivo per i programmatori. Ecco un paio di modi per migliorare questo meccanismo: