Suche…


Einführung

Bei einem Missbrauch von Java-Programmiersprachen kann es vorkommen, dass ein Programm fehlerhafte Ergebnisse generiert, obwohl es korrekt kompiliert wurde. Das Hauptziel dieses Themas ist es, häufige Fallstricke im Zusammenhang mit der Ausnahmebehandlung aufzulisten und den richtigen Weg vorzuschlagen, um solche Fallstricke zu vermeiden.

Pitfall - Ignorieren oder Quetschen von Ausnahmen

In diesem Beispiel werden Ausnahmen absichtlich ignoriert oder "gestaucht". Oder genauer gesagt, es geht darum, eine Ausnahme so zu erfassen und zu behandeln, dass sie ignoriert wird. Bevor wir jedoch beschreiben, wie dies zu tun ist, sollten wir zunächst darauf hinweisen, dass das Quetschen von Ausnahmen im Allgemeinen nicht die richtige Vorgehensweise ist.

Ausnahmen werden normalerweise (durch etwas) ausgelöst, um andere Teile des Programms darüber zu informieren, dass ein bedeutendes (dh "außergewöhnliches") Ereignis aufgetreten ist. Im Allgemeinen (wenn auch nicht immer) bedeutet eine Ausnahme, dass etwas schiefgegangen ist. Wenn Sie Ihr Programm so programmieren, dass es die Ausnahme unterdrückt, besteht eine gute Chance, dass das Problem in einer anderen Form erneut auftritt. Um es noch schlimmer zu machen, wenn Sie die Exception quetschen, werfen Sie die Informationen im Exception-Objekt und der zugehörigen Stack-Ablaufverfolgung weg. Das macht es wahrscheinlich schwieriger, die ursprüngliche Ursache des Problems herauszufinden.

In der Praxis kommt es häufig zu Ausnahmebereitschaft, wenn Sie die automatische Korrekturfunktion einer IDE verwenden, um einen Kompilierungsfehler zu beheben, der durch eine nicht behandelte Ausnahme verursacht wurde. Beispielsweise könnte Code wie folgt angezeigt werden:

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

Natürlich hat der Programmierer den Vorschlag der IDE akzeptiert, den Kompilierungsfehler zu beseitigen, der Vorschlag war jedoch unangemessen. (Wenn das Öffnen der Datei fehlgeschlagen ist, sollte das Programm höchstwahrscheinlich etwas dagegen unternehmen. Bei der obigen "Korrektur" kann das Programm später versagen; z. B. mit einer NullPointerException da inputStream jetzt null .)

Dies ist jedoch ein Beispiel für das absichtliche Zusammendrücken einer Ausnahme. (Nehmen Sie zur Argumentation an, dass wir festgestellt haben, dass ein Interrupt beim Anzeigen des Selfies harmlos ist.) Der Kommentar sagt dem Leser, dass wir die Ausnahme absichtlich zusammengedrückt haben und warum wir das getan haben.

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

Eine andere konventionelle Möglichkeit, hervorzuheben, dass wir eine Ausnahme absichtlich ausschließen, ohne zu sagen, warum, ist dies mit dem Namen der Ausnahmevariable anzugeben:

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

Einige IDEs (wie IntelliJ IDEA) zeigen keine Warnung bezüglich des leeren catch-Blocks an, wenn der Variablenname auf ignored .

Pitfall - Abfangen von Throwable, Exception, Error oder RuntimeException

Ein gemeinsames Denkmuster für unerfahrenen Java - Programmierer ist , dass Ausnahmen „ein Problem“ oder „Last“ sind und der beste Weg , damit umzugehen ist , sie alle 1 so schnell wie möglich zu fangen. Dies führt zu Code wie folgt:

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

Der obige Code hat einen erheblichen Fehler. Der catch tatsächlich mehr Ausnahmen, als der Programmierer erwartet. Nehmen wir an, dass der Wert der fileName ist null , aufgrund eines Fehlers an anderer Stelle in der Anwendung. Dies bewirkt, dass der FileInputStream Konstruktor eine NullPointerException . Der Handler wird dies abfangen und dem Benutzer mitteilen:

    Could not open file null

das ist nicht hilfreich und verwirrend. Schlimmer noch, nehmen Sie an, dass es der Code "Verarbeitung der Eingabe" war, der die unerwartete Ausnahmebedingung ausgelöst hat (aktiviert oder nicht angehakt!). Der Benutzer erhält nun die irreführende Meldung für ein Problem, das beim Öffnen der Datei nicht aufgetreten ist und möglicherweise nicht mit der E / A zusammenhängt.

Die Ursache des Problems ist, dass der Programmierer einen Handler für Exception codiert hat. Das ist fast immer ein Fehler:

  • Durch das Abfangen von Exception werden alle markierten Ausnahmen und die meisten ungeprüften Ausnahmen ebenfalls erfasst.
  • RuntimeException Abfangen von RuntimeException fängt die meisten ungeprüften Ausnahmen ab.
  • Catching Error fängt ungeprüfte Ausnahmen auf, die interne JVM-Fehler anzeigen. Diese Fehler können im Allgemeinen nicht behoben werden und sollten nicht abgefangen werden.
  • Catching Throwable fängt alle möglichen Ausnahmen auf.

Das Problem beim Erfassen einer zu breiten Anzahl von Ausnahmen besteht darin, dass der Handler normalerweise nicht alle angemessen behandeln kann. Im Falle der Exception und so weiter, ist es schwierig für den Programmierer vorherzusagen , was gefangen werden könnten; dh was zu erwarten ist.

Im Allgemeinen ist die richtige Lösung mit den Ausnahmen zu behandeln , die ausgelöst werden. Beispielsweise können Sie sie fangen und in situ handhaben:

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

oder Sie können sie als von der einschließenden Methode thrown erklären.


Es gibt sehr wenige Situationen, in denen das Ausnehmen von Exception angemessen ist. Die einzige, die häufig auftritt, ist etwa so:

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);
    }
}

Hier wollen wir uns wirklich mit allen Ausnahmen befassen, also ist das Fangen von Exception (oder sogar Throwable ) richtig.


1 - Wird auch als Pokemon-Ausnahmebehandlung bezeichnet .

Pitfall - Throwable Throwable, Exception, Error oder RuntimeException

Die Exception Throwable , Exception , Error und RuntimeException sind zwar schlecht, aber sie zu werfen ist noch schlimmer.

Das grundlegende Problem ist, dass die Ausnahmebedingungen der obersten Ebene die Unterscheidung zwischen verschiedenen Fehlerbedingungen schwierig machen, wenn Ihre Anwendung mit Ausnahmen umgehen muss. Zum Beispiel

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
}

Das Problem ist, dass wir, weil wir eine Exception Instanz geworfen haben, gezwungen werden, sie zu fangen. Wie in einem anderen Beispiel beschrieben, ist das Abfangen von Exception jedoch schlecht. In dieser Situation wird es schwierig , zwischen dem „erwarteten“ Fall einen diskriminieren Exception , die ausgelöst wird , wenn somethingBad ist true , und der unerwarteten Fall, dass wir tatsächlich eine ungeprüfte Ausnahme wie fangen NullPointerException .

Wenn sich die Ausnahme der obersten Ebene ausbreiten darf, treten andere Probleme auf:

  • Wir müssen uns nun an die verschiedenen Gründe erinnern, die wir auf die oberste Ebene geworfen haben, und diese diskriminieren / behandeln.
  • Im Falle von Exception und Throwable wir diese Exceptions auch der throws Klausel von Methoden hinzufügen, wenn die Exception propagiert werden soll. Dies ist problematisch, wie unten beschrieben.

Kurz gesagt, werfen Sie diese Ausnahmen nicht auf. Wirft eine spezifischere Ausnahme, die das aufgetretene "außergewöhnliche Ereignis" genauer beschreibt. Definieren und verwenden Sie ggf. eine benutzerdefinierte Ausnahmeklasse.

Das Deklarieren von Throwable oder Exception in "Throws" einer Methode ist problematisch.

Es ist verlockend , eine lange Liste von Ausnahmen geworfen in einem Verfahren zu ersetzen , der throws Klausel mit Exception oder sogar `Throwable. Das ist eine schlechte Idee:

  1. Der Aufrufer wird gezwungen, die Exception zu behandeln (oder zu verbreiten).
  2. Wir können uns nicht mehr darauf verlassen, dass der Compiler uns bestimmte geprüfte Ausnahmen mitteilt, die behandelt werden müssen.
  3. Die richtige Handhabung von Exception ist schwierig. Es ist schwer zu wissen, welche Ausnahmen tatsächlich gefasst werden könnten, und wenn Sie nicht wissen, was gefangen werden könnte, ist es schwierig zu wissen, welche Erholungsstrategie geeignet ist.
  4. Der Umgang mit Throwable ist noch schwieriger, da Sie jetzt auch mit potenziellen Ausfällen fertig werden müssen, bei denen niemals eine Throwable ist.

Dieser Hinweis bedeutet, dass bestimmte andere Muster vermieden werden sollten. Zum Beispiel:

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

Die oben genannten Versuche, alle Ausnahmen zu protokollieren, ohne sie endgültig zu behandeln. Leider wurde vor Java 7 der throw ex; Die Anweisung veranlasste den Compiler zu der Annahme, dass eine Exception ausgelöst werden könnte. Dies kann dazu führen, dass Sie die einschließende Methode als throws Exception deklarieren. Der Compiler weiß ab Java 7, dass der Satz von Ausnahmen, die dort (erneut ausgelöst) werden könnten, kleiner ist.

Pitfall - InterruptedException abfangen

Wie bereits in anderen Fallstricken dargelegt, alle Ausnahmen mit fangen

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

Kommt mit vielen verschiedenen Problemen. Ein besonderes Problem ist jedoch, dass es zu Deadlocks kommen kann, da das Interrupt-System beim Schreiben von Multithread-Anwendungen unterbrochen wird.

Wenn Sie einen Thread starten, müssen Sie in der Regel aus verschiedenen Gründen abrupt stoppen können.

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() eine InterruptedException in diesem Thread aus, um den Thread herunterzufahren. Was aber, wenn der Thread einige Ressourcen bereinigen muss, bevor er vollständig gestoppt wird? Dafür kann es die InterruptedException abfangen und einige Bereinigungen durchführen.

 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(); 
        }
    }
}

Wenn Sie jedoch einen Catch-All-Ausdruck in Ihrem Code haben, wird die InterruptedException ebenfalls davon abgefangen und die Unterbrechung wird nicht fortgesetzt. Was in diesem Fall zu einem Deadlock führen könnte, da der übergeordnete Thread unbestimmt darauf wartet, dass dieser mit 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(); 
        }
    }
}

Es ist also besser, Exceptions einzeln abzufangen, aber wenn Sie auf der Verwendung eines Catch-All bestehen, sollten Sie die InterruptedException mindestens vorher einzeln abfangen.

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(); 
        }
    }
}

Fallstricke - Verwenden von Ausnahmen für die normale Flusskontrolle

Es gibt ein Mantra, das einige Java-Experten zu rezitieren pflegen:

"Ausnahmen sollten nur in Ausnahmefällen verwendet werden."

(Zum Beispiel: http://programmers.stackexchange.com/questions/184654 )

Das Wesentliche dabei ist, dass es (in Java) eine schlechte Idee ist, Ausnahmen und Ausnahmebehandlung zu verwenden, um die normale Flusssteuerung zu implementieren. Vergleichen Sie zum Beispiel diese beiden Arten des Umgangs mit einem Parameter, der null sein könnte.

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 diesem Beispiel behandeln wir (durch das Design) den Fall, dass das word null als wäre es ein leeres Wort. Die beiden Versionen behandeln null entweder mit konventionellem if ... else und oder versuchen Sie ... catch . Wie sollen wir entscheiden, welche Version besser ist?

Das erste Kriterium ist die Lesbarkeit. Obwohl Lesbarkeit schwer objektiv zu quantifizieren ist, würden sich die meisten Programmierer darin einig sein, dass die grundlegende Bedeutung der ersten Version leichter zu erkennen ist. Um das zweite Formular wirklich verstehen zu können, müssen Sie NullPointerException , dass eine NullPointerException nicht von den Math.min oder String.substring Methoden String.substring werden kann.

Das zweite Kriterium ist die Effizienz. In Java-Versionen vor Java 8 ist die zweite Version erheblich (um Größenordnungen) langsamer als die erste Version. Insbesondere erfordert der Aufbau eines Ausnahmeobjekts das Erfassen und Aufzeichnen der Stapelrahmen, nur für den Fall, dass die Stapelverfolgung erforderlich ist.

Auf der anderen Seite gibt es viele Situationen, in denen die Verwendung von Ausnahmen lesbarer, effizienter und (manchmal) korrekter ist als die Verwendung von Bedingungscode für "außergewöhnliche" Ereignisse. In seltenen Fällen ist es notwendig, sie für "nicht außergewöhnliche" Ereignisse zu verwenden. dh Ereignisse, die relativ häufig auftreten. Für Letzteres lohnt es sich, nach Möglichkeiten zu suchen, um den Aufwand für das Erstellen von Ausnahmeobjekten zu reduzieren.

Pitfall - Übermäßige oder unangemessene Stacktraces

Das Ärgerlichste, was Programmierer tun können, ist, Aufrufe von printStackTrace() in ihrem gesamten Code zu printStackTrace() .

Das Problem ist, dass printStackTrace() den Stacktrace in die Standardausgabe schreibt.

  • Für eine Anwendung, die für Endbenutzer gedacht ist, die kein Java-Programmierer sind, ist ein Stacktrace bestenfalls nicht informativ und im schlimmsten Fall alarmierend.

  • Bei einer serverseitigen Anwendung besteht die Möglichkeit, dass niemand die Standardausgabe betrachtet.

Besser ist es, printStackTrace direkt printStackTrace Wenn Sie es aufrufen, tun Sie dies so, dass die Stack-Ablaufverfolgung in eine Protokolldatei oder Fehlerdatei und nicht in die Konsole des Endbenutzers geschrieben wird.

Eine Möglichkeit, dies zu tun, besteht darin, ein Protokollierungsframework zu verwenden und das Exception-Objekt als Parameter des Protokollereignisses zu übergeben. Die Protokollierung der Ausnahme kann jedoch schädlich sein, wenn sie nicht ordnungsgemäß ausgeführt wird. Folgendes berücksichtigen:

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;
    }
}

Wenn die Ausnahme in method2 ausgelöst wird, werden in der method2 wahrscheinlich zwei Kopien desselben Stacktraces angezeigt, die demselben Fehler entsprechen.

Kurz gesagt, entweder die Ausnahme protokollieren oder erneut ausführen (möglicherweise mit einer anderen Ausnahme verpackt). Beides nicht tun

Pitfall - Direkt Unterklasse "werfen"

Throwable hat zwei direkte Unterklassen, Exception und Error . Es ist zwar möglich, eine neue Klasse zu erstellen, die Throwable direkt erweitert. Dies ist jedoch nicht zu Throwable , da viele Anwendungen nur Exception und Error Throwable .

Throwable auf den Punkt zu bringen, gibt es keinen praktischen Nutzen für die direkte Unterklasse Throwable , da die resultierende Klasse Throwable nur eine geprüfte Ausnahme ist. Subclassing Exception stattdessen zu demselben Verhalten, vermittelt jedoch Ihre Absicht klarer.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow