Il caso contro le eccezioni verificate


456

Da diversi anni non riesco a ottenere una risposta decente alla seguente domanda: perché alcuni sviluppatori sono così contrari alle eccezioni verificate? Ho avuto numerose conversazioni, letto cose sui blog, letto cosa diceva Bruce Eckel (la prima persona che ho visto parlare contro di loro).

Attualmente sto scrivendo un nuovo codice e prestando molta attenzione a come gestisco le eccezioni. Sto cercando di vedere il punto di vista della folla "Non ci piacciono le eccezioni controllate" e ancora non riesco a vederlo.

Ogni conversazione che ho termina con la stessa domanda senza risposta ... fammi impostare:

In generale (da come è stato progettato Java),

  • Error è per cose che non dovrebbero mai essere scoperte (la VM ha un'allergia alle arachidi e qualcuno ha lasciato cadere un barattolo di arachidi)
  • RuntimeException è per cose che il programmatore ha fatto di sbagliato (il programmatore ha abbandonato la fine di un array)
  • Exception(eccetto RuntimeException) è per cose che sono fuori dal controllo del programmatore (il disco si riempie durante la scrittura nel file system, è stato raggiunto il limite di gestione dei file per il processo e non è possibile aprire altri file)
  • Throwable è semplicemente il genitore di tutti i tipi di eccezione.

Un argomento comune che sento è che se si verifica un'eccezione, tutto ciò che lo sviluppatore farà è uscire dal programma.

Un altro argomento comune che sento è che le eccezioni controllate rendono più difficile il refactoring del codice.

Per l'argomento "Tutto ciò che sto per fare è uscire" dico che anche se stai uscendo devi visualizzare un messaggio di errore ragionevole. Se stai solo puntando sulla gestione degli errori, i tuoi utenti non saranno eccessivamente felici quando il programma esce senza una chiara indicazione del perché.

Per la folla "rende difficile il refactoring", ciò indica che non è stato scelto il giusto livello di astrazione. Invece di dichiarare un metodo che genera un IOException, il IOExceptiondovrebbe essere trasformato in un'eccezione più adatta a ciò che sta succedendo.

Non ho problemi con il wrapping di Main con catch(Exception)(o in alcuni casi catch(Throwable)per garantire che il programma possa uscire con garbo - ma prendo sempre le eccezioni specifiche di cui ho bisogno. Ciò mi consente di visualizzare almeno messaggio di errore.

La domanda a cui le persone non rispondono mai è questa:

Se lanci RuntimeException sottoclassi anziché Exception sottoclassi, come fai a sapere cosa dovresti catturare?

Se la risposta è "cattiva", Exceptionallora hai a che fare con gli errori del programmatore allo stesso modo delle eccezioni del sistema. Mi sembra sbagliato.

Se catturi, Throwablestai trattando le eccezioni del sistema e gli errori della macchina virtuale (e simili) allo stesso modo. Mi sembra sbagliato.

Se la risposta è che catturi solo le eccezioni che conosci vengono generate, allora come fai a sapere quali vengono generate? Cosa succede quando il programmatore X lancia una nuova eccezione e ha dimenticato di catturarla? Mi sembra molto pericoloso.

Direi che un programma che mostra una traccia dello stack è sbagliato. Le persone a cui non piacciono le eccezioni controllate non si sentono così?

Quindi, se non ti piacciono le eccezioni controllate, puoi spiegare perché no E rispondere alla domanda a cui non viene data risposta, per favore?

Modifica: non sto cercando consigli su quando utilizzare entrambi i modelli, quello che sto cercando è perché le persone si estendono RuntimeExceptionperché non amano estendersi da Exceptione / o perché prendono un'eccezione e quindi ricominciano a lanciare RuntimeExceptioninvece di aggiungere il loro metodo. Voglio capire la motivazione per non gradire le eccezioni verificate.


46
Non penso che sia completamente soggettivo - è una funzionalità linguistica progettata per avere un uso specifico, piuttosto che per tutti decidere cosa fare per se stessi. E non è particolarmente polemico, si rivolge in anticipo a confutazioni specifiche che le persone avrebbero potuto facilmente inventare.
Gareth,

6
Dai. Visto come una caratteristica della lingua, questo argomento è stato e può essere affrontato in modo obiettivo.
Kurt Schelfthout,

6
@cletus "rispondendo alla tua domanda" se avessi la risposta non avrei posto la domanda!
TofuBeer,

8
Ottima domanda In C ++ non ci sono eccezioni verificate e, a mio avviso, rende inutilizzabile la funzione di eccezione. Ti ritrovi in ​​una situazione in cui devi catturare ogni singola chiamata di funzione che fai, perché non sai se potrebbe lanciare qualcosa.
Dimitri C.

4
L'argomento più forte che conosco per le eccezioni verificate è che in origine non erano presenti in Java e che quando furono introdotti scoprirono centinaia di bug nel JDK. Questo è in qualche modo precedente a Java 1.0. Personalmente non sarei senza di loro, e non sono d'accordo con Bruce Eckel e altri su questo.
Marchese di Lorne,

Risposte:


277

Penso di aver letto la stessa intervista a Bruce Eckel che hai fatto - e mi ha sempre infastidito. In realtà, l'argomentazione ha sostenuto l'argomentazione (se questo è davvero il post di cui stai parlando) Anders Hejlsberg, il genio della SM dietro a .NET e C #.

http://www.artima.com/intv/handcuffs.html

Anche se sono un fan di Hejlsberg e del suo lavoro, questa discussione mi ha sempre reso falso. Fondamentalmente si riduce a:

"Le eccezioni verificate sono cattive perché i programmatori li abusano sempre catturandoli e scartandoli, il che porta a nascondere e ignorare i problemi che altrimenti verrebbero presentati all'utente".

Con "altrimenti presentato all'utente" intendo che se usi un'eccezione di runtime il programmatore pigro lo ignorerà (invece di catturarlo con un blocco catch vuoto) e l'utente lo vedrà.

Il sommario del sommario dell'argomento è che "I programmatori non li useranno correttamente e non usarli correttamente è peggio che non averli" .

C'è del vero in questo argomento e, in effetti, sospetto che la motivazione di Goslings per non mettere le sostituzioni degli operatori in Java provenga da un argomento simile: confondono il programmatore perché sono spesso abusati.

Ma alla fine, trovo un argomento fasullo di Hejlsberg e forse uno post-hoc creato per spiegare la mancanza piuttosto che una decisione ben ponderata.

Direi che mentre l'uso eccessivo di eccezioni controllate è una cosa negativa e tende a condurre a una gestione sciatta da parte degli utenti, ma il loro corretto utilizzo consente al programmatore API di offrire grandi vantaggi al programmatore client API.

Ora il programmatore dell'API deve fare attenzione a non gettare eccezioni verificate in tutto il luogo, o semplicemente infastidiranno il programmatore del client. Il programmatore client molto pigro ricorrerà alla cattura (Exception) {}mentre Hejlsberg avverte e tutti i benefici andranno persi e ne conseguirà l'inferno. Ma in alcune circostanze, non c'è proprio alcun sostituto per una buona eccezione verificata.

Per me, il classico esempio è l'API di apertura file. Ogni linguaggio di programmazione nella storia dei linguaggi (almeno sui file system) ha un'API da qualche parte che ti consente di aprire un file. E ogni programmatore client che utilizza questa API sa di dover affrontare il caso in cui il file che sta tentando di aprire non esiste. Fammi riformulare: Ogni programmatore client che utilizza questa API dovrebbe sapere che devono occuparsi di questo caso. E c'è il problema: il programmatore API può aiutarli a sapere che dovrebbero occuparsene commentando da soli o possono davvero insistere il cliente lo affronti.

In C il linguaggio dice qualcosa di simile

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

dove fopenindica il fallimento restituendo 0 e C (stupidamente) ti permette di trattare 0 come un valore booleano e ... Fondamentalmente, impari questo idioma e stai bene. Ma cosa succede se sei un noob e non hai imparato il linguaggio. Quindi, ovviamente, inizi con

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

e impara nel modo più duro.

Si noti che stiamo parlando solo di linguaggi fortemente tipizzati qui: c'è una chiara idea di cosa sia un'API in un linguaggio fortemente tipizzato: è una smorgasbord di funzionalità (metodi) da usare con un protocollo chiaramente definito per ognuno.

Quel protocollo chiaramente definito è in genere definito da una firma del metodo. Qui fopen richiede di passargli una stringa (o un carattere * nel caso di C). Se gli dai qualcos'altro ottieni un errore di compilazione. Non hai seguito il protocollo: non stai utilizzando correttamente l'API.

In alcune lingue (oscure) anche il tipo restituito fa parte del protocollo. Se provi a chiamare l'equivalente difopen() in alcune lingue senza assegnarlo a una variabile, otterrai anche un errore di compilazione (puoi farlo solo con le funzioni void).

Il punto che sto cercando di sottolineare è che: in un linguaggio tipicamente statico il programmatore API incoraggia il client a utilizzare correttamente l'API impedendo la compilazione del codice client in caso di errori evidenti.

(In un linguaggio tipizzato in modo dinamico, come Ruby, puoi passare qualsiasi cosa, dire un float, come il nome del file - e verrà compilato. Perché disturbare l'utente con le eccezioni verificate se non hai nemmeno intenzione di controllare gli argomenti del metodo. gli argomenti fatti qui si applicano solo alle lingue tipicamente statiche.)

Quindi, per quanto riguarda le eccezioni controllate?

Bene, ecco una delle API Java che puoi usare per aprire un file.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Vedi quella cattura? Ecco la firma per quel metodo API:

public FileInputStream(String name)
                throws FileNotFoundException

Si noti che FileNotFoundExceptionè un'eccezione controllata .

Il programmatore API ti sta dicendo questo: "Puoi usare questo costruttore per creare un nuovo FileInputStream ma tu

a) deve passare il nome del file come stringa
b) deve accettare la possibilità che il file non venga trovato in fase di esecuzione "

E questo è il punto per quanto mi riguarda.

La chiave è fondamentalmente ciò che la domanda afferma come "Cose che sono fuori dal controllo del programmatore". Il mio primo pensiero è stato che lui / lei significhi cose che sono fuori dal controllo dei programmatori API . Ma in realtà, le eccezioni controllate se usate correttamente dovrebbero essere realmente per cose che sono fuori dal controllo sia del programmatore client sia del programmatore API. Penso che questa sia la chiave per non abusare delle eccezioni verificate.

Penso che l'apertura del file illustri bene il punto. Il programmatore API sa che potresti dare loro un nome file che risulta essere inesistente al momento in cui viene chiamata l'API e che non saranno in grado di restituirti ciò che volevi, ma dovranno lanciare un'eccezione. Sanno anche che questo accadrà abbastanza regolarmente e che il programmatore del client potrebbe aspettarsi che il nome del file sia corretto al momento in cui ha scritto la chiamata, ma potrebbe essere sbagliato in fase di esecuzione per ragioni indipendenti dalla sua volontà.

Quindi l'API lo rende esplicito: ci saranno casi in cui questo file non esiste al momento in cui mi chiami e tu hai dannatamente meglio affrontarlo.

Ciò sarebbe più chiaro con una contro-causa. Immagina di scrivere un'API per la tabella. Ho il modello di tabella da qualche parte con un'API che include questo metodo:

public RowData getRowData(int row) 

Ora come programmatore API so che ci saranno casi in cui alcuni client passano un valore negativo per la riga o un valore di riga al di fuori della tabella. Quindi potrei essere tentato di lanciare un'eccezione controllata e costringere il cliente a gestirlo:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Non lo chiamerei davvero "Controllato" ovviamente.)

Questo è un cattivo uso delle eccezioni verificate. Il codice client sarà pieno di chiamate per recuperare i dati delle righe, ognuno dei quali dovrà utilizzare un tentativo / cattura, e per cosa? Riferiranno all'utente che è stata cercata la riga sbagliata? Probabilmente no, poiché qualunque sia l'interfaccia utente che circonda la mia vista tabella, non dovrebbe consentire all'utente di entrare in uno stato in cui viene richiesta una riga illegale. Quindi è un bug da parte del programmatore client.

Il programmatore API può ancora prevedere che il client codificherà tali bug e dovrebbe gestirlo con un'eccezione di runtime come un IllegalArgumentException.

Con un'eccezione verificata getRowData, questo è chiaramente un caso che porterà al programmatore pigro di Hejlsberg semplicemente aggiungendo catture vuote. Quando ciò accade, i valori delle righe illegali non saranno evidenti nemmeno per il tester o il debug dello sviluppatore del client, ma porteranno a errori a catena di cui è difficile individuare l'origine. I razzi Arianne esploderanno dopo il lancio.

Ok, quindi ecco il problema: sto dicendo che l'eccezione selezionata FileNotFoundExceptionnon è solo una buona cosa, ma uno strumento essenziale nella casella degli strumenti dei programmatori API per definire l'API nel modo più utile per il programmatore client. Ma questo CheckedInvalidRowNumberExceptionè un grosso inconveniente, che porta a una cattiva programmazione e dovrebbe essere evitato. Ma come dire la differenza.

Immagino che non sia una scienza esatta e suppongo che sia alla base e forse giustifichi in una certa misura l'argomento di Hejlsberg. Ma non sono felice di buttare qui il bambino con l'acqua del bagno, quindi permettimi di estrarre alcune regole qui per distinguere le buone eccezioni verificate da quelle cattive:

  1. Fuori dal controllo del cliente o Chiuso vs Aperto:

    Le eccezioni verificate devono essere utilizzate solo quando il caso di errore è fuori controllo sia dell'API che del programmatore client. Questo ha a che fare con quanto sia aperto o chiuso il sistema. In un'interfaccia utente limitata in cui il programmatore client ha il controllo, ad esempio, su tutti i pulsanti, i comandi da tastiera ecc. Che aggiungono ed eliminano le righe dalla vista tabella (un sistema chiuso), è un bug di programmazione del client se tenta di recuperare i dati da una riga inesistente. In un sistema operativo basato su file in cui un numero qualsiasi di utenti / applicazioni può aggiungere ed eliminare file (un sistema aperto), è concepibile che il file richiesto dal client sia stato eliminato a loro insaputa, quindi ci si dovrebbe aspettare che lo gestiscano .

  2. Ubiquità:

    Le eccezioni verificate non devono essere utilizzate su una chiamata API effettuata frequentemente dal client. Spesso intendo da molti punti del codice client, non frequentemente in tempo. Quindi un codice client non tende a tentare di aprire molto lo stesso file, ma la vista della mia tabella arriva RowDataovunque con metodi diversi. In particolare, scriverò un sacco di codice come

    if (model.getRowData().getCell(0).isEmpty())

    e sarà doloroso doverlo avvolgere in try / catch ogni volta.

  3. Informare l'utente:

    Le eccezioni verificate devono essere utilizzate nei casi in cui si possa immaginare un utile messaggio di errore che viene presentato all'utente finale. Questo è il "e cosa farai quando succede?" domanda che ho sollevato sopra. Si riferisce anche all'elemento 1. Poiché puoi prevedere che qualcosa al di fuori del tuo sistema API client potrebbe causare la mancata presenza del file, puoi ragionevolmente dirlo all'utente:

    "Error: could not find the file 'goodluckfindingthisfile'"

    Poiché il tuo numero di riga illegale è stato causato da un bug interno e non per colpa dell'utente, in realtà non ci sono informazioni utili che puoi fornire. Se la tua app non lascia cadere eccezioni di runtime nella console, probabilmente finirà per dare loro un brutto messaggio come:

    "Internal error occured: IllegalArgumentException in ...."

    In breve, se non pensi che il tuo programmatore client possa spiegare la tua eccezione in un modo che aiuti l'utente, probabilmente non dovresti usare un'eccezione controllata.

Quindi quelle sono le mie regole. Un po 'inventato, e ci saranno senza dubbio delle eccezioni (per favore aiutatemi a perfezionarle se volete). Ma il mio argomento principale è che ci sono casi in FileNotFoundExceptioncui l'eccezione controllata è una parte importante e utile del contratto API come i tipi di parametro. Quindi non dovremmo rinunciarci solo perché è usato in modo improprio.

Mi dispiace, non intendevo renderlo così lungo e waffly. Vorrei concludere con due suggerimenti:

A: Programmatori API: usa le eccezioni selezionate con parsimonia per preservarne l'utilità. In caso di dubbio, utilizzare un'eccezione non selezionata.

B: Programmatori client: abituati a creare un'eccezione chiusa (google it) all'inizio del tuo sviluppo. JDK 1.4 e versioni successive forniscono un costruttore RuntimeExceptionper questo, ma puoi facilmente crearne uno tuo. Ecco il costruttore:

public RuntimeException(Throwable cause)

Quindi prendi l'abitudine di quando devi gestire un'eccezione controllata e ti senti pigro (o pensi che il programmatore API sia stato troppo zelante nell'usare l'eccezione controllata in primo luogo), non ingoiare solo l'eccezione, avvolgila e riprovare.

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Inseriscilo in uno dei piccoli template di codice IDE e usalo quando ti senti pigro. In questo modo, se hai davvero bisogno di gestire l'eccezione selezionata, sarai costretto a tornare e occuparsene dopo aver visto il problema in fase di esecuzione. Perché, credimi (e Anders Hejlsberg), non tornerai mai più a quel TODO nel tuo

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

110
L'apertura dei file è in realtà un buon esempio di eccezioni verificate assolutamente controproducenti. Perché la maggior parte delle volte il codice che apre il file NON può fare nulla di utile sull'eccezione - è meglio fare diversi strati nello stack di chiamate. L'eccezione selezionata ti costringe a ingombrare le firme del metodo solo in modo da poter fare comunque quello che avresti fatto - gestisci l'eccezione nel punto in cui ha più senso farlo.
Michael Borgwardt,

116
@ Michael: D'accordo si potrebbe generalmente gestire queste eccezioni IO diversi livelli più alti - ma non sento si può negare che come un client API si deve anticipare questi. Per questo motivo, penso che le eccezioni verificate siano appropriate. Sì, dovrai dichiarare "tiri" su ogni metodo su stack di chiamate, ma non sono d'accordo sul fatto che sia un disordine. Sono utili informazioni dichiarative nelle firme del metodo. Stai dicendo: i miei metodi di basso livello potrebbero incorrere in un file mancante, ma te ne lasceranno la gestione. Non vedo nulla da perdere e solo un codice buono e pulito da guadagnare
Rabarbaro,

23
@Rhubarb: +1, risposta molto interessante, mostra argomenti per entrambe le parti. Grande. Informazioni sul tuo ultimo commento: nota che dichiarare i lanci su ogni metodo nello stack di chiamate potrebbe non essere sempre possibile, specialmente se stai implementando un'interfaccia lungo la strada.
R. Martinho Fernandes,

8
Questo è un argomento molto forte (e sono d'accordo con esso in generale), ma c'è un caso in cui non funziona: callback generici. Se ho una libreria che chiama un codice definito dall'utente, non c'è modo (che io sappia, in Java) per quella libreria di propagare le eccezioni verificate dal codice utente che chiama al codice utente che lo chiama. Questo problema si estende anche ad altre parti del sistema di tipi di molte lingue tipizzate staticamente che non supportano tipi generici adeguati. Questa risposta sarebbe migliorata da una menzione di questo (e forse una risposta).
Mankarse,

7
@Rhubarb sbagliato. Le eccezioni controllate erano intese per "contingenze" che potevano e dovevano essere gestite, ma erano sempre incompatibili con le migliori pratiche "lancia presto, cattura tardi". L'apertura di un file è un esempio selezionato, che può essere gestito. La maggior parte delle IOException (ad es. Scrivere un byte), SQLException, RemoteException sono impossibili da gestire in modo significativo. Gli errori o i tentativi dovrebbero essere a livello "aziendale" o "richiesta" e le eccezioni verificate, come utilizzate nelle librerie Java, sono un errore che rende questo difficile. literatejava.com/exceptions/…
Thomas W

184

La cosa sulle eccezioni controllate è che non sono realmente eccezioni per la normale comprensione del concetto. Sono invece valori di ritorno alternativi all'API.

L'idea generale delle eccezioni è che un errore lanciato da qualche parte lungo la catena della chiamata può rompersi ed essere gestito dal codice da qualche parte più in alto, senza che il codice intermedio debba preoccuparsene. Le eccezioni controllate, d'altra parte, richiedono ogni livello di codice tra il lanciatore e il ricevitore per dichiarare di essere a conoscenza di tutte le forme di eccezione che possono attraversarle. In pratica, ciò è leggermente diverso se le eccezioni verificate fossero semplicemente valori di ritorno speciali che il chiamante doveva verificare. ad esempio [pseudocodice].:

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

Poiché Java non può eseguire valori di ritorno alternativi o semplici tuple incorporate come valori di ritorno, le eccezioni verificate sono una risposta ragionevole.

Il problema è che un sacco di codice, comprese grandi parti della libreria standard, usano in modo improprio le eccezioni controllate per condizioni eccezionali reali che potresti voler raggiungere diversi livelli. Perché IOException non è una RuntimeException? In ogni altra lingua posso lasciare che si verifichi un'eccezione IO e se non faccio nulla per gestirla, la mia applicazione si fermerà e otterrò una comoda traccia dello stack da guardare. Questa è la cosa migliore che può succedere.

Forse due metodi dall'esempio che vuoi catturare tutte le IOExceptions dall'intero processo di scrittura in streaming, interrompere il processo e passare al codice di segnalazione errori; in Java non è possibile farlo senza aggiungere "genera IOException" a tutti i livelli di chiamata, anche livelli che a loro volta non eseguono IO. Tali metodi non dovrebbero avere bisogno di conoscere la gestione delle eccezioni; dover aggiungere eccezioni alle loro firme:

  1. aumenta inutilmente l'accoppiamento;
  2. rende le firme dell'interfaccia molto fragili da modificare;
  3. rende il codice meno leggibile;
  4. è così fastidioso che la reazione comune del programmatore è quella di sconfiggere il sistema facendo qualcosa di orribile come "genera l'eccezione", "cattura (eccezione e) {}" o racchiudendo tutto in una RuntimeException (che rende più difficile il debug).

E poi ci sono molte ridicole eccezioni alla libreria come:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

Quando devi ingombrare il tuo codice con un greggio ridicolo come questo, non c'è da meravigliarsi se le eccezioni controllate ricevano un sacco di odio, anche se in realtà si tratta solo di una semplice e scadente progettazione API.

Un altro effetto particolarmente negativo è su Inversion of Control, in cui il componente A fornisce un callback al componente generico B. Il componente A vuole essere in grado di lasciare che un'eccezione passi dal suo callback al punto in cui ha chiamato il componente B, ma non può perché ciò cambierebbe l'interfaccia di callback che è riparata da B. A può farlo solo avvolgendo la vera eccezione in una RuntimeException, che è ancora più la piastra di gestione delle eccezioni da scrivere.

Le eccezioni controllate come implementate in Java e nella sua libreria standard significano boilerplate, boilerplate, boilerplate. In un linguaggio già prolisso, questa non è una vittoria.


14
Nel tuo esempio di codice, sarebbe meglio concatenare le eccezioni in modo che la causa originale possa essere trovata durante la lettura dei registri: throw CanNeverHappenException (e);
Esko Luontola,

5
Non sono d'accordo. Le eccezioni, verificate o meno, sono condizioni eccezionali. Esempio: un metodo che recupera un oggetto tramite HTTP. Il valore restituito è altrimenti l'oggetto o niente, tutte le cose che possono andare male sono eccezionali. Trattarli come valori di ritorno come avviene in C porta solo a confusione e cattiva progettazione.
Mister Smith,

15
@Mister: Quello che sto dicendo è che le eccezioni controllate come implementate in Java si comportano, in pratica, più come valori di ritorno come in C che come le "eccezioni" tradizionali che potremmo riconoscere dal C ++ e da altri linguaggi pre-Java. E che l'IMO questo porta effettivamente a confusione e design scadente.
Bobince,

9
Concordo sul fatto che le librerie standard utilizzano in modo improprio le eccezioni verificate aggiunte definitivamente alla confusione e al cattivo comportamento di cattura. E spesso viene solo da una scarsa documentazione, ad esempio un metodo di smontaggio come disconnect () che genera IOException quando "si verifica un altro errore I / O". Beh, mi stavo disconnettendo! Sto perdendo una maniglia o altra risorsa? Devo riprovare? Senza sapere perché è successo, non posso derivare l'azione che dovrei intraprendere e quindi devo indovinare se dovrei semplicemente ingoiarlo, riprovare o liberare.
Charstar,

14
+1 per "Valori di restituzione alternativi API". Modo interessante di esaminare le eccezioni verificate.
Zsolt Török,

75

Invece di ripassare tutte le (molte) ragioni contro le eccezioni verificate, sceglierò solo una. Ho perso il conto del numero di volte in cui ho scritto questo blocco di codice:

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

Il 99% delle volte non posso farci niente. Alla fine i blocchi eseguono qualsiasi pulizia necessaria (o almeno dovrebbero).

Ho anche perso il conto del numero di volte che ho visto questo:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

Perché? Perché qualcuno doveva affrontarlo ed era pigro. È stato sbagliato? Sicuro. Succede? Assolutamente. E se invece si trattasse di un'eccezione non selezionata? L'app sarebbe appena morta (il che è preferibile ingoiare un'eccezione).

E poi abbiamo un codice esasperante che usa le eccezioni come una forma di controllo del flusso, come java.text.Format fa . Bzzzt. Sbagliato. Un utente che inserisce "abc" in un campo numerico in un modulo non fa eccezione.

Ok, immagino che fossero tre ragioni.


4
Ma se un'eccezione viene rilevata correttamente, è possibile informare l'utente, eseguire altre attività (registro?) Ed uscire dall'applicazione in modo controllato. Concordo sul fatto che alcune parti dell'API avrebbero potuto essere progettate meglio. E per il pigro motivo del programmatore, beh, penso che come programmatore tu sia responsabile al 100% del tuo codice.
Mister Smith,

3
nota che try-catch-rethrow ti consente di specificare un messaggio - di solito lo uso per aggiungere informazioni sul contenuto delle variabili di stato. Un esempio frequente è che IOExceptions aggiunga absolutePathName () del file in questione.
Thorbjørn Ravn Andersen,

15
Penso che gli IDE come Eclipse abbiano molto da incolpare per il numero di volte che hai visto il blocco catch vuoto. Davvero, dovrebbero ricodificarli di default.
artbristol

12
"Il 99% delle volte non posso farci niente" - sbagliato, puoi mostrare all'utente un messaggio che dice "Impossibile connettersi al server" o "L'IO Device non è riuscito", invece di lasciare semplicemente l'app in crash a causa di un piccolo singhiozzo di rete. Entrambi i tuoi esempi sono arte del lavoro di cattivi programmatori. Dovresti attaccare i programmatori sbagliati e non controllare le eccezioni stesse. È come se attaccassi l'insulina per non avermi aiutato con il diabete quando la uso come condimento per l'insalata.
AxiomaticNexus,

2
@YasmaniLlanes Non puoi sempre fare queste cose. A volte hai un'interfaccia a cui aderire. E questo è particolarmente vero quando si progettano buone API gestibili perché non si può semplicemente iniziare a lanciare effetti collaterali dappertutto. Sia questo, sia la complessità che introdurrà, ti morderanno gravemente su larga scala. Quindi sì, il 99% delle volte, non c'è niente da fare al riguardo.
MasterMastic,

50

So che questa è una vecchia domanda, ma ho passato un po 'di tempo a lottare con le eccezioni verificate e ho qualcosa da aggiungere. Per favore, perdonami per la lunghezza!

La mia carne di manzo principale con eccezioni controllate è che rovinano il polimorfismo. È impossibile farli giocare bene con le interfacce polimorfiche.

Prendi la buona vecchia Listinterfaccia Java . Abbiamo implementazioni comuni in memoria come ArrayListe LinkedList. Abbiamo anche la classe scheletrica AbstractListche semplifica la progettazione di nuovi tipi di elenchi. Per un elenco di sola lettura dobbiamo implementare solo due metodi: size()e get(int index).

Questa WidgetListclasse di esempio legge alcuni oggetti di dimensioni fisse di tipo Widget(non mostrati) da un file:

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

Esponendo i widget utilizzando l' Listinterfaccia familiare , è possibile recuperare elementi ( list.get(123)) o iterare un elenco ( for (Widget w : list) ...) senza bisogno di conoscere se WidgetListstesso. Si può passare questo elenco a tutti i metodi standard che utilizzano elenchi generici o racchiuderlo in a Collections.synchronizedList. Il codice che lo utilizza non deve sapere né preoccuparsi se i "Widget" sono costituiti sul posto, provengono da un array o vengono letti da un file o da un database o da tutta la rete o da un futuro relay di sottospazio. Funzionerà ancora correttamente perché l' Listinterfaccia è implementata correttamente.

Solo che non lo è. La classe sopra non viene compilata perché i metodi di accesso ai file possono generare IOExceptionun'eccezione controllata che devi "intercettare o specificare". Non è possibile specificarlo come generato : il compilatore non te lo consentirà perché ciò violerebbe il contratto Listdell'interfaccia. E non esiste un modo utile che WidgetListpossa gestire l'eccezione (come spiegherò più avanti).

Apparentemente l'unica cosa da fare è catturare e ricodificare le eccezioni verificate come un'eccezione non selezionata:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Modifica: Java 8 ha aggiunto una UncheckedIOExceptionclasse proprio per questo caso: per catturare e riproporre IOExceptions oltre i limiti del metodo polimorfico. Il tipo di prova dimostra il mio punto!))

Quindi le eccezioni verificate semplicemente non funzionano in casi come questo. Non puoi buttarli. Idem per un intelligente Mapsupportato da un database o un'implementazione dijava.util.Random una sorgente di entropia quantistica connessa tramite una porta COM. Non appena si tenta di fare qualcosa di nuovo con l'implementazione di un'interfaccia polimorfica, il concetto di eccezioni verificate fallisce. Ma le eccezioni verificate sono così insidiose che non ti lasceranno ancora in pace, perché devi ancora catturare e ricrescere qualsiasi metodo di livello inferiore, ingombrando il codice e ingombrando la traccia dello stack.

Trovo che l'onnipresente Runnable interfaccia sia spesso supportata in questo angolo, se chiama qualcosa che genera eccezioni verificate. Non può generare l'eccezione così com'è, quindi tutto ciò che può fare è ingombrare il codice catturando e ricodificando come RuntimeException.

In realtà, puoi buttare non dichiarato eccezioni controllato se ricorrere a hack. La JVM, in fase di esecuzione, non si preoccupa delle regole di eccezione verificate, quindi dobbiamo ingannare solo il compilatore. Il modo più semplice per farlo è abusare dei generici. Questo è il mio metodo (nome della classe mostrato perché (prima di Java 8) è richiesto nella sintassi chiamante per il metodo generico):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Evviva! Usando questo possiamo lanciare un'eccezione controllata a qualsiasi profondità nella pila senza dichiararla, senza racchiuderla in aRuntimeException , e senza ingombrare la traccia dello stack! Utilizzando nuovamente l'esempio "WidgetList":

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

Sfortunatamente, l'insulto finale delle eccezioni verificate è che il compilatore rifiuta di permetterti di prendere un'eccezione controllata se, a suo avviso errato, non avrebbe potuto essere lanciato. (Le eccezioni non selezionate non hanno questa regola.) Per cogliere l'eccezione generata in modo nascosto dobbiamo fare questo:

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

È un po 'imbarazzante, ma sul lato positivo, è ancora leggermente più semplice del codice per estrarre un'eccezione controllata che è stata racchiusa in un RuntimeException .

Fortunatamente, la throw t;dichiarazione è legale qui, anche se il tipo dit è verificato, grazie a una regola aggiunta in Java 7 sul ricondizionamento delle eccezioni rilevate.


Quando le eccezioni controllate incontrano il polimorfismo, anche il caso opposto è un problema: quando un metodo viene specificato come potenzialmente generare un'eccezione controllata, ma un'implementazione ignorata non lo fa. Ad esempio, tutti OutputStreami writemetodi della classe astratta specificano throws IOException. ByteArrayOutputStreamè una sottoclasse che scrive in un array in memoria anziché in una vera sorgente I / O. I suoi writemetodi ignorati non possono causare IOExceptions, quindi non hannothrows clausole e puoi chiamarli senza preoccuparti del requisito catch-or-specific.

Tranne che non sempre. Supponiamo che Widgetabbia un metodo per salvarlo su uno stream:

public void writeTo(OutputStream out) throws IOException;

Dichiarare questo metodo per accettare una pianura OutputStreamè la cosa giusta da fare, quindi può essere usato polimorficamente con tutti i tipi di output: file, database, la rete e così via. E array in memoria. Con un array in memoria, tuttavia, esiste un requisito spurio per gestire un'eccezione che non può effettivamente accadere:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

Come al solito, le eccezioni controllate si frappongono. Se le variabili vengono dichiarate come tipo di base con requisiti di eccezione più aperti, è necessario aggiungere gestori per tali eccezioni anche se si sa che non si verificheranno nella propria applicazione.

Ma aspetta, le eccezioni verificate sono in realtà così fastidiose, che non ti lasceranno nemmeno fare il contrario! Immagina di essere al momento in grado di catturare qualsiasi IOExceptionlancio generato dalle writechiamate su unOutputStream , ma vuoi cambiare il tipo dichiarato della variabile in un ByteArrayOutputStream, il compilatore ti rimprovererà per aver tentato di catturare un'eccezione controllata che dice che non può essere generata.

Tale regola provoca alcuni problemi assurdi. Ad esempio, uno dei tre writemetodi di nonOutputStream viene ignorato da . In particolare, è un metodo pratico che scrive l'intero array chiamando con un offset di 0 e la lunghezza dell'array. ignora il metodo a tre argomenti ma eredita il metodo di convenienza a un argomento così com'è. Il metodo ereditato fa esattamente la cosa giusta, ma include una clausola indesiderata . Questa è stata forse una svista nella progettazione diByteArrayOutputStreamwrite(byte[] data)write(byte[] data, int offset, int length)ByteArrayOutputStreamthrowsByteArrayOutputStream , ma non possono mai risolverlo perché infrange la compatibilità del codice sorgente con qualsiasi codice che rileva l'eccezione: l'eccezione che non ha mai, non è mai e non verrà mai lanciata!

Questa regola è fastidiosa anche durante l'editing e il debug. Ad esempio, a volte commenterò temporaneamente una chiamata al metodo e, se avesse potuto generare un'eccezione verificata, il compilatore ora si lamenterà dell'esistenza del locale trye dei catchblocchi. Quindi devo commentare anche quelli, e ora quando si modifica il codice all'interno, l'IDE rientra a un livello errato perché gli {e }vengono commentati. Gah! È una piccola lamentela, ma sembra che l'unica cosa che le eccezioni controllate siano mai state causate da problemi.


Ho quasi finito. La mia frustrazione finale con le eccezioni verificate è che nella maggior parte dei siti di chiamata , non c'è nulla di utile che puoi fare con loro. Idealmente, quando qualcosa va storto, avremmo un gestore competente specifico dell'applicazione che può informare l'utente del problema e / o terminare o ritentare l'operazione nel modo appropriato. Solo un gestore in cima alla pila può farlo perché è l'unico che conosce l'obiettivo generale.

Invece otteniamo il seguente linguaggio, che dilaga come un modo per chiudere il compilatore:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

In una GUI o in un programma automatizzato il messaggio stampato non verrà visualizzato. Peggio ancora, continua con il resto del codice dopo l'eccezione. L'eccezione non è in realtà un errore? Quindi non stamparlo. Altrimenti, qualcos'altro sta per esplodere in un momento, entro il quale l'oggetto dell'eccezione originale sparirà. Questo idioma non è migliore di BASIC On Error Resume Nexto PHP error_reporting(0);.

Chiamare un qualche tipo di classe logger non è molto meglio:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

È altrettanto pigro e.printStackTrace();e continua a lavorare con il codice in uno stato indeterminato. Inoltre, la scelta di un particolare sistema di registrazione o di un altro gestore è specifica dell'applicazione, quindi fa male il riutilizzo del codice.

Ma aspetta! Esiste un modo semplice e universale per trovare il gestore specifico dell'applicazione. È più in alto nello stack di chiamate (o è impostato come gestore di eccezioni non rilevate del thread ). Quindi nella maggior parte dei posti, tutto ciò che devi fare è gettare l'eccezione più in alto nello stack . Ad es throw e;. Le eccezioni verificate si intromettono.

Sono sicuro che le eccezioni verificate sembravano una buona idea quando il linguaggio è stato progettato, ma in pratica le ho trovate tutte fastidiose e senza alcun vantaggio.


Per il tuo metodo di dimensione con WidgetList, memorizzerei la dimensione in cache in una variabile e la imposterei nel costruttore. Il costruttore è libero di generare un'eccezione. Questo non funzionerà se il file cambia mentre si utilizza WidgetList, il che probabilmente sarebbe male se lo facesse.
TofuBeer,

2
SomeStupidExceptionOmgWhoCares beh qualcuno si è preso abbastanza cura di buttarlo. Quindi o non avrebbe mai dovuto essere lanciato (cattivo design) o dovresti davvero gestirlo. Lo stesso vale per la cattiva implementazione di una classe pre-1.0 (il flusso di output dell'array di byte) in cui la progettazione era, purtroppo, cattiva.
TofuBeer,

2
Il linguaggio corretto sarebbe stato una direttiva che catturerebbe qualsiasi specifica eccezione generata da chiamate di subroutine nidificate e le riproponesse racchiuse in un RuntimeException. Si noti che una routine può essere dichiarata simultaneamente come throws IOExceptione tuttavia specificare anche che qualsiasi IOExceptionlanciata da una chiamata nidificata deve essere considerata imprevista e racchiusa.
supercat il

15
Sono uno sviluppatore professionista C # con qualche esperienza Java che si è imbattuto in questo post. Sono sconcertato dal motivo per cui qualcuno sosterrebbe questo bizzarro comportamento. In .NET se voglio prendere un tipo specifico di eccezione, posso prenderlo. Se voglio lasciarlo buttare in pila, non c'è niente da fare. Vorrei che Java non fosse così eccentrico. :)
aikeru,

Riguardo "a volte commenterò temporaneamente una chiamata di metodo" - ho imparato a usarlo if (false)per questo. Evita il problema della clausola di lancio e l'avvertimento mi aiuta a tornare indietro più velocemente. +++ Detto questo, sono d'accordo con tutto ciò che hai scritto. Le eccezioni verificate hanno un certo valore, ma questo valore è trascurabile rispetto al loro costo. Quasi sempre si intromettono.
maaartinus,

45

Bene, non si tratta di mostrare una stack stack o di un crash silenzioso. Si tratta di essere in grado di comunicare errori tra i livelli.

Il problema con le eccezioni controllate è che incoraggiano le persone a ingoiare dettagli importanti (vale a dire, la classe di eccezione). Se scegli di non ingoiare quel dettaglio, devi continuare ad aggiungere dichiarazioni di lancio nell'intera app. Ciò significa 1) che un nuovo tipo di eccezione influirà su molte firme di funzioni e 2) potresti perdere un'istanza specifica dell'eccezione che in realtà vuoi-catturare (diciamo che apri un file secondario per una funzione che scrive i dati in un Il file secondario è facoltativo, quindi puoi ignorare i suoi errori, ma a causa della firma throws IOExceptionè facile trascurarlo.

Attualmente sto affrontando questa situazione in un'applicazione. Abbiamo riconfezionato quasi eccezioni come AppSpecificException. Questo ha reso le firme davvero pulite e non abbiamo dovuto preoccuparci di esplodere throwsnelle firme.

Naturalmente, ora dobbiamo specializzare la gestione degli errori ai livelli più alti, implementando la logica dei tentativi e simili. Tutto è AppSpecificException, tuttavia, quindi non possiamo dire "Se viene generata una IOException, riprovare" o "Se viene generata ClassNotFound, interrompere completamente". Non abbiamo un modo affidabile per arrivare alla vera eccezione perché le cose vengono riconfezionate più volte mentre passano tra il nostro codice e il codice di terze parti.

Questo è il motivo per cui sono un grande fan della gestione delle eccezioni in Python. Puoi catturare solo le cose che vuoi e / o puoi gestire. Tutto il resto bolle come se l'avessi riprovato da solo (cosa che hai fatto comunque).

Ho scoperto, più volte, e in tutto il progetto che ho citato, che la gestione delle eccezioni rientra in 3 categorie:

  1. Cattura e gestisci un'eccezione specifica . Questo per implementare la logica dei tentativi, per esempio.
  2. Cattura e rilancia altre eccezioni. Tutto ciò che accade qui è in genere la registrazione, ed è di solito un messaggio banale come "Impossibile aprire $ nomefile". Questi sono errori di cui non puoi fare nulla; solo i livelli superiori ne sanno abbastanza per gestirlo.
  3. Cattura tutto e visualizza un messaggio di errore. Questo di solito è alla radice di un dispatcher e tutto ciò che fa si assicura che possa comunicare l'errore al chiamante tramite un meccanismo di non eccezione (finestra di dialogo popup, marshalling di un oggetto Errore RPC, ecc.).

5
Avresti potuto creare sottoclassi specifiche di AppSpecificException per consentire la separazione mantenendo le firme del metodo semplice.
Thorbjørn Ravn Andersen,

1
Un'aggiunta molto importante al punto 2 è che consente di AGGIUNGERE INFORMAZIONI all'eccezione rilevata (ad esempio annidando in RuntimeException). È molto, molto meglio avere il nome del file non trovato nella traccia dello stack, piuttosto che nascosto in un file di registro.
Thorbjørn Ravn Andersen,

1
Fondamentalmente il tuo argomento è "Gestire le eccezioni è stancante, quindi preferirei non affrontarlo". Man mano che l'eccezione si dissolve, perde significato e la creazione del contesto è praticamente inutile. Come progettista di un'API che dovresti chiarire contrattualmente su cosa ci si può aspettare quando le cose vanno male, se il mio programma si arresta in modo anomalo perché non sono stato informato che questa o quell'eccezione può "gonfiarsi", allora tu, come progettista, fallisci e come a causa del tuo fallimento il mio sistema non è stabile come può essere.
Newtopian,

4
Non è affatto quello che sto dicendo. La tua ultima frase è davvero d'accordo con me. Se tutto è racchiuso in AppSpecificException, allora non si gonfia (e il significato / contesto è perso) e, sì, il client API non viene informato - questo è esattamente ciò che accade con le eccezioni verificate (poiché sono in Java) , perché le persone non vogliono avere a che fare con funzioni con molte throwsdichiarazioni.
Richard Levasseur,

6
@Newtopian: le eccezioni possono essere gestite in gran parte solo a livello "commerciale" o "richiesta". Ha senso fallire o riprovare con granularità elevata, non per ogni piccolo potenziale fallimento. Per questo motivo, le migliori pratiche di gestione delle eccezioni sono riassunte come "lanciare in anticipo, prendere in ritardo". Le eccezioni verificate rendono più difficile gestire l'affidabilità al livello corretto e incoraggiano un gran numero di blocchi di cattura con codici errati. literatejava.com/exceptions/…
Thomas W

24

SNR

Innanzitutto, le eccezioni verificate riducono il "rapporto segnale-rumore" per il codice. Anders Hejlsberg parla anche della programmazione imperativa vs dichiarativa che è un concetto simile. Considerare comunque i seguenti frammenti di codice:

Aggiornamento dell'interfaccia utente da thread non UI in Java:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Aggiornamento dell'interfaccia utente da thread non UI in C #:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Il che mi sembra molto più chiaro. Quando inizi a svolgere sempre più operazioni di interfaccia utente in Swing, le eccezioni controllate iniziano a diventare davvero fastidiose e inutili.

Pausa di prigione

Per implementare anche le implementazioni più elementari, come l'interfaccia Elenco Java, sono state verificate le eccezioni come strumento di progettazione per contratto. Si consideri un elenco supportato da un database o un filesystem o qualsiasi altra implementazione che genera un'eccezione controllata. L'unica implementazione possibile è catturare l'eccezione controllata e riproporla come eccezione non selezionata:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

E ora devi chiederti qual è il punto di tutto quel codice? Le eccezioni verificate aggiungono solo rumore, l'eccezione è stata rilevata ma non gestita e la progettazione per contratto (in termini di eccezioni controllate) è stata analizzata.

Conclusione

  • La cattura delle eccezioni è diversa dalla loro gestione.
  • Le eccezioni selezionate aggiungono rumore al codice.
  • La gestione delle eccezioni funziona bene in C # senza di esse.

Ho scritto un blog su questo in precedenza .


3
Nell'esempio sia Java che C # stanno solo permettendo alle eccezioni di propagarsi senza gestirle (Java via IllegalStateException). La differenza è che potresti voler gestire FileNotFoundException ma è improbabile che sia utile la gestione di InvocationTargetException o InterruptedException.
Luke Quinane,

3
E nel modo C # come faccio a sapere che può verificarsi l'eccezione I / O? Inoltre, non lancerei mai un'eccezione dalla corsa ... Considero quell'abuso la gestione delle eccezioni. Ci dispiace ma per quella parte del tuo codice posso ancora vedere il tuo lato di esso.
TofuBeer,

7
Ci stiamo arrivando :-) Quindi, ad ogni nuova versione di un'API, devi controllare tutte le tue chiamate e cercare eventuali nuove eccezioni che potrebbero accadere? Ciò può accadere facilmente con le API interne di un'azienda poiché non devono preoccuparsi della retrocompatibilità.
TofuBeer,

3
Intendevi ridurre il rapporto segnale-rumore?
neo2862,

3
@TofuBeer Non è stato forzato l'aggiornamento del codice dopo che l'interfaccia di un'API di base è cambiata? Se avessi avuto solo eccezioni non controllate lì, avresti finito con un programma rotto / incompleto senza saperlo.
Francois Bourgeois,

23

Artima ha pubblicato un'intervista con uno degli architetti di .NET, Anders Hejlsberg, che tratta in modo approfondito gli argomenti contro le eccezioni verificate. Un breve assaggio:

La clausola dei tiri, almeno nel modo in cui è implementata in Java, non ti obbliga necessariamente a gestire le eccezioni, ma se non le gestisci, ti obbliga a riconoscere con precisione quali eccezioni potrebbero passare. Richiede di catturare le eccezioni dichiarate o di inserirle nella propria clausola sui tiri. Per aggirare questo requisito, le persone fanno cose ridicole. Ad esempio, decorano ogni metodo con "genera l'eccezione". Questo ha annullato completamente la funzionalità e hai appena fatto scrivere al programmatore più schifezze ingenue. Questo non aiuta nessuno.


2
In effetti, il capo architetto.
Trappola

18
Ho letto che, per me, il suo argomento si riduce a "ci sono programmatori cattivi là fuori".
TofuBeer,

6
TofuBeer, per niente. Il punto è che molte volte non sai cosa fare con l'eccezione generata dal metodo chiamato e il caso a cui sei veramente interessato non è nemmeno menzionato. Apri un file, ottieni un'eccezione IO, ad esempio ... non è un mio problema, quindi lo vomito. Ma il metodo di chiamata di livello superiore vorrà solo interrompere l'elaborazione e informare l'utente che c'è un problema sconosciuto. L'eccezione selezionata non ha aiutato affatto. È stata una delle milioni di cose strane che possono accadere.
Dan Rosenstark,

4
@yar, se non ti piace l'eccezione selezionata, fai un "lancio nuova RuntimeException (" non ci aspettavamo questo quando facevamo Foo.bar () ", e)" e finisci.
Thorbjørn Ravn Andersen,

4
@ ThorbjørnRavnAndersen: un punto debole di progettazione fondamentale in Java, che purtroppo .net è stato copiato, è che usa il tipo di eccezione come mezzo principale per decidere se debba essere attuato, e il mezzo principale per indicare il tipo generale di cosa che è andato storto, quando in realtà le due questioni sono in gran parte ortogonali. Ciò che conta non è ciò che è andato storto, ma in che cosa si trovano gli oggetti di stato. Inoltre, sia .net che Java assumono per impostazione predefinita che agire e risolvere un'eccezione sono generalmente la stessa cosa, quando in realtà sono spesso diversi.
supercat,

20

Inizialmente sono stato d'accordo con te, poiché sono sempre stato a favore delle eccezioni verificate e ho iniziato a pensare al motivo per cui non mi piace non aver verificato le eccezioni in .Net. Ma poi mi sono reso conto che non mi infatto come eccezioni controllate.

Per rispondere alla tua domanda, sì, mi piacciono i miei programmi per mostrare tracce dello stack, preferibilmente molto brutte. Voglio che l'applicazione esploda in un orribile mucchio dei più brutti messaggi di errore che potresti mai voler vedere.

E il motivo è perché, se lo fa, devo ripararlo e devo ripararlo immediatamente. Voglio sapere immediatamente che c'è un problema.

Quante volte in realtà gestisci le eccezioni? Non sto parlando di catturare le eccezioni - sto parlando di gestirle? È troppo facile scrivere quanto segue:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

E so che puoi dire che è una cattiva pratica e che "la risposta" è fare qualcosa con l'eccezione (fammi indovinare, registrarlo?), Ma nel mondo reale (tm), la maggior parte dei programmatori semplicemente non lo fa esso.

Quindi sì, non voglio catturare eccezioni se non devo farlo e voglio che il mio programma esploda in modo spettacolare quando faccio un casino. Il fallimento peggiore è il risultato peggiore possibile.


Java ti incoraggia a fare questo genere di cose, in modo da non dover aggiungere ogni tipo di eccezione a ogni firma del metodo.
yfeldblum,

17
Divertente ... da quando ho abbracciato correttamente le eccezioni verificate e le ho usate in modo appropriato i miei programmi hanno smesso di esplodere in un'enorme pila fumante di insoddisfazione del cliente in faccia. Se durante lo sviluppo hai una brutta traccia dello stack, anche il cliente è tenuto a prenderli. D'amore per vedere la sua faccia quando vede ArrayIndexOutOfBoundsException con una traccia dello stack alta un miglio sul suo sistema in crash invece di una piccola notifica del vassoio che dice che la configurazione del colore per il pulsante XYZ non può essere analizzata, quindi il valore predefinito è stato usato invece con il software che ronzava felicemente lungo
Newtopian,

1
Forse ciò di cui Java ha bisogno è una dichiarazione "cantHandle" che specifichi che un metodo o un blocco try / catch di codice non è preparato per gestire una particolare eccezione che si verifica al suo interno e che tale eccezione che si verifica tramite mezzi diversi da un esplicito il lancio all'interno di quel metodo (al contrario di un metodo chiamato) dovrebbe essere automaticamente racchiuso e riproposto in una RuntimeException. IMHO, le eccezioni controllate dovrebbero raramente propagarsi nello stack di chiamate senza essere avvolte.
supercat

3
@Newtopian - Scrivo server e software ad alta affidabilità e lo faccio da 25 anni. I miei programmi non sono mai esplosi e lavoro con sistemi finanziari e militari ad alta disponibilità, riprova e riconnetti. Ho una base oggettiva assoluta per preferire le eccezioni di runtime. Le eccezioni verificate rendono più difficile seguire le migliori pratiche corrette "lanciare presto, prendere tardi". La corretta affidabilità e la gestione degli errori sono a livello "aziendale", "connessione" o "richiesta". (O occasionalmente durante l'analisi dei dati). Le eccezioni controllate impediscono di farlo nel modo giusto.
Thomas W,

1
Le eccezioni di cui stai parlando qui sono RuntimeExceptionsche in realtà non devi catturare, e sono d'accordo che dovresti lasciar saltare il programma. Le eccezioni che dovresti sempre catturare e gestire sono le eccezioni verificate come IOException. Se ottieni un IOException, non c'è niente da correggere nel tuo codice; il tuo programma non dovrebbe esplodere solo perché c'è stato un singhiozzo di rete.
AxiomaticNexus,

20

L'articolo Eccezioni Java efficaci spiega bene quando utilizzare le eccezioni non selezionate e quando utilizzare le eccezioni verificate. Ecco alcune citazioni da quell'articolo per evidenziare i punti principali:

Contingenza: una condizione attesa che richiede una risposta alternativa da un metodo che può essere espressa in termini di scopo previsto dal metodo. Il chiamante del metodo si aspetta questo tipo di condizioni e ha una strategia per affrontarle.

Errore: una condizione non pianificata che impedisce a un metodo di raggiungere lo scopo previsto che non può essere descritto senza riferimento all'implementazione interna del metodo.

(SO non consente le tabelle, quindi potresti voler leggere quanto segue dalla pagina originale ...)

Contingenza

  • È considerato: una parte del design
  • Si prevede che accada: regolarmente ma raramente
  • A chi importa: il codice a monte che invoca il metodo
  • Esempi: modalità di ritorno alternative
  • Migliore mappatura: un'eccezione controllata

Colpa

  • È considerato: una brutta sorpresa
  • Si prevede che accada: mai
  • Chi se ne frega: le persone che hanno bisogno di risolvere il problema
  • Esempi: bug di programmazione, malfunzionamenti hardware, errori di configurazione, file mancanti, server non disponibili
  • Migliore mappatura: un'eccezione non selezionata

So quando usarli, voglio sapere perché le persone che non seguono quel consiglio ... non seguono quel consiglio :-)
TofuBeer

Cosa sono i bug di programmazione e come distinguerli dai bug di utilizzo ? È un bug di programmazione se l'utente passa al programma argomenti sbagliati? Dal punto di vista Java potrebbe non essere un bug di programmazione ma dal punto di vista dello script di shell è un bug di programmazione. Quindi quali sono gli argomenti non validi args[]? Sono una contingenza o un errore?
ceving il

3
@TofuBeer - Poiché i progettisti delle librerie Java, hanno scelto di inserire tutti i tipi di guasti irrecuperabili di basso livello come eccezioni verificate quando avrebbero dovuto chiaramente essere deselezionati . FileNotFound è l'unica IOException che dovrebbe essere verificata, ad esempio. Per quanto riguarda JDBC - solo la connessione al database, può ragionevolmente essere considerata una contingenza . Tutte le altre SQLExceptions avrebbero dovuto essere errori e deselezionate. La gestione degli errori dovrebbe essere correttamente al livello "business" o "richiesta" - vedere la best practice "lancia in anticipo, cattura in ritardo". Le eccezioni verificate rappresentano un ostacolo a questo.
Thomas W,

1
C'è un unico enorme difetto nella tua discussione. La "contingenza" NON deve essere gestita mediante eccezioni, ma attraverso valori di restituzione del codice aziendale e del metodo. Eccezioni sono, come dice la parola, situazioni ECCEZIONALI, quindi Difetti.
Matteo Mosca,

@MatteoMosca I codici di ritorno dell'errore tendono a essere ignorati e questo è sufficiente per squalificarli. In realtà, qualsiasi cosa insolita può spesso essere gestita solo da qualche parte nello stack e questo è un caso d'uso per le eccezioni. Potrei immaginare qualcosa come File#openInputStreamtornare Either<InputStream, Problem>- se è questo che intendi, allora potremmo essere d'accordo.
maaartinus,

19

In breve:

Le eccezioni sono una domanda di progettazione API. -- Ne più ne meno.

L'argomento per le eccezioni verificate:

Per capire perché le eccezioni controllate potrebbero non essere una buona cosa, giriamo la domanda e chiediamoci: quando o perché le eccezioni controllate sono interessanti, ovvero perché vorresti che il compilatore imponesse la dichiarazione delle eccezioni?

La risposta è ovvia: a volte è necessario rilevare un'eccezione e ciò è possibile solo se il codice chiamato offre una classe di eccezione specifica per l'errore a cui si è interessati.

Quindi, l'argomento per le eccezioni verificate è che il compilatore costringe i programmatori a dichiarare quali eccezioni vengono generate e, si spera, il programmatore documenterà anche specifiche classi di eccezioni e gli errori che le causano.

In realtà, però, troppo spesso un pacchetto com.acmegenera solo AcmeExceptionsottoclassi piuttosto che specifiche. I chiamanti devono quindi gestire, dichiarare o riprogrammare AcmeExceptions, ma non possono ancora essere certi che si sia AcmeFileNotFoundErrorverificato un evento o un AcmePermissionDeniedError.

Quindi, se ti interessa solo un AcmeFileNotFoundError, la soluzione è quella di presentare una richiesta di funzionalità ai programmatori ACME e dire loro di implementare, dichiarare e documentare quella sottoclasse di AcmeException.

Allora perché preoccuparsi?

Quindi, anche con le eccezioni verificate, il compilatore non può forzare i programmatori a generare eccezioni utili . È ancora solo una questione di qualità dell'API.

Di conseguenza, le lingue senza eccezioni verificate di solito non vanno molto peggio. I programmatori potrebbero essere tentati di lanciare istanze non specifiche di una Errorclasse generale piuttosto che di una AcmeException, ma se si preoccupano della loro qualità API, impareranno a introdurre unAcmeFileNotFoundError dopotutto.

Nel complesso, le specifiche e la documentazione delle eccezioni non sono molto diverse dalle specifiche e dalla documentazione, per esempio, dei metodi ordinari. Anche questa è una domanda di progettazione dell'API e se un programmatore ha dimenticato di implementare o esportare una funzione utile, l'API deve essere migliorata in modo da poter lavorare con essa in modo utile.

Se segui questa linea di ragionamento, dovrebbe essere ovvio che la "seccatura" di dichiarare, catturare e riproporre eccezioni così comuni in linguaggi come Java spesso aggiunge poco valore.

Vale anche la pena notare che la VM Java non ha verificato le eccezioni: solo il compilatore Java le controlla e i file di classe con dichiarazioni di eccezione modificate sono compatibili in fase di esecuzione. La sicurezza della VM Java non viene migliorata dalle eccezioni verificate, ma solo dallo stile di codifica.


4
La tua argomentazione discute contro se stessa. Se "a volte devi catturare un'eccezione" e la qualità dell'API è spesso scadente, senza eccezioni verificate non saprai se il designer ha trascurato di documentare che un determinato metodo genera un'eccezione che deve essere rilevata. Abbinalo al lancio AcmeExceptionpiuttosto che alla AcmeFileNotFoundErrorbuona fortuna per capire cosa hai fatto di sbagliato e dove devi prenderlo. Le eccezioni verificate forniscono ai programmatori un minimo di protezione contro la progettazione errata delle API.
Eva,

1
Il design della libreria Java ha commesso gravi errori. Le "eccezioni controllate" riguardavano contingenze prevedibili e recuperabili, ad esempio file non trovato, mancata connessione. Non sono mai stati pensati o adatti per un fallimento sistemico di basso livello. Sarebbe andato bene forzare l'apertura di un file da controllare, ma non vi sono tentativi o tentativi ragionevoli per non riuscire a scrivere un singolo byte / eseguire una query SQL, ecc. Riprovare o ripristinare sono gestiti correttamente alla richiesta "business" o " "livello, che ha verificato le eccezioni rendono inutilmente difficile. literatejava.com/exceptions/…
Thomas W

17

Negli ultimi tre anni ho lavorato con diversi sviluppatori in applicazioni relativamente complesse. Abbiamo una base di codice che utilizza le eccezioni controllate abbastanza spesso con una corretta gestione degli errori, e altre no.

Finora, ho trovato più facile lavorare con la base di codice con Checked Exceptions. Quando sto usando l'API di qualcun altro, è bello poter vedere esattamente che tipo di condizioni di errore posso aspettarmi quando chiamo il codice e lo gestisco correttamente, sia registrando, visualizzando o ignorando (Sì, ci sono casi validi per ignorare eccezioni, come un'implementazione ClassLoader). Questo dà al codice che sto scrivendo l'opportunità di recuperare. Tutte le eccezioni di runtime che propongo fino a quando non vengono memorizzate nella cache e gestite con un codice di gestione degli errori generico. Quando trovo un'eccezione verificata che non voglio davvero gestire a un livello specifico o che considero un errore di logica di programmazione, la avvolgo in RuntimeException e la lascio sfogliare. Mai, mai ingoiare un'eccezione senza una buona ragione (e le buone ragioni per farlo sono piuttosto scarse)

Quando lavoro con la base di codice che non ha verificato le eccezioni, mi rende un po 'più difficile sapere in anticipo cosa posso aspettarmi quando chiamo la funzione, il che può interrompere alcune cose terribilmente.

Tutto questo è ovviamente una questione di preferenze e abilità degli sviluppatori. Entrambe le modalità di programmazione e gestione degli errori possono essere ugualmente efficaci (o non efficaci), quindi non direi che c'è The One Way.

Tutto sommato, trovo più facile lavorare con Checked Exceptions, specialmente in grandi progetti con molti sviluppatori.


6
Lo faccio. Per me sono una parte essenziale di un contratto. Senza dover essere dettagliato nella documentazione dell'API, posso conoscere rapidamente gli scenari di errore più probabili.
Wayne Hartman,

2
Essere d'accordo. Ho provato la necessità di verificare le eccezioni in .Net una volta quando ho provato a effettuare chiamate di rete. Sapendo che un singhiozzo di rete poteva accadere in qualsiasi momento, ho dovuto leggere tutta la documentazione delle API per scoprire quale eccezione dovessi catturare specificamente per quello scenario. Se C # avesse verificato le eccezioni, l'avrei saputo immediatamente. Altri sviluppatori di C # probabilmente lascerebbero andare in crash l'app da un semplice errore di rete.
AxiomaticNexus,

13

Categorie di eccezioni

Quando parlo di eccezioni, rimando sempre all'articolo del blog sulle eccezioni di Vexing di Eric Lippert . Inserisce eccezioni in queste categorie:

  • Fatale - Queste eccezioni non sono colpa tua : non puoi prevenirle e non puoi gestirle in modo ragionevole. Ad esempio, OutOfMemoryErroroppure ThreadAbortException.
  • Boneheaded - Queste eccezioni sono colpa tua : avresti dovuto prevenirle e rappresentano bug nel tuo codice. Ad esempio ArrayIndexOutOfBoundsException, NullPointerExceptiono qualsiasi IllegalArgumentException.
  • Vexing : queste eccezioni non sono eccezionali , non è colpa tua, non puoi impedirle, ma dovrai affrontarle. Essi sono spesso il risultato di una decisione di design sfortunato, come il lancio NumberFormatExceptionda Integer.parseIntinvece di fornire un Integer.tryParseIntmetodo che restituisce un falso booleano in caso di fallimento di analisi.
  • Esogeno - Queste eccezioni sono generalmente eccezionali , non per colpa tua, non puoi (ragionevolmente) prevenirle, ma devi gestirle . Ad esempio FileNotFoundException,.

Un utente API:

  • non deve gestire eccezioni fatali o stordite .
  • dovrebbe gestire eccezioni fastidiose , ma non dovrebbero verificarsi in un'API ideale.
  • deve gestire eccezioni esogene .

Eccezioni controllate

Il fatto che l'utente dell'API debba gestire una particolare eccezione fa parte del contratto del metodo tra il chiamante e il chiamante. Il contratto specifica, tra le altre cose: il numero e il tipo di argomenti attesi dalla chiamata, il tipo di valore restituito che il chiamante può aspettarsi e le eccezioni che il chiamante dovrebbe gestire .

Poiché le eccezioni fastidiose non dovrebbero esistere in un'API, solo queste eccezioni esogene devono essere verificate eccezioni per far parte del contratto del metodo. Relativamente poche eccezioni sono esogene , quindi qualsiasi API dovrebbe avere relativamente poche eccezioni verificate.

Un'eccezione controllata è un'eccezione che deve essere gestita . Gestire un'eccezione può essere semplice come ingoiarla. Là! L'eccezione viene gestita. Periodo. Se lo sviluppatore vuole gestirlo in questo modo, va bene. Ma non può ignorare l'eccezione ed è stato avvertito.

Problemi API

Ma qualsiasi API che ha verificato fastidi e eccezioni fatali (ad es. JCL) metterà a dura prova gli utenti API. Tali eccezioni devono essere gestite, ma o l'eccezione è così comune che non avrebbe dovuto essere un'eccezione in primo luogo, oppure non si può fare nulla durante la sua gestione. E questo fa sì che gli sviluppatori Java odiano le eccezioni verificate.

Inoltre, molte API non hanno una corretta gerarchia di classi di eccezioni, causando la rappresentazione di tutti i tipi di cause di eccezione non esogena in una singola classe di eccezioni verificata (ad es IOException.). E questo fa sì che gli sviluppatori Java odiano le eccezioni verificate.

Conclusione

Le eccezioni esogene sono quelle che non sono colpa tua, che non avrebbero potuto essere evitate e che dovrebbero essere gestite. Questi formano un piccolo sottoinsieme di tutte le eccezioni che possono essere generate. Le API dovrebbero aver verificato solo le eccezioni esogene e tutte le altre eccezioni non selezionate. Ciò renderà le API migliori, metterà meno a dura prova l'utente API e quindi ridurrà la necessità di catturare tutte, ingoiare o riproporre eccezioni non controllate.

Quindi non odiare Java e le sue eccezioni verificate. Al contrario, odio le API che usano in modo eccessivo le eccezioni verificate.


E abusarli di non avere una gerarchia.
TofuBeer,

1
FileNotFound e stabilire una connessione JDBC / rete sono contingenze e correzioni da verificare, in quanto sono prevedibili e (possibili) recuperabili. La maggior parte delle altre IOExceptions, SQLExceptions, RemoteException ecc. Sono guasti imprevedibili e irrecuperabili e avrebbero dovuto essere eccezioni di runtime. A causa dell'errato design della libreria Java, siamo stati tutti confusi con questo errore e ora stiamo usando principalmente Spring & Hibernate (che ha ottenuto il design giusto).
Thomas W,

Di solito dovresti gestire le eccezioni con la testa chiusa, anche se potresti non volerlo chiamare "maneggiamento". Ad esempio, in un server Web, li registro e visualizzo 500 all'utente. Poiché l'eccezione è inaspettata, c'è tutto quello che posso fare prima della correzione del bug.
maaartinus,

6

Ok ... Le eccezioni controllate non sono l'ideale e hanno qualche avvertimento ma servono a uno scopo. Quando si crea un'API ci sono casi specifici di errori contrattuali di questa API. Quando nel contesto di un linguaggio fortemente staticamente tipizzato come Java se non si usano eccezioni controllate, si deve fare affidamento sulla documentazione e sulla convenzione ad hoc per comunicare la possibilità di errore. In questo modo si eliminano tutti i vantaggi che il compilatore può apportare nell'errore di gestione e si viene lasciati completamente alla buona volontà dei programmatori.

Quindi, si rimuove l'eccezione Checked, come è stato fatto in C #, come si può quindi trasmettere programmaticamente e strutturalmente la possibilità di errore? Come informare il codice client che tali e tali errori possono verificarsi e devono essere gestiti?

Sento ogni sorta di orrori quando si tratta di eccezioni controllate, sono abusate di questo è certo, ma lo sono anche le eccezioni non controllate. Dico di aspettare qualche anno quando le API sono impilate in profondità su molti livelli e chiederai il ritorno di un qualche tipo di mezzo strutturato per trasmettere i guasti.

Prendi il caso quando l'eccezione è stata lanciata da qualche parte nella parte inferiore dei livelli API e ha fatto gorgogliare perché nessuno sapeva che fosse possibile che si verificasse questo errore, anche se si trattava di un tipo di errore che era molto plausibile quando il codice chiamante lo ha lanciato (FileNotFoundException ad esempio rispetto a VogonsTrashingEarthExcept ... nel qual caso non importa se lo gestiamo o no poiché non c'è più niente con cui gestirlo).

Molti hanno sostenuto che non riuscire a caricare il file era quasi sempre la fine del mondo per il processo e che doveva morire una morte orribile e dolorosa. Quindi sì .. certo ... ok .. costruisci un'API per qualcosa e carica il file ad un certo punto ... Io come utente di detta API posso solo rispondere ... "Chi diavolo sei tu a decidere quando il mio il programma dovrebbe andare in crash! " Certo, data la scelta in cui le eccezioni vengono divorate e non lasciano traccia o l'EletroFlabbingChunkFluxManifoldChuggingException con una traccia dello stack più profonda della trincea di Marianna, prenderei quest'ultima senza un pizzico di esitazione, ma ciò significa che è il modo desiderabile di affrontare l'eccezione ? Non possiamo essere da qualche parte nel mezzo, dove l'eccezione verrebbe rifusa e racchiusa ogni volta che attraversasse un nuovo livello di astrazione in modo che significasse effettivamente qualcosa?

Infine, la maggior parte dell'argomento che vedo è "Non voglio trattare le eccezioni, molte persone non vogliono avere a che fare con le eccezioni. Le eccezioni controllate mi costringono a gestirle, quindi odio l'eccezione controllata" Per eliminare del tutto tale meccanismo e relegarlo nel baratro dell'inferno è solo sciocco e privo di risonanza e visione.

Se eliminiamo l'eccezione verificata, potremmo anche eliminare il tipo restituito per le funzioni e restituire sempre una variabile "anytype" ... Ciò renderebbe la vita molto più semplice ora no?


2
Le eccezioni verificate sarebbero utili se esistesse un mezzo dichiarativo per dire che nessuna delle chiamate di metodo all'interno di un blocco dovrebbe generare alcune (o eventuali) eccezioni verificate e tali eccezioni dovrebbero essere automaticamente racchiuse e riproposte. Potrebbero essere ancora più utili se le chiamate a metodi che erano stati dichiarati come il lancio di eccezioni controllate venivano scambiate con la velocità / ritorno della chiamata per la velocità di gestione delle eccezioni (in modo che le eccezioni previste potessero essere gestite quasi alla stessa velocità del normale flusso di programma). Nessuna situazione al momento si applica, tuttavia.
supercat

6

Infatti, le eccezioni controllate da un lato aumentano la solidità e la correttezza del programma (sei costretto a fare dichiarazioni corrette delle tue interfacce - le eccezioni che un metodo genera sono fondamentalmente un tipo di ritorno speciale). D'altra parte, si deve affrontare il problema che, poiché le eccezioni "aumentano", molto spesso è necessario cambiare un sacco di metodi (tutti i chiamanti, i chiamanti dei chiamanti e così via) quando si cambiano le eccezioni il metodo lancia.

Le eccezioni verificate in Java non risolvono quest'ultimo problema; C # e VB.NET buttano via il bambino con l'acqua del bagno.

Un approccio gradevole che prende la via di mezzo è descritto in questo documento OOPSLA 2005 (o nel relativo rapporto tecnico .)

In breve, ti permette di dire:, il method g(x) throws like f(x)che significa che g genera tutte le eccezioni che genera. Voila, ha verificato le eccezioni senza il problema delle modifiche a cascata.

Sebbene sia un documento accademico, ti incoraggio a leggerlo (in parte), poiché fa un buon lavoro nel spiegare quali sono i vantaggi e gli svantaggi delle eccezioni verificate.


5

Questo non è un argomento contro il puro concetto di eccezioni verificate, ma la gerarchia di classi che Java utilizza per loro è uno spettacolo strano. Chiamiamo sempre le cose semplicemente "eccezioni" - il che è corretto, perché anche le specifiche del linguaggio le chiamano così - ma come viene chiamata e rappresentata un'eccezione nel sistema dei tipi?

Dalla classe Exceptionsi immagina? Bene no, perché Exceptions sono eccezioni, e allo stesso modo eccezioni sono Exceptions, tranne quelle eccezioni che non sono Exceptions, perché altre eccezioni sono in realtà Errors, che sono l'altro tipo di eccezione, una sorta di eccezione extra-eccezionale che non dovrebbe mai accadere tranne quando lo fa, e che non dovresti mai catturare tranne che a volte devi farlo. Tranne che non è tutto perché puoi anche definire altre eccezioni che non sono né Exceptions né Errors ma solo Throwableeccezioni.

Quali di queste sono le eccezioni "controllate"? Throwablele s sono eccezioni controllate, tranne se sono anche Errors, che sono eccezioni non selezionate, e poi ci sono le Exceptions, che sono anche se Throwablesono il tipo principale di eccezione verificata, tranne che c'è anche un'eccezione, che è che se sono anche RuntimeExceptions, perché è l'altro tipo di eccezione non selezionata.

A cosa servono RuntimeException? Bene, proprio come suggerisce il nome, sono eccezioni, come tutte le Exceptione si verificano in fase di esecuzione, come tutte le eccezioni in realtà, tranne che RuntimeExceptionsono eccezionali rispetto ad altre in fase di esecuzione Exceptionperché non dovrebbero accadere tranne quando commetti un errore sciocco, anche se RuntimeExceptions non sono mai Errors, quindi sono per cose che sono eccezionalmente errate ma che in realtà non lo sono Error. Tranne RuntimeErrorExceptionche, che è davvero un RuntimeExceptionfor Errors. Ma non si suppone comunque che tutte le eccezioni rappresentino circostanze errate? Sì, tutti quanti. Ad eccezione di ThreadDeath, un'eccezione eccezionalmente non eccezionale, in quanto la documentazione spiega che si tratta di un "evento normale" e che "Error

Ad ogni modo, dal momento che stiamo dividendo tutte le eccezioni nel mezzo in Errors (che sono eccezioni di esecuzione eccezionali, così deselezionate) e Exceptions (che sono per errori di esecuzione meno eccezionali, quindi controllate tranne quando non lo sono), ora abbiamo bisogno di due diversi tipi di ciascuna delle diverse eccezioni. Quindi abbiamo bisogno di IllegalAccessErrore IllegalAccessException, e InstantiationErrore InstantiationExceptione NoSuchFieldErrore NoSuchFieldExceptione NoSuchMethodErrore NoSuchMethodExceptione ZipErrore e e ZipException.

Tranne che anche quando viene verificata un'eccezione, ci sono sempre (abbastanza facili) modi per ingannare il compilatore e lanciarlo senza che sia controllato. Se lo fai, potresti ottenere un UndeclaredThrowableException, tranne in altri casi, in cui potrebbe vomitare come un UnexpectedException, o un UnknownException(che non è correlato a UnknownError, che è solo per "eccezioni gravi"), o un ExecutionException, o un InvocationTargetException, o un ExceptionInInitializerError.

Oh, e non dobbiamo dimenticare il nuovo accattivante di Java 8 UncheckedIOException, che è RuntimeExceptionun'eccezione progettata per farti lanciare il concetto di controllo delle eccezioni fuori dalla finestra avvolgendo le IOExceptioneccezioni verificate causate da errori I / O (che non causano IOErroreccezioni, anche se esiste anche) che sono estremamente difficili da gestire e quindi è necessario che non vengano controllati.

Grazie Java!


2
Per quanto posso dire, questa risposta dice solo "Le eccezioni di Java sono un disastro" in un modo pieno di ironia, probabilmente divertente. Ciò che sembra non fare è spiegare perché i programmatori tendono ad evitare di cercare di capire come dovrebbero funzionare queste cose. Inoltre, nei casi di vita reale (almeno quelli con cui ho avuto la possibilità di affrontare), se i programmatori non cercano deliberatamente di rendere le loro vite più difficili, le eccezioni non sono affatto complicate come hai descritto.
CptBartender,

5

Il problema

Il peggior problema che vedo con il meccanismo di gestione delle eccezioni è che introduce la duplicazione del codice su larga scala ! Siamo onesti: nella maggior parte dei progetti nel 95% delle volte tutto ciò che gli sviluppatori devono veramente fare con l'eccezione è comunicarlo in qualche modo all'utente (e, in alcuni casi, anche al team di sviluppo, ad esempio inviando un e -mail con la traccia dello stack). Quindi di solito viene utilizzata la stessa riga / blocco di codice in ogni luogo in cui viene gestita l'eccezione.

Supponiamo che eseguiamo una registrazione semplice in ciascun blocco catch per un tipo di eccezione verificata:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

Se si tratta di un'eccezione comune, potrebbero esserci anche diverse centinaia di blocchi try-catch in una base di codice più ampia. Ora supponiamo che sia necessario introdurre la gestione delle eccezioni basata su finestre di dialogo popup anziché la registrazione della console o iniziare a inviare un'e-mail al team di sviluppo.

Aspetta un momento ... modificheremo davvero tutte queste centinaia di posizioni nel codice ?! Ottieni il mio punto :-).

La soluzione

Ciò che abbiamo fatto per risolvere il problema è stato l'introduzione del concetto di gestori di eccezioni (a cui farò riferimento come EH) per centralizzare la gestione delle eccezioni. Ad ogni classe che deve gestire le eccezioni, un'istanza del gestore delle eccezioni viene iniettata dal nostro framework di Iniezione delle dipendenze . Quindi il tipico schema di gestione delle eccezioni ora appare così:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

Ora per personalizzare la nostra gestione delle eccezioni dobbiamo solo cambiare il codice in un unico posto (codice EH).

Naturalmente per casi più complessi possiamo implementare diverse sottoclassi di EH e sfruttare le funzionalità che il nostro framework DI ci offre. Modificando la nostra configurazione del framework DI, possiamo facilmente cambiare l'implementazione EH a livello globale o fornire implementazioni specifiche di EH a classi con particolari esigenze di gestione delle eccezioni (ad esempio utilizzando l'annotazione Guice @Named).

In questo modo possiamo differenziare il comportamento di gestione delle eccezioni nello sviluppo e rilasciare la versione dell'applicazione (ad es. Sviluppo - registrazione dell'errore e arresto dell'applicazione, produzione dell'errore con maggiori dettagli e lasciando che l'applicazione continui la sua esecuzione) senza sforzo.

Un'ultima cosa

Ultimo ma non meno importante, può sembrare che lo stesso tipo di centralizzazione possa essere ottenuto semplicemente passando le nostre eccezioni "in alto" fino a quando non arrivano a una classe di gestione delle eccezioni di livello superiore. Ma ciò porta a un ingombro di codice e firme dei nostri metodi e introduce problemi di manutenzione citati da altri in questo thread.


6
Sono state inventate eccezioni per fare qualcosa di utile con loro. Scriverli in un file di registro o renderizzare una bella finestra non è utile, perché il problema originale non viene risolto da questo. Fare qualcosa di utile richiede provare una strategia di soluzione diversa. Esempi: se non riesco a ottenere i miei dati dal server AI, provalo sul server B. O se l'algoritmo A produce un overflow dell'heap, provo l'algoritmo B che è molto più lento ma potrebbe avere successo.
ceving il

2
Sì, è tutto buono e vero in teoria. Ma ora torniamo a fare pratica con le parole. Per favore rispondi davvero onestamente quanto spesso lo fai nel tuo progetto di parole reali? Quale parte dei catchblocchi in questo vero progetto fa qualcosa di veramente "utile" con leinsine? Il 10% sarebbe buono. I soliti problemi che generano eccezioni sono come tentare di leggere la configurazione da un file che non esiste, OutOfMemoryErrors, NullPointerExceptions, errori di integrità del vincolo del database ecc., Ecc. Stai davvero cercando di ripristinarti con grazia da tutti? Non ti credo :). Spesso non c'è proprio modo di recuperare.
Piotr Sobczyk,

3
@PiotrSobczyk: se un programma intraprende un'azione a seguito di una richiesta suer e l'operazione non riesce in qualche modo che non ha danneggiato nulla nello stato del sistema, notificare all'utente che l'operazione non può essere completata è perfettamente utile modo di gestire la situazione. Il più grande fallimento delle eccezioni in C # e .net è che non esiste un modo coerente per accertare se qualcosa nello stato del sistema potrebbe essere stato danneggiato.
supercat

4
Corretto, @PiotrSobczyk. Molto spesso, l'unica azione corretta da intraprendere in risposta a un'eccezione è il rollback della transazione e la restituzione di una risposta di errore. L'idea di "risolvere l'eccezione" implica la conoscenza e l'autorità che non abbiamo (e non dovremmo) e violano l'incapsulamento. Se la nostra applicazione non è un DB, non dovremmo provare a riparare il DB. Fallire in modo pulito ed evitare di scrivere dati errati, il ceving, è abbastanza utile .
Thomas W,

@PiotrSobczyk Ieri ho affrontato un'eccezione "Impossibile leggere l'oggetto" (che avverrà solo perché il database sottostante è stato aggiornato prima del software - cosa che non dovrebbe mai accadere ma è possibile a causa di un errore umano) passando a un versione storica del database garantita per puntare a una versione precedente dell'oggetto.
Eponimo

4

Anders parla delle insidie ​​delle eccezioni verificate e del perché le ha lasciate fuori da C # nell'episodio 97 della radio di ingegneria del software.


4

Il mio writeup su c2.com è per lo più invariato rispetto alla sua forma originale: CheckedExceptionsAreIncompatibleWithVisitorPattern

In sintesi:

Il modello visitatore e i suoi parenti sono una classe di interfacce in cui il chiamante indiretto e l'implementazione dell'interfaccia sono entrambi a conoscenza di un'eccezione, ma l'interfaccia e il chiamante diretto formano una libreria che non può conoscere.

Il presupposto fondamentale di CheckedExceptions è che tutte le eccezioni dichiarate possono essere generate da qualsiasi punto che chiama un metodo con quella dichiarazione. VisitorPattern rivela che questa ipotesi è errata.

Il risultato finale delle eccezioni verificate in casi come questi è un sacco di codice altrimenti inutile che rimuove essenzialmente il vincolo di eccezione verificata del compilatore in fase di esecuzione.

Per quanto riguarda il problema di fondo:

La mia idea generale è che il gestore di livello superiore deve interpretare l'eccezione e visualizzare un messaggio di errore appropriato. Vedo quasi sempre eccezioni di I / O, eccezioni di comunicazione (per qualche ragione distinguono le API) o errori fatali (bug del programma o grave problema sul server di backup), quindi questo non dovrebbe essere troppo difficile se consentiamo una traccia dello stack per un grave problema del server.


1
Dovresti avere qualcosa di simile a DAGNodeException nell'interfaccia, quindi prendere IOException e convertirlo in DAGNodeException: public void call (DAGNode arg) genera DAGNodeException;
TofuBeer,

2
@TofuBeer, questo è esattamente il mio punto. Trovo che avvolgere e scartare costantemente le eccezioni sia peggio che rimuovere le eccezioni verificate.
Giosuè,

2
Bene, allora non siamo completamente d'accordo ... ma il tuo articolo non risponde ancora alla vera domanda di fondo su come si impedisce all'applicazione di mostrare all'utente una traccia dello stack quando viene generata un'eccezione di runtime.
TofuBeer,

1
@TofuBeer - Quando fallisce, dire all'utente che è fallito è corretto! Qual è la tua alternativa, se non quella di "sovrastampare" l'errore con "null" o dati incompleti / errati? Fingere che ci sia riuscita è una bugia, che peggiora le cose. Con 25 anni di esperienza in sistemi ad alta affidabilità, la logica dei tentativi deve essere usata con attenzione e dove appropriato. Mi aspetterei anche che un Visitatore probabilmente fallisca di nuovo, non importa quante volte lo riprovi. A meno che non voli su un aereo, passare a una seconda versione dello stesso algoritmo è impraticabile e non plausibile (e potrebbe non riuscire comunque).
Thomas W,

4

Per tentare di affrontare solo la domanda senza risposta:

Se si lanciano sottoclassi RuntimeException anziché sottoclassi di eccezione, come si fa a sapere cosa si dovrebbe catturare?

La domanda contiene un ragionamento specioso IMHO. Solo perché l'API ti dice cosa genera non significa che la gestisci allo stesso modo in tutti i casi. Per dirla in altro modo, le eccezioni che è necessario rilevare variano in base al contesto in cui si utilizza il componente che genera l'eccezione.

Per esempio:

Se sto scrivendo un tester di connessione per un database o qualcosa per verificare la validità di un utente inserito in XPath, probabilmente vorrei prendere e riferire su tutte le eccezioni verificate e non verificate che vengono generate dall'operazione.

Se, tuttavia, sto scrivendo un motore di elaborazione, probabilmente tratterò una XPathException (selezionata) allo stesso modo di un NPE: lascerei correre fino all'inizio del thread di lavoro, saltare il resto di quel batch, accedere il problema (o inviarlo a un dipartimento di supporto per la diagnosi) e lasciare un feedback per l'utente di contattare il supporto.


2
Esattamente. Semplice e diretto, come deve essere la gestione delle eccezioni. Come dice Dave, la corretta gestione delle eccezioni viene normalmente eseguita ad alto livello . "Gettare in anticipo, prendere in ritardo" è il principio. Le eccezioni verificate lo rendono difficile.
Thomas W,

4

Questo articolo è il miglior testo sulla gestione delle eccezioni in Java che abbia mai letto.

Favorisce le eccezioni non controllate rispetto alle eccezioni verificate, ma questa scelta è spiegata in modo molto scrupoloso e basata su argomentazioni forti.

Non voglio citare troppo il contenuto dell'articolo qui (è meglio leggerlo nel suo insieme) ma copre la maggior parte degli argomenti delle eccezioni non verificate sostenitori di questo thread. Soprattutto questo argomento (che sembra essere abbastanza popolare) è coperto:

Prendi il caso quando l'eccezione è stata lanciata da qualche parte nella parte inferiore dei livelli API e ha fatto gorgogliare perché nessuno sapeva che fosse possibile che si verificasse questo errore, anche se si trattava di un tipo di errore che era molto plausibile quando il codice chiamante lo ha lanciato (FileNotFoundException ad esempio rispetto a VogonsTrashingEarthExcept ... nel qual caso non importa se lo gestiamo o no poiché non c'è più niente con cui gestirlo).

L'autore "risposte":

È assolutamente errato supporre che tutte le eccezioni di runtime non debbano essere rilevate e consentite di propagarsi fino alla "cima" dell'applicazione. (...) Per ogni condizione eccezionale che deve essere gestita in modo distinto - dai requisiti di sistema / aziendali - i programmatori devono decidere dove catturarlo e cosa fare una volta che la condizione viene rilevata. Questo deve essere fatto rigorosamente in base alle effettive esigenze dell'applicazione, non sulla base di un avviso del compilatore. A tutti gli altri errori deve essere consentito di propagarsi liberamente al gestore più in alto dove verranno registrati e verrà intrapresa un'azione aggraziata (forse, la risoluzione).

E il pensiero o l'articolo principale è:

Quando si tratta di gestione degli errori nel software, l'unica ipotesi sicura e corretta che possa mai essere fatta è che si possa verificare un errore in ogni subroutine o modulo esistente!

Quindi, se " nessuno sapeva che fosse possibile che si verificasse questo errore ", c'è qualcosa che non va in quel progetto. Tale eccezione dovrebbe essere gestita almeno dal gestore di eccezioni più generico (ad esempio quello che gestisce tutte le eccezioni non gestite da gestori più specifici) come suggerisce l'autore.

Così triste, non molti sembrano scoprire questo fantastico articolo :-(. Consiglio vivamente a tutti coloro che esitano quale approccio è meglio prendere un po 'di tempo e leggerlo.


4

Le eccezioni verificate erano, nella loro forma originale, un tentativo di gestire le contingenze piuttosto che i fallimenti. L'obiettivo lodevole era evidenziare punti prevedibili specifici (impossibilità di connettersi, file non trovato, ecc.) E garantire che gli sviluppatori li gestissero.

Ciò che non è mai stato incluso nel concetto originale, è stato quello di forzare una vasta gamma di fallimenti sistemici e irrecuperabili da dichiarare. Questi errori non sono mai stati corretti per essere dichiarati come eccezioni verificate.

Gli errori sono generalmente possibili nel codice e i contenitori EJB, web e Swing / AWT già provvedono a questo fornendo un gestore di eccezioni "richiesta non riuscita" esterno. La strategia corretta di base è il rollback della transazione e la restituzione di un errore.

Un punto cruciale è che le eccezioni di runtime e controllate sono funzionalmente equivalenti. Non esiste alcuna gestione o ripristino che le eccezioni verificate possano fare, mentre le eccezioni di runtime non possono farlo.

L'argomento principale contro le eccezioni "controllate" è che la maggior parte delle eccezioni non può essere risolta. Il fatto è che non possediamo il codice / sottosistema che si è rotto. Non possiamo vedere l'implementazione, non ne siamo responsabili e non possiamo ripararlo.

Se la nostra applicazione non è un DB .. non dovremmo provare a riparare il DB. Ciò violerebbe il principio dell'incapsulamento .

Particolarmente problematiche sono state le aree di JDBC (SQLException) e RMI per EJB (RemoteException). Piuttosto che identificare contingenze riparabili secondo il concetto originale di "eccezione verificata", questi problemi di affidabilità sistemica pervasiva forzata, non effettivamente risolvibili, devono essere ampiamente dichiarati.

L'altro grave difetto del progetto Java era che la gestione delle eccezioni doveva essere correttamente posizionata al livello "business" o "richiesta" più elevato possibile. Il principio qui è "lanciare in anticipo, prendere in ritardo". Le eccezioni verificate non fanno altro che ostacolarlo.

Abbiamo un ovvio problema in Java di richiedere migliaia di blocchi try-catch praticamente nulla, con una proporzione significativa (40% +) errata. Quasi nessuno di questi implementa una gestione o affidabilità autentica, ma impone un sovraccarico di codice maggiore.

Infine, le "eccezioni controllate" sono praticamente incompatibili con la programmazione funzionale FP.

La loro insistenza sul "maneggiare immediatamente" è in contrasto sia con le migliori pratiche di "gestione tardiva" della gestione delle eccezioni, sia con qualsiasi struttura FP che astrasse i cicli / o il flusso di controllo.

Molte persone parlano di "gestire" le eccezioni controllate, ma stanno parlando con i loro cappelli. Continuare dopo un errore con dati nulli, incompleti o errati a pretendere il successo non gestisce nulla. Si tratta di negligenza tecnica / di affidabilità nella forma più bassa.

Fallire in modo pulito, è la strategia corretta di base per la gestione di un'eccezione. Il rollback della transazione, la registrazione dell'errore e la segnalazione di una risposta "non riuscita" all'utente sono una buona pratica e, cosa più importante, impedire che vengano archiviati dati aziendali errati nel database.

Altre strategie per la gestione delle eccezioni sono "riprova", "riconnetti" o "salta", a livello aziendale, di sottosistema o di richiesta. Tutte queste sono strategie generali di affidabilità e funzionano bene / meglio con le eccezioni di runtime.

Infine, è preferibile fallire, piuttosto che eseguire con dati errati. Continuare provocherà errori secondari, distanti dalla causa originale e più difficili da eseguire il debug; o alla fine comporterà il commit di dati errati. Le persone vengono licenziate per questo.

Vedi:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/


1
Il mio punto è fallire correttamente come strategia generale. Le eccezioni non selezionate aiutano a non interporre i blocchi di cattura in quanto non impongono. La cattura e la registrazione degli errori possono quindi essere lasciate ad alcuni gestori più esterni, anziché essere erroneamente codificate migliaia di volte in tutta la base di codice (che in realtà è ciò che nasconde i bug) . Per guasti arbitrari, le eccezioni non controllate sono assolutamente corrette. Le contingenze - esiti prevedibili come fondi insufficienti - sono le uniche eccezioni che valgono la pena di essere verificate.
Thomas W,

1
La mia risposta già sopra risolve questo problema. Innanzitutto, 1) Il gestore di guasti più esterno dovrebbe catturare tutto. Oltre a ciò, solo per specifici siti identificati, 2) È possibile rilevare e gestire specifiche contingenze attese - nel sito immediato vengono lanciate. Ciò significa che il file non è stato trovato, fondi insufficienti ecc. Al punto da cui questi potrebbero essere recuperati - non più in alto. Principio di incapsulamento significa che gli strati esterni non possono / non devono essere responsabili di comprendere / recuperare da guasti profondi all'interno. Terzo, 3) Tutto il resto dovrebbe essere gettato verso l'esterno - deselezionato se possibile.
Thomas W,

1
Il gestore esterno rileva Eccezione, la registra e restituisce una risposta "non riuscita" o mostra una finestra di dialogo di errore. Molto semplice, non è affatto difficile da definire. Il punto è che ogni eccezione non immediatamente e localmente recuperabile è un fallimento irrecuperabile a causa del principio di incapsulamento. Se il codice che si intende conoscere non è in grado di recuperarlo, la richiesta nel suo complesso non riesce in modo pulito e corretto. Questo è il modo giusto per farlo correttamente.
Thomas W,

1
Non corretto. Il lavoro del gestore esterno è quello di fallire in modo pulito e registrare gli errori sul limite 'richiesta'. La richiesta interrotta non riesce correttamente, viene segnalata un'eccezione, il thread è in grado di continuare a servire la richiesta successiva. Un tale gestore più esterno è una funzionalità standard nei contenitori Tomcat, AWT, Spring, EJB e nel thread 'principale' di Java.
Thomas W,

1
Perché è pericoloso segnalare "bug autentici" al limite della richiesta o al gestore esterno ??? Lavoro frequentemente nell'integrazione e nell'affidabilità dei sistemi, dove la corretta ingegneria dell'affidabilità è in realtà importante e, per farlo, utilizzo approcci di "eccezione non controllata". Non sono davvero sicuro di cosa stia realmente discutendo: sembra che tu possa voler effettivamente passare 3 mesi in modo non controllato per le eccezioni, farti un'idea, e quindi forse potremo discutere ulteriormente. Grazie.
Thomas W,

4

Come già affermato dalla gente, nel codice bytecode Java non esistono eccezioni verificate. Sono semplicemente un meccanismo di compilazione, non diversamente da altri controlli di sintassi. Vedo eccezioni controllato un po 'come vedo il compilatore lamentano un condizionale ridondante: if(true) { a; } b;. È utile, ma potrei averlo fatto apposta, quindi lasciami ignorare i tuoi avvertimenti.

Il fatto è che non sarai in grado di costringere tutti i programmatori a "fare la cosa giusta" se imponi eccezioni verificate e tutti gli altri ora sono danni collaterali che ti odiano solo per la regola che hai fatto.

Risolvi i cattivi programmi là fuori! Non provare a correggere la lingua per non permetterli! Per la maggior parte delle persone, "fare qualcosa per un'eccezione" è davvero solo per dirlo all'utente. Posso dire anche all'utente di un'eccezione non selezionata, quindi tieni le classi di eccezioni selezionate fuori dalla mia API.


Bene, volevo solo sottolineare la differenza tra codice irraggiungibile (che genera un errore) e condizionali con un risultato prevedibile. Rimuoverò questo commento più tardi.
Holger,

3

Un problema con le eccezioni verificate è che le eccezioni sono spesso associate ai metodi di un'interfaccia se viene utilizzata anche da un'implementazione di tale interfaccia.

Un altro problema con le eccezioni verificate è che tendono ad essere utilizzate in modo improprio. L'esempio perfetto di questo è in java.sql.Connection's close()metodo. Può generare un SQLException, anche se hai già dichiarato esplicitamente di aver terminato la connessione. Quali informazioni potrebbero essere chiuse () che potrebbero interessarti?

Di solito, quando chiudo () una connessione *, sembra qualcosa del genere:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

Inoltre, non farmi iniziare sui vari metodi di analisi e su NumberFormatException ... TryParse di .NET, che non genera eccezioni, è molto più facile da usare, è doloroso dover tornare a Java (usiamo sia Java che C # dove lavoro).

*Come commento aggiuntivo, Connection.close () di PooledConnection non chiude nemmeno una connessione, ma è comunque necessario intercettare SQLException perché è un'eccezione verificata.


2
Giusto, uno qualsiasi dei driver può ... la domanda è piuttosto "perché dovrebbe interessarsi al programmatore?" come ha fatto comunque accedendo al database. I documenti ti avvertono persino che dovresti sempre eseguire il commit () o il rollback () della transazione corrente prima di chiamare close ().
Powerlord

Molte persone pensano che chiudere su un file non possa generare un'eccezione ... stackoverflow.com/questions/588546/… sei sicuro al 100% che non ci sono casi che contano?
TofuBeer,

Sono sicuro al 100% che non ci sono casi in cui sarebbe importante e che il chiamante non avrebbe tentato.
Martin,

1
Ottimo esempio con i collegamenti di chiusura, Martin! Posso solo riformularti: se abbiamo semplicemente dichiarato esplicitamente che abbiamo finito con una connessione, perché dovremmo preoccuparci di cosa succede quando lo chiudiamo. Ci sono altri casi come questo che al programmatore non importa davvero se si verifica un'eccezione e ha assolutamente ragione.
Piotr Sobczyk,

1
@PiotrSobczyk: alcuni driver SQL squawk se si chiude una connessione dopo aver avviato una transazione, ma non confermandola né eseguendo il rollback. IMHO, lo squawking è meglio che ignorare silenziosamente il problema, almeno nei casi in cui lo squawking non causerà la perdita di altre eccezioni.
supercat

3

Il programmatore deve conoscere tutte le eccezioni che un metodo può generare, al fine di utilizzarlo correttamente. Quindi, batterlo sopra la testa con solo alcune delle eccezioni non aiuta necessariamente un programmatore incurante a evitare errori.

Il vantaggio sottile è compensato dal costo oneroso (specialmente nelle basi di codice più grandi e meno flessibili dove la modifica costante delle firme dell'interfaccia non è pratica).

L'analisi statica può essere piacevole, ma un'analisi statica veramente affidabile spesso richiede in modo inflessibile un lavoro rigoroso da parte del programmatore. Esiste un calcolo costi-benefici e la barra deve essere impostata in alto per un controllo che porta a un errore di tempo di compilazione. Sarebbe più utile se l'IDE assumesse il ruolo di comunicare quali eccezioni può generare un metodo (incluso quali sono inevitabili). Sebbene forse non sarebbe altrettanto affidabile senza dichiarazioni di eccezione forzata, la maggior parte delle eccezioni sarebbe comunque dichiarata nella documentazione e l'affidabilità di un avviso IDE non è così cruciale.


2

Penso che questa sia una domanda eccellente e per nulla polemica. Penso che le librerie di terze parti dovrebbero (in generale) generare eccezioni non controllate . Ciò significa che puoi isolare le tue dipendenze dalla libreria (cioè non devi né respingere le loro eccezioni né lanciare Exception- di solito cattiva pratica). Lo strato DAO di Spring ne è un eccellente esempio.

D'altra parte, le eccezioni dall'API Java principale dovrebbero in generale essere verificate se potessero mai essere gestite. Prendi FileNotFoundExceptiono (il mio preferito) InterruptedException. Queste condizioni dovrebbero quasi sempre essere gestite in modo specifico (cioè la tua reazione a una InterruptedExceptionnon è la stessa della tua reazione a una IllegalArgumentException). Il fatto che le tue eccezioni siano controllate costringe gli sviluppatori a pensare se una condizione è gestibile o meno. (Detto questo, raramente ho visto InterruptedExceptiongestito correttamente!)

Ancora una cosa: a RuntimeExceptionnon è sempre "dove uno sviluppatore ha sbagliato qualcosa". Viene generata un'eccezione di argomento illegale quando si tenta di creare un enumutilizzo valueOfe non è presente enumtale nome. Questo non è necessariamente un errore da parte dello sviluppatore!


1
Sì, è un errore dello sviluppatore. Chiaramente non hanno usato il nome giusto, quindi devono tornare indietro e correggere il loro codice.
AxiomaticNexus,

@AxiomaticNexus Nessuno sviluppatore sano usa enumnomi di membri, semplicemente perché usano enuminvece oggetti. Quindi un nome sbagliato può provenire solo dall'esterno, sia esso un file di importazione o altro. Un modo possibile per gestire tali nomi è chiamare MyEnum#valueOfe catturare l'IAE. Un altro modo è quello di utilizzare un pre-compilato Map<String, MyEnum>, ma questi sono dettagli di implementazione.
maaartinus,

@maaartinus Ci sono casi in cui vengono usati nomi di membri enum senza che la stringa provenga dall'esterno. Ad esempio, quando si desidera scorrere in modo dinamico tutti i membri per fare qualcosa con ciascuno. Inoltre, non importa se la stringa proviene dall'esterno o meno. Lo sviluppatore ha tutte le informazioni di cui ha bisogno per sapere se passare la stringa x a "MyEnum # valueOf" comporterà un errore prima di passarlo. Passare la stringa x a "MyEnum # valueOf" comunque quando avrebbe causato un errore, sarebbe chiaramente un errore da parte dello sviluppatore.
AxiomaticNexus,

2

Ecco un argomento contro le eccezioni verificate (da joelonsoftware.com):

Il ragionamento è che considero le eccezioni non migliori di "goto's", considerate dannose dagli anni '60, in quanto creano un brusco salto da un punto di codice a un altro. In effetti sono significativamente peggiori di quelli di goto:

  • Sono invisibili nel codice sorgente. Osservando un blocco di codice, incluse le funzioni che possono o meno generare eccezioni, non c'è modo di vedere quali eccezioni potrebbero essere generate e da dove. Ciò significa che anche un'attenta ispezione del codice non rivela potenziali bug.
  • Creano troppi possibili punti di uscita per una funzione. Per scrivere il codice corretto, devi davvero pensare a ogni possibile percorso del codice attraverso la tua funzione. Ogni volta che chiami una funzione che può sollevare un'eccezione e non rilevarla sul posto, crei opportunità per bug a sorpresa causati da funzioni che si sono interrotte bruscamente, lasciando i dati in uno stato incoerente o altri percorsi di codice che non hai Pensa a.

3
+1 Tuttavia potresti voler riassumere l'argomento nella tua risposta? Sono come goto invisibili e prime uscite per le tue routine, sparse per tutto il programma.
MarkJ

13
Questo è più un argomento contro le eccezioni in generale.
Ionuț G. Stan,

10
hai letto l'articolo !! In primo luogo parla delle eccezioni in generale, in secondo luogo la sezione "Sono invisibili nel codice sorgente" si applica specificamente all'eccezione NON CONTROLLATA. Questo è il punto di eccezione verificata ... in modo che SAPI quale codice genera cosa dove
Newtopian

2
@Eva Non sono gli stessi. Con un'istruzione goto puoi vedere la gotoparola chiave. Con un ciclo puoi vedere la parentesi graffa di chiusura o la parola chiave breako continue. Tutti saltano a un punto del metodo corrente. Ma non puoi sempre vedere il throw, perché spesso non è nel metodo corrente ma in un altro metodo che chiama (possibilmente indirettamente)
finnw

5
Le funzioni di @finnw sono esse stesse una forma di goto. Di solito non sai quali funzioni stanno chiamando le funzioni che stai chiamando. Se programmassi senza funzioni, non avresti problemi con eccezioni invisibili. Ciò significa che il problema non è specificamente legato alle eccezioni e non è un argomento valido contro le eccezioni in generale. Si potrebbe dire che i codici di errore sono più veloci, si potrebbe dire che le monadi sono più pulite, ma l'argomento goto è sciocco.
Eva

2

La buona prova che non sono necessarie eccezioni controllate sono:

  1. Un sacco di framework che funziona un po 'per Java. Come Spring che racchiude l'eccezione JDBC in eccezioni non selezionate, lanciando messaggi nel registro
  2. Molte lingue arrivate dopo Java, anche in cima alla piattaforma Java - non le usano
  3. Eccezioni verificate, è una previsione gentile su come il client userebbe il codice che genera un'eccezione. Ma uno sviluppatore che scrive questo codice non conoscerebbe mai il sistema e il business in cui sta lavorando il client di codice. Ad esempio, i metodi di Interfcace che costringono a generare un'eccezione controllata. Esistono 100 implementazioni sul sistema, 50 o addirittura 90 implementazioni non generano questa eccezione, ma il client deve comunque rilevare questa eccezione se fa riferimento all'utente a quell'interfaccia. Quelle 50 o 90 implementazioni tendono a gestire quelle eccezioni al loro interno, mettendo un'eccezione nel registro (e questo è un buon comportamento per loro). Cosa dovremmo fare con quello? Farei meglio ad avere una logica di sottofondo che farebbe tutto quel lavoro, inviando un messaggio al registro. E se io, come client di codice, sentissi di dover gestire l'eccezione, lo farò.
  4. Un altro esempio quando lavoro con l'I / O in Java, mi costringe a controllare tutte le eccezioni, se il file non esiste? cosa dovrei fare con quello? Se non esiste, il sistema non passerà al passaggio successivo. Il client di questo metodo, non otterrebbe il contenuto previsto da quel file: può gestire l'eccezione di runtime, altrimenti dovrei prima controllare Eccezione controllata, inserire un messaggio per accedere, quindi eliminare l'eccezione dal metodo. No ... no - Meglio farlo automaticamente con RuntimeEception, che lo fa / si accende automaticamente. Non ha alcun senso gestirlo manualmente - Sarei felice di aver visto un messaggio di errore nel registro (AOP può aiutare con quello .. qualcosa che risolve java). Se, alla fine, desidero che quel sistema dovrebbe mostrare un messaggio pop-up all'utente finale, lo mostrerò, non è un problema.

Sono stato felice se java mi avrebbe fornito una scelta su cosa usare, quando lavoravo con librerie core, come l'I / O. Like fornisce due copie delle stesse classi: una racchiusa con RuntimeEception. Quindi possiamo confrontare ciò che la gente userebbe . Per ora, tuttavia, molte persone farebbero meglio a scegliere un framework in cima a Java o in una lingua diversa. Come Scala, JRuby qualunque cosa. Molti credono solo che SUN avesse ragione.


1
Invece di avere due versioni di classi, dovrebbe esserci un modo conciso di specificare che nessuna delle chiamate di metodo effettuate da un blocco di codice non dovrebbe generare eccezioni di determinati tipi e che tali eccezioni debbano essere racchiuse in alcuni modi specifici e riprogrammato (per impostazione predefinita, crearne uno nuovo RuntimeExceptioncon un'eccezione interna appropriata). È un peccato che sia più conciso avere il metodo esterno throwsun'eccezione dal metodo interno, piuttosto che avere delle eccezioni rispetto al metodo interno, quando quest'ultimo corso dell'azione sarebbe più spesso corretto.
supercat,

2

Una cosa importante che nessuno ha menzionato è come interferisce con le interfacce e le espressioni lambda.

Supponiamo che tu definisca a MyAppException extends Exception. È l'eccezione di livello superiore ereditata da tutte le eccezioni generate dall'applicazione. Ogni metodo dichiara throws MyAppExceptionche è un po 'sfacciato, ma gestibile. Un gestore di eccezioni registra l'eccezione e avvisa l'utente in qualche modo.

Tutto sembra OK fino a quando non vuoi implementare un'interfaccia che non è tua. Ovviamente non dichiara l'intenzione di lanciare MyApException, quindi il compilatore non ti consente di lanciare l'eccezione da lì.

Tuttavia, se la tua eccezione si estende RuntimeException, non ci saranno problemi con le interfacce. Se lo desideri, puoi menzionare volontariamente l'eccezione in JavaDoc. Ma a parte questo, silenziosamente senza bolle attraverso qualsiasi cosa, da catturare nel tuo livello di gestione delle eccezioni.


1

Abbiamo visto alcuni riferimenti al capo architetto di C #.

Ecco un punto di vista alternativo da parte di un ragazzo Java su quando usare le eccezioni controllate. Riconosce molti dei negativi che altri hanno menzionato: Eccezioni efficaci


2
Il problema con le eccezioni verificate in Java deriva da un problema più profondo, in questo modo troppe informazioni sono incapsulate nel TIPO dell'eccezione, piuttosto che nelle proprietà di un'istanza. Sarebbe utile avere delle eccezioni verificate, se essere "controllato" fosse un attributo dei siti di lancio / cattura e se si potesse specificare in modo dichiarativo se un'eccezione controllata che sfugge a un blocco di codice debba rimanere come eccezione verificata o essere vista da qualsiasi blocco racchiuso come eccezione non selezionata; allo stesso modo i blocchi catch dovrebbero essere in grado di specificare che vogliono solo eccezioni verificate.
supercat

1
Supponiamo che sia specificata una routine di ricerca nel dizionario per generare un particolare tipo di eccezione se si tenta di accedere a una chiave inesistente. Potrebbe essere ragionevole che il codice client rilevi tale eccezione. Se, tuttavia, un metodo utilizzato dalla routine di ricerca sembra generare lo stesso tipo di eccezione in un modo che la routine di ricerca non prevede, il codice client probabilmente non dovrebbe rilevarlo. La verifica della proprietà di istanze di eccezione , lancio di siti e siti di cattura eviterebbe tali problemi. Il client rileva eccezioni "verificate" di quel tipo, schivando quelle inattese.
supercat

0

Ho letto molto sulla gestione delle eccezioni, anche se (la maggior parte delle volte) non posso davvero dire di essere felice o triste dell'esistenza di eccezioni verificate, questa è la mia opinione: eccezioni verificate nel codice di basso livello (IO, rete , SO, ecc.) Ed eccezioni non selezionate nelle API di alto livello / a livello di applicazione.

Anche se non è così facile tracciare una linea di demarcazione tra loro, trovo che sia davvero fastidioso / difficile integrare più API / librerie sotto lo stesso tetto senza racimolare sempre molte eccezioni verificate ma d'altra parte, a volte è utile / meglio essere costretti a cogliere qualche eccezione e fornirne una diversa che abbia più senso nel contesto attuale.

Il progetto a cui sto lavorando prende molte librerie e le integra nella stessa API, API che è completamente basata su eccezioni non controllate. Questo framework fornisce un'API di alto livello che all'inizio era piena di eccezioni verificate e aveva solo diverse non verificate eccezioni (eccezione di inizializzazione, ConfigurationException, ecc.) e devo dire che non era molto amichevole . Il più delle volte dovevi catturare o rigettare le eccezioni che non sai come gestire, o non ti importa nemmeno (non essere confuso con te dovresti ignorare le eccezioni), specialmente sul lato client in cui un singolo il clic potrebbe generare 10 possibili (verificate) eccezioni.

La versione corrente (terza) utilizza solo eccezioni non controllate e ha un gestore di eccezioni globale che è responsabile di gestire qualsiasi cosa non rilevata. L'API fornisce un modo per registrare i gestori di eccezioni, che decideranno se un'eccezione è considerata un errore (il più delle volte questo è il caso) che significa registrare e avvisare qualcuno, oppure può significare qualcos'altro, come questa eccezione, AbortException, il che significa interrompere il thread di esecuzione corrente e non registrare alcun errore perché si desidera non farlo. Naturalmente, al fine di elaborare tutto il thread personalizzato deve gestire il metodo run () con un tentativo try {...} catch (all).

public void run () {

try {
     ... do something ...
} catch (Throwable throwable) {
     ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

}

Ciò non è necessario se si utilizza WorkerService per pianificare i lavori (eseguibile, richiamabile, lavoratore), che gestisce tutto per voi.

Naturalmente questa è solo la mia opinione, e potrebbe non essere quella giusta, ma mi sembra un buon approccio. Vedrò dopo che rilascerò il progetto se ciò che penso sia buono per me, lo è anche per gli altri ... :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.