Ricerca…


introduzione

Diverse attività linguistiche di programmazione Java potrebbero condurre un programma a generare risultati errati nonostante siano stati compilati correttamente. Lo scopo principale di questo argomento è elencare le insidie ​​più comuni relative alla gestione delle eccezioni e proporre il modo corretto per evitare tali insidie.

Pitfall - Eccezioni ignoranti o di schiacciamento

Questo esempio riguarda deliberatamente ignorare o "schiacciare" le eccezioni. O per essere più precisi, si tratta di come catturare e gestire un'eccezione in un modo che la ignora. Tuttavia, prima di descrivere come farlo, dovremmo innanzitutto sottolineare che le eccezioni dello schiacciamento non sono generalmente il modo corretto per gestirle.

Le eccezioni vengono solitamente generate (da qualcosa) per notificare ad altre parti del programma che si è verificato un evento significativo (cioè "eccezionale"). Generalmente (sebbene non sempre) un'eccezione significa che qualcosa è andato storto. Se si codifica il programma per eliminare l'eccezione, è probabile che il problema riappaia in un'altra forma. Per peggiorare le cose, quando si schiaccia l'eccezione, si eliminano le informazioni nell'oggetto eccezione e la relativa traccia dello stack associata. È probabile che sia più difficile capire quale fosse la fonte originale del problema.

In pratica, lo schiacciamento delle eccezioni si verifica spesso quando si utilizza la funzione di correzione automatica dell'IDE per "correggere" un errore di compilazione causato da un'eccezione non gestita. Ad esempio, potresti vedere un codice come questo:

try {
    inputStream = new FileInputStream("someFile");
} catch (IOException e) {
    /* add exception handling code here */
}

Chiaramente, il programmatore ha accettato il suggerimento dell'IDE di far sparire l'errore di compilazione, ma il suggerimento era inappropriato. (Se il file aperto non funziona, probabilmente il programma dovrebbe fare qualcosa al riguardo: con la "correzione" sopra, il programma potrebbe fallire in un secondo momento, ad es. Con NullPointerException perché inputStream ora è null ).

Detto questo, ecco un esempio di deliberatamente schiacciare un'eccezione. (Ai fini della discussione, supponiamo di aver determinato che un'interruzione mentre si mostra il selfie è innocua.) Il commento dice al lettore che abbiamo schiacciato deliberatamente l'eccezione, e perché lo abbiamo fatto.

try {
    selfie.show();
} catch (InterruptedException e) {
    // It doesn't matter if showing the selfie is interrupted.
}

Un altro modo convenzionale per evidenziare che stiamo deliberatamente schiacciando un'eccezione senza dire perché è quello di indicare questo con il nome della variabile di eccezione, come questo:

try { 
    selfie.show(); 
} catch (InterruptedException ignored) {  }

Alcuni IDE (come IntelliJ IDEA) non visualizzeranno un avviso sul blocco catch vuoto se il nome della variabile è impostato su ignored .

Pitfall - Catching Throwable, Exception, Error o RuntimeException

Uno schema di pensiero comune per i programmatori Java inesperti è che le eccezioni sono "un problema" o "un peso" e il modo migliore per affrontarle è catturarle tutte 1 il prima possibile. Questo porta a codice come questo:

....
try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (Exception ex) {
    System.out.println("Could not open file " + fileName);
}

Il codice sopra riportato ha un difetto significativo. La catch è in realtà andando a prendere più eccezioni che il programmatore si aspetta. Supponiamo che il valore di fileName sia null , a causa di un bug altrove nell'applicazione. Ciò farà sì che il costruttore FileInputStream lanci una NullPointerException . Il gestore prenderà questo e riferirà all'utente:

    Could not open file null

che è inutile e confuso. Peggio ancora, supponiamo che sia stato il codice "process the input" che ha lanciato l'eccezione inaspettata (selezionata o deselezionata!). Ora l'utente riceverà il messaggio fuorviante per un problema che non si è verificato durante l'apertura del file e potrebbe non essere affatto correlato all'I / O.

La radice del problema è che il programmatore ha codificato un gestore per Exception . Questo è quasi sempre un errore:

  • L' Exception cattura rileverà tutte le eccezioni controllate e anche la maggior parte delle eccezioni non controllate.
  • Catching RuntimeException catturerà la maggior parte delle eccezioni non controllate.
  • Error cattura rileverà eccezioni non controllate che segnalano errori interni JVM. Questi errori non sono generalmente recuperabili e non dovrebbero essere scoperti.
  • Catching Throwable catturerà tutte le possibili eccezioni.

Il problema con l'acquisizione di un insieme troppo ampio di eccezioni è che il gestore in genere non è in grado di gestirli tutti in modo appropriato. Nel caso Exception e così via, è difficile per il programmatore prevedere cosa potrebbe essere catturato; cioè cosa aspettarsi.

In generale, la soluzione corretta è quello di affrontare le eccezioni che vengono gettati. Ad esempio, puoi prenderli e gestirli in situ:

try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (FileNotFoundException ex) {
    System.out.println("Could not open file " + fileName);
}

oppure puoi dichiararli come thrown dal metodo di inclusione.


Ci sono pochissime situazioni in cui è opportuno catturare l' Exception . L'unico che si presenta comunemente è qualcosa del genere:

public static void main(String[] args) {
    try {
        // do stuff
    } catch (Exception ex) {
        System.err.println("Unfortunately an error has occurred. " +
                           "Please report this to X Y Z");
        // Write stacktrace to a log file.
        System.exit(1);
    }
}

Qui vogliamo sinceramente occuparci di tutte le eccezioni, quindi prendere Exception (o anche Throwable ) è corretto.


1 - Conosciuto anche come Pokemon Exception Handling .

Trappola - Lancio Throwable, Exception, Error o RuntimeException

Anche se la cattura delle Exception Throwable , Exception , Error e RuntimeException è buona, lanciarle è ancora peggio.

Il problema di base è che quando l'applicazione deve gestire le eccezioni, la presenza delle eccezioni di primo livello rende difficile discriminare tra diverse condizioni di errore. Per esempio

try {
    InputStream is = new FileInputStream(someFile);  // could throw IOException
    ...
    if (somethingBad) {
        throw new Exception();  // WRONG
    }
} catch (IOException ex) {
    System.err.println("cannot open ...");
} catch (Exception ex) {
    System.err.println("something bad happened");  // WRONG
}

Il problema è che, poiché abbiamo lanciato un'istanza Exception , siamo costretti a prenderlo. Tuttavia, come descritto in un altro esempio, la cattura di Exception è errata. In questa situazione, diventa difficile distinguere tra il caso "attesa" di Exception che viene generata se somethingBad è true , e il caso inaspettato in cui abbiamo effettivamente rilevare un'eccezione incontrollato come NullPointerException .

Se è consentita la propagazione dell'eccezione di primo livello, ci imbattiamo in altri problemi:

  • Ora dobbiamo ricordare tutti i diversi motivi per cui abbiamo lanciato il livello più alto e discriminato / gestito.
  • Nel caso di Exception e Throwable dobbiamo anche aggiungere queste eccezioni alla clausola dei metodi throws se vogliamo che l'eccezione si propaghi. Questo è problematico, come descritto di seguito.

In breve, non gettare queste eccezioni. Getta un'eccezione più specifica che descrive più da vicino l'evento eccezionale che è accaduto. Se necessario, definire e utilizzare una classe di eccezioni personalizzata.

Dichiarare Throwable o Exception nei "lanci" di un metodo è problematico.

Si è tentati di sostituire una lunga lista di eccezioni generate in un metodo di throws clausola con Exception o anche `Throwable. Questa è una cattiva idea:

  1. Costringe il chiamante a gestire (o propagare) l' Exception .
  2. Non possiamo più fare affidamento sul compilatore per dirci quali eccezioni controllate specifiche devono essere gestite.
  3. Gestire correttamente l' Exception è difficile. È difficile sapere quali sono le reali eccezioni che possono essere scoperte e se non sai cosa potrebbe essere catturato, è difficile sapere quale strategia di ripristino sia appropriata.
  4. Handling Throwable è ancora più difficile, dal momento che ora devi affrontare anche i potenziali fallimenti che non dovrebbero mai essere recuperati.

Questo consiglio significa che alcuni altri modelli dovrebbero essere evitati. Per esempio:

try {
    doSomething();
} catch (Exception ex) {
    report(ex);
    throw ex;
}

Quanto sopra tenta di registrare tutte le eccezioni mentre passano, senza gestirle definitivamente. Sfortunatamente, prima di Java 7, il throw ex; dichiarazione ha indotto il compilatore a pensare che qualsiasi Exception potesse essere lanciata. Questo potrebbe costringerti a dichiarare il metodo di inclusione come throws Exception . Da Java 7 in poi, il compilatore sa che l'insieme di eccezioni che potrebbero essere (ri-generate) è più piccolo.

Pitfall - Catching InterruptedException

Come già sottolineato in altre insidie, prendendo tutte le eccezioni usando

try {
    // Some code
} catch (Exception) {
    // Some error handling
}

Viene fornito con molti problemi diversi. Ma un problema perticolare è che può portare a deadlock in quanto interrompe il sistema di interrupt durante la scrittura di applicazioni multi-thread.

Se inizi una discussione, di solito devi anche essere in grado di fermarla bruscamente per vari motivi.

Thread t = new Thread(new Runnable() {
    public void run() {
         while (true) {
             //Do something indefinetely
         }
    }
}

t.start();

//Do something else

// The thread should be canceld if it is still active. 
// A Better way to solve this is with a shared variable that is tested 
// regularily by the thread for a clean exit, but for this example we try to 
// forcibly interrupt this thread.
if (t.isAlive()) {
   t.interrupt();
   t.join();
}

//Continue with program

t.interrupt() genererà un InterruptedException in quel thread, che è inteso per arrestare il thread. Ma cosa succede se il thread ha bisogno di ripulire alcune risorse prima che si fermi completamente? Per questo può prendere l'InterruptedException e fare un po 'di pulizia.

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                //Do something indefinetely
            }
        } catch (InterruptedException ex) {
            //Do some quick cleanup

            // In this case a simple return would do. 
            // But if you are not 100% sure that the thread ends after 
            // catching the InterruptedException you will need to raise another 
            // one for the layers surrounding this code.                
            Thread.currentThread().interrupt(); 
        }
    }
}

Ma se hai un'espressione catch-all nel tuo codice, anche l'InterruptedException verrà catturata da esso e l'interruzione non continuerà. Che in questo caso potrebbe portare a un deadlock come thread genitore attende indefinitamente per questo thead per interrompere con t.join() .

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                }
                catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Dead code as the interrupt exception was already caught in
            // the inner try-catch           
            Thread.currentThread().interrupt(); 
        }
    }
}

Quindi è meglio catturare le eccezioni individualmente, ma se insisti a usare un catch-all, prendi almeno l'InterruptedException individualmente in anticipo.

Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                } catch (InterruptedException ex) {
                    throw ex; //Send it up in the chain
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Some quick cleanup code 
    
            Thread.currentThread().interrupt(); 
        }
    }
}

Pitfall - Utilizzo delle eccezioni per il normale controllo del flusso

C'è un mantra che alcuni esperti di Java sono soliti recitare:

"Le eccezioni dovrebbero essere utilizzate solo per casi eccezionali."

(Ad esempio: http://programmers.stackexchange.com/questions/184654 )

L'essenza di questo è che è una cattiva idea (in Java) utilizzare le eccezioni e la gestione delle eccezioni per implementare il normale controllo del flusso. Ad esempio, confronta questi due modi di trattare un parametro che potrebbe essere nullo.

public String truncateWordOrNull(String word, int maxLength) {
    if (word == null) {
        return "";
    } else {
        return word.substring(0, Math.min(word.length(), maxLength));
    }
}

public String truncateWordOrNull(String word, int maxLength) {
    try {
        return word.substring(0, Math.min(word.length(), maxLength));
    } catch (NullPointerException ex) {
        return "";
    }
}

In questo esempio, siamo (in base alla progettazione) trattando il caso in cui la word è null come se fosse una parola vuota. Le due versioni si occupano di null usando convenzionale se ... else e o try ... catch . Come dovremmo decidere quale versione è migliore?

Il primo criterio è la leggibilità. Sebbene la leggibilità sia difficile da quantificare oggettivamente, la maggior parte dei programmatori concorda sul fatto che il significato essenziale della prima versione sia più facile da comprendere. In effetti, per capire veramente il secondo modulo, è necessario capire che una NullPointerException non può essere lanciata dai metodi Math.min o String.substring .

Il secondo criterio è l'efficienza. Nelle versioni di Java precedenti a Java 8, la seconda versione è significativamente (ordini di grandezza) più lenta della prima versione. In particolare, la costruzione di un oggetto di eccezione comporta l'acquisizione e la registrazione degli stackframes, nel caso sia necessario lo stacktrace.

D'altra parte, ci sono molte situazioni in cui l'uso delle eccezioni è più leggibile, più efficiente e (a volte) più corretto dell'uso del codice condizionale per gestire eventi "eccezionali". In effetti, ci sono rare situazioni in cui è necessario utilizzarle per eventi "non eccezionali"; cioè eventi che si verificano relativamente frequentemente. Per quest'ultimo, vale la pena esaminare i modi per ridurre i costi generali della creazione di oggetti di eccezione.

Pitfall - Stacktraces eccessivi o inappropriati

Una delle cose più fastidiose che i programmatori possono fare è distribuire le chiamate a printStackTrace() attraverso il loro codice.

Il problema è che printStackTrace() sta per scrivere lo stacktrace sullo standard output.

  • Per un'applicazione destinata agli utenti finali che non sono programmatori Java, uno stacktrace non è informativo al meglio e allarmante nel peggiore dei casi.

  • Per un'applicazione lato server, è probabile che nessuno guarderà lo standard output.

Un'idea migliore è quella di non chiamare direttamente printStackTrace , o se lo si chiama, farlo in modo che la traccia dello stack sia scritta in un file di registro o in un file di errore piuttosto che nella console dell'utente finale.

Un modo per farlo consiste nell'utilizzare un framework di registrazione e passare l'oggetto di eccezione come parametro dell'evento di registro. Tuttavia, anche la registrazione dell'eccezione può essere dannosa se eseguita in modo non appropriato. Considera quanto segue:

public void method1() throws SomeException {
    try {
        method2();
        // Do something
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method1", ex);
        throw ex;
    }
}

public void method2() throws SomeException {
    try {
        // Do something else
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method2", ex);
        throw ex;
    }
}

Se l'eccezione è lanciata in method2 , è probabile che si vedano due copie dello stesso stacktrace nel file di log, corrispondente allo stesso errore.

In breve, registra l'eccezione o rilancia ulteriormente (eventualmente racchiusa in un'altra eccezione). Non fare entrambe le cose.

Pitfall - Direttamente sottoclassando `Throwable`

Throwable ha due sottoclassi dirette, Exception ed Error . Sebbene sia possibile creare una nuova classe che estenda Throwable direttamente, ciò è sconsigliabile dal momento che molte applicazioni presuppongono solo Exception ed Error .

Più Throwable , non vi è alcun vantaggio pratico per la sottoclasse diretta di Throwable , poiché la classe risultante è, in effetti, semplicemente un'eccezione controllata. Subclassing Exception invece produrrà lo stesso comportamento, ma renderà più chiaro il tuo intento.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow