Attenzione ai pericoli delle eccezioni generiche

Mentre lavoravo a un progetto recente, ho trovato un pezzo di codice che eseguiva la pulizia delle risorse. Poiché aveva molte chiamate diverse, potrebbe potenzialmente generare sei diverse eccezioni. Il programmatore originale, nel tentativo di semplificare il codice (o semplicemente salvare la digitazione), ha dichiarato che il metodo genera Exceptioninvece delle sei diverse eccezioni che potrebbero essere lanciate. Ciò ha costretto il codice chiamante a essere racchiuso in un blocco try / catch che ha catturato Exception. Il programmatore ha deciso che, poiché il codice era a scopo di pulizia, i casi di errore non erano importanti, quindi il blocco catch è rimasto vuoto mentre il sistema si spegneva comunque.

Ovviamente, queste non sono le migliori pratiche di programmazione, ma nulla sembra essere terribilmente sbagliato ... tranne per un piccolo problema logico nella terza riga del codice originale:

Listato 1. Codice originale di pulizia

private void cleanupConnections () genera ExceptionOne, ExceptionTwo {for (int i = 0; i <connections.length; i ++) {connection [i] .release (); // Genera una connessione ExceptionOne, ExceptionTwo [i] = null; } connessioni = null; } protected abstract void cleanupFiles () genera ExceptionThree, ExceptionFour; protected abstract void removeListeners () genera ExceptionFive, ExceptionSix; public void cleanupEverything () genera un'eccezione {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); cleanupEverything (); doMoreStuff (); } catch (eccezione e) {}}

In un'altra parte del codice, la connectionsmatrice non viene inizializzata finché non viene creata la prima connessione. Ma se non viene mai creata una connessione, l'array di connessioni è nullo. Quindi, in alcuni casi, la chiamata a connections[i].release()risultati in a NullPointerException. Questo è un problema relativamente facile da risolvere. Aggiungi semplicemente un segno di spunta connections != null.

Tuttavia, l'eccezione non viene mai segnalata. Viene lanciata cleanupConnections(), lanciata di nuovo cleanupEverything()e infine catturata done(). Il done()metodo non fa nulla con l'eccezione, non lo registra nemmeno. E poiché cleanupEverything()viene chiamato solo attraverso done(), l'eccezione non viene mai vista. Quindi il codice non viene mai corretto.

Pertanto, nello scenario di errore, i metodi cleanupFiles()e removeListeners()non vengono mai chiamati (quindi le loro risorse non vengono mai rilasciate) e doMoreStuff()non vengono mai chiamate, quindi l'elaborazione finale done()non viene mai completata. A peggiorare le cose, done()non viene chiamato quando il sistema si spegne; invece è chiamato a completare ogni transazione. Quindi le risorse perdono in ogni transazione.

Questo problema è chiaramente uno dei principali: gli errori non vengono segnalati e le risorse perdono. Ma il codice stesso sembra piuttosto innocente e, dal modo in cui il codice è stato scritto, questo problema risulta difficile da rintracciare. Tuttavia, applicando alcune semplici linee guida, il problema può essere trovato e risolto:

  • Non ignorare le eccezioni
  • Non prendere generici Exceptions
  • Non gettare generici Exceptions

Non ignorare le eccezioni

Il problema più ovvio con il codice del listato 1 è che un errore nel programma viene completamente ignorato. Viene generata un'eccezione imprevista (le eccezioni, per loro natura, sono impreviste) e il codice non è preparato per gestire tale eccezione. L'eccezione non viene nemmeno segnalata perché il codice presume che le eccezioni previste non avranno conseguenze.

Nella maggior parte dei casi, almeno un'eccezione dovrebbe essere registrata. Diversi pacchetti di registrazione (vedere la barra laterale "Eccezioni di registrazione") possono registrare errori ed eccezioni di sistema senza influire in modo significativo sulle prestazioni del sistema. La maggior parte dei sistemi di registrazione consente anche la stampa delle tracce dello stack, fornendo così preziose informazioni su dove e perché si è verificata l'eccezione. Infine, poiché i log vengono generalmente scritti su file, è possibile esaminare e analizzare un record di eccezioni. Vedere il Listato 11 nella barra laterale per un esempio di registrazione delle tracce dello stack.

La registrazione delle eccezioni non è fondamentale in alcune situazioni specifiche. Uno di questi è pulire le risorse in una clausola finalmente.

Eccezioni finalmente

Nel Listato 2, alcuni dati vengono letti da un file. Il file deve essere chiuso indipendentemente dal fatto che un'eccezione legga i dati, quindi il close()metodo è racchiuso in una clausola finalmente. Ma se un errore chiude il file, non si può fare molto al riguardo:

Listato 2

public void loadFile (String fileName) genera IOException {InputStream in = null; prova {in = new FileInputStream (fileName); readSomeData (in); } infine {if (in! = null) {try {in.close (); } catch (IOException ioe) {// Ignored}}}}

Si noti che loadFile()riporta ancora un IOExceptional metodo chiamante se il caricamento dei dati effettivi non riesce a causa di un problema di I / O (input / output). Si noti inoltre che anche se un'eccezione da close()viene ignorata, il codice lo afferma esplicitamente in un commento per renderlo chiaro a chiunque lavori sul codice. È possibile applicare la stessa procedura per ripulire tutti i flussi di I / O, chiudere i socket e le connessioni JDBC e così via.

La cosa importante nell'ignorare le eccezioni è assicurarsi che un solo metodo sia racchiuso nel blocco try / catch ignorato (in modo che vengano comunque chiamati altri metodi nel blocco che lo racchiude) e che venga catturata un'eccezione specifica. Questa circostanza speciale differisce nettamente dalla cattura di un generico Exception. In tutti gli altri casi, l'eccezione dovrebbe essere (almeno) registrata, preferibilmente con un'analisi dello stack.

Non intercettare eccezioni generiche

Spesso in un software complesso, un determinato blocco di codice esegue metodi che generano una serie di eccezioni. Caricamento dinamico di una classe e istanziare un oggetto può lanciare diverse eccezioni diversi, tra cui ClassNotFoundException, InstantiationException, IllegalAccessException, e ClassCastException.

Invece di aggiungere i quattro diversi blocchi catch al blocco try, un programmatore impegnato può semplicemente racchiudere le chiamate al metodo in un blocco try / catch che cattura Exceptions generici (vedere il Listato 3 sotto). Anche se questo sembra innocuo, potrebbero verificarsi alcuni effetti collaterali indesiderati. Ad esempio, se className()è null, Class.forName()genererà un NullPointerException, che verrà catturato nel metodo.

In tal caso, il blocco catch cattura le eccezioni che non ha mai inteso catturare perché a NullPointerExceptionè una sottoclasse di RuntimeException, che a sua volta è una sottoclasse di Exception. Così i generici catch (Exception e)catture tutte le sottoclassi di RuntimeException, tra cui NullPointerException, IndexOutOfBoundsException, e ArrayStoreException. In genere, un programmatore non intende rilevare tali eccezioni.

Nel listato 3, i null classNamerisultati in a NullPointerException, che indica al metodo chiamante che il nome della classe non è valido:

Listato 3

public SomeInterface buildInstance (String className) {SomeInterface impl = null; prova {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Eccezione e) {log.error ("Errore durante la creazione della classe:" + className); } return impl; }

Un'altra conseguenza della clausola catch generica è che la registrazione è limitata perché catchnon conosce l'eccezione specifica rilevata. Alcuni programmatori, di fronte a questo problema, ricorrono all'aggiunta di un segno di spunta per vedere il tipo di eccezione (vedi Listato 4), che contraddice lo scopo dell'uso dei blocchi catch:

Listato 4

catch (Eccezione e) {if (e instanceof ClassNotFoundException) {log.error ("Nome classe non valido:" + className + "," + e.toString ()); } else {log.error ("Impossibile creare la classe:" + className + "," + e.toString ()); }}

Il Listato 5 fornisce un esempio completo di cattura di eccezioni specifiche a cui un programmatore potrebbe essere interessato. L' instanceofoperatore non è richiesto perché le eccezioni specifiche vengono catturate. Ognuna delle eccezioni controllate ( ClassNotFoundException, InstantiationException, IllegalAccessException) e 'colto e affrontato. Il caso speciale che produrrebbe a ClassCastException(la classe si carica correttamente, ma non implementa l' SomeInterfaceinterfaccia) viene verificato anche controllando tale eccezione:

Listato 5

public SomeInterface buildInstance(String className) { SomeInterface impl = null; try { Class clazz = Class.forName(className); impl = (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); } catch (InstantiationException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (IllegalAccessException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (ClassCastException e) { log.error("Invalid class type, " + className + " does not implement " + SomeInterface.class.getName()); } return impl; } 

In some cases, it is preferable to rethrow a known exception (or perhaps create a new exception) than try to deal with it in the method. This allows the calling method to handle the error condition by putting the exception into a known context.

Listing 6 below provides an alternate version of the buildInterface() method, which throws a ClassNotFoundException if a problem occurs while loading and instantiating the class. In this example, the calling method is assured to receive either a properly instantiated object or an exception. Thus, the calling method does not need to check if the returned object is null.

Note that this example uses the Java 1.4 method of creating a new exception wrapped around another exception to preserve the original stack trace information. Otherwise, the stack trace would indicate the method buildInstance() as the method where the exception originated, instead of the underlying exception thrown by newInstance():

Listing 6

public SomeInterface buildInstance(String className) throws ClassNotFoundException { try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); throw e; } catch (InstantiationException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e); } } 

In some cases, the code may be able to recover from certain error conditions. In these cases, catching specific exceptions is important so the code can figure out whether a condition is recoverable. Look at the class instantiation example in Listing 6 with this in mind.

In Listing 7, the code returns a default object for an invalid className, but throws an exception for illegal operations, like an invalid cast or a security violation.

Note:IllegalClassException is a domain exception class mentioned here for demonstration purposes.

Listing 7

public SomeInterface buildInstance(String className) throws IllegalClassException { SomeInterface impl = null; try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (InstantiationException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (IllegalAccessException e) { throw new IllegalClassException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e); } if (impl == null) { impl = new DefaultImplemantation(); } return impl; } 

When generic Exceptions should be caught

Certain cases justify when it is handy, and required, to catch generic Exceptions. These cases are very specific, but important to large, failure-tolerant systems. In Listing 8, requests are read from a queue of requests and processed in order. But if any exceptions occur while the request is being processed (either a BadRequestException or any subclass of RuntimeException, including NullPointerException), then that exception will be caught outside the processing while loop. So any error causes the processing loop to stop, and any remaining requests will not be processed. That represents a poor way of handling an error during request processing:

Listing 8

public void processAllRequests () {Request req = null; prova {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // genera BadRequestException} else {// La coda delle richieste è vuota, deve essere interrotta; }}} catch (BadRequestException e) {log.error ("Richiesta non valida:" + req, e); }}