Sök…


Introduktion

Flera missbruk av Java-programmeringsspråk kan utföra ett program för att generera felaktiga resultat trots att de har komponerats korrekt. Huvudsyftet med detta ämne är att lista vanliga fallgropar relaterade till undantagshantering och att föreslå rätt sätt att undvika att ha sådana fallgropar.

Fallgrop - Ignorera eller krossa undantag

Detta exempel handlar om att medvetet ignorera eller "krossa" undantag. Eller för att vara mer exakt, det handlar om hur man fångar och hanterar ett undantag på ett sätt som ignorerar det. Innan vi beskriver hur vi gör detta bör vi dock först påpeka att squashundantag i allmänhet inte är det rätta sättet att hantera dem på.

Undantag kastas vanligtvis (av något) för att meddela andra delar av programmet att någon betydande (dvs. "exceptionell") händelse har inträffat. Generellt (dock inte alltid) betyder ett undantag att något har gått fel. Om du kodar ditt program för att krossa undantaget finns det en god chans att problemet kommer att dyka upp i en annan form. För att förvärra saker och ting, kollar du bort informationen i undantagsobjektet och dess tillhörande stapelspår när du squashar undantaget. Det kommer sannolikt att göra det svårare att ta reda på vad den ursprungliga källan till problemet var.

I praktiken inträffar ofta squashing med undantag när du använder en IDE: s autokorrigeringsfunktion för att "fixa" ett kompilationsfel som orsakas av ett obehandlat undantag. Till exempel kan du se kod som denna:

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

Det är uppenbart att programmeraren har accepterat IDE: s förslag att få kompilationsfelet att försvinna, men förslaget var olämpligt. (Om den öppna filen har misslyckats bör programmet troligen göra något åt det. Med ovanstående "korrigering" kan programmet kanske misslyckas senare; t.ex. med en NullPointerException eftersom inputStream nu är null .)

Med detta sagt är här ett exempel på att medvetet krossa ett undantag. (För argumentets syfte, antar vi att vi har bestämt att ett avbrott när vi visar selfien är ofarligt.) Kommentaren berättar för läsaren att vi medvetet krossade undantaget och varför vi gjorde det.

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

Ett annat konventionellt sätt att lyfta fram att vi medvetet krossar ett undantag utan att säga varför är att ange detta med undantagsvariabelns namn, så här:

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

Vissa IDE: er (som IntelliJ IDEA) visar inte en varning om det tomma fångstblocket om variabelns namn är inställt på ignored .

Fallgrop - Fånga kast, undantag, fel eller RuntimeException

Ett vanligt tankemönster för oerfarna Java programmerare är att undantagen är "ett problem" eller "en börda" och det bästa sättet att hantera detta är fånga dem alla en så snart som möjligt. Detta leder till kod som denna:

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

Ovanstående kod har en betydande brist. catch kommer faktiskt att fånga fler undantag än programmeraren förväntar sig. Anta att värdet på fileName är null grund av ett fel någon annanstans i applikationen. Detta kommer att få FileInputStream konstruktören att kasta en NullPointerException . Handlaren kommer att fånga detta och rapportera till användaren:

    Could not open file null

vilket är hjälpsamt och förvirrande. Ännu värre, antar att det var "processen för inmatning" -koden som kastade det oväntade undantaget (markerat eller avmarkerat!). Nu får användaren det vilseledande meddelandet för ett problem som inte inträffade när filen öppnades, och kanske inte är relaterad till I / O alls.

Roten till problemet är att programmeraren har kodat en hanterare för Exception . Detta är nästan alltid ett misstag:

  • Fånga Exception kommer att fånga alla kontrolleras undantag, och de flesta okontrollerade undantag också.
  • Att fånga RuntimeException kommer att få de flesta okontrollerade undantag.
  • Fånga Error kommer att fånga okontrollerade undantag som signal JVM interna fel. Dessa fel kan i allmänhet inte återvinnas och bör inte fångas.
  • Att fånga Throwable kommer att fånga alla möjliga undantag.

Problemet med att fånga för bred undantag är att hanteraren vanligtvis inte kan hantera dem alla på lämpligt sätt. I fallet med Exception och så vidare, är det svårt för programmeraren att förutsäga vad som kan fångas; dvs vad du kan förvänta dig.

I allmänhet är den rätta lösningen för att ta itu med de undantag som kastas. Till exempel kan du fånga dem och hantera dem på plats:

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

eller så kan du förklara att de thrown med den bifogade metoden.


Det finns väldigt få situationer där det är lämpligt att fånga Exception . Den enda som uppstår ofta är något liknande:

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

Här vill vi verkligen ta itu med alla undantag, så att fånga Exception (eller till och med Throwable ) är korrekt.


1 - Även känd som Pokemon Exception Handling .

Fallgrop - Kasta kasta, undantag, fel eller RuntimeException

RuntimeException är dåligt att fånga Throwable , Exception , Error och RuntimeException är ännu värre att kasta dem.

Det grundläggande problemet är att när din ansökan behöver hantera undantag gör förekomsten av toppnivåundantag det svårt att skilja mellan olika felförhållanden. Till exempel

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
}

Problemet är att eftersom vi kastade en Exception tvingas vi fånga den. Men som beskrivs i ett annat exempel är att fånga Exception dåligt. I den här situationen blir det svårt att skilja mellan det "förväntade" fallet med ett Exception som kastas om somethingBad är true , och det oväntade fallet där vi faktiskt fångar ett okontrollerat undantag som NullPointerException .

Om det högsta undantaget får spridas, stöter vi på andra problem:

  • Vi måste nu komma ihåg alla de olika skälen till att vi kastade toppnivån och diskriminera / hantera dem.
  • När det gäller Exception och Throwable vi också lägga till dessa undantag till throws klausul om metoder om vi vill att undantaget ska spridas. Detta är problematiskt, som beskrivs nedan.

Kort sagt, kast inte dessa undantag. Kasta ett mer specifikt undantag som närmare beskriver den "exceptionella händelsen" som har hänt. Om du behöver definiera och använda en anpassad undantagsklass.

Att förklara kasta eller undantag i metodens "kast" är problematiskt.

Det är frestande att ersätta en lång lista över kastas undantag i metodens throws klausul med Exception eller `Throwable. Det här är en dålig idé:

  1. Det tvingar uppringaren att hantera (eller sprida) Exception .
  2. Vi kan inte längre lita på kompilatorn för att berätta om specifika kontrollerade undantag som måste hanteras.
  3. Att hantera Exception rätt sätt är svårt. Det är svårt att veta vilka faktiska undantag som kan fångas, och om du inte vet vad som kan fångas är det svårt att veta vilken återhämtningsstrategi som är lämplig.
  4. Att Throwable är ännu svårare, eftersom du nu också måste hantera potentiella fel som aldrig borde återhämtas från.

Detta råd innebär att vissa andra mönster bör undvikas. Till exempel:

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

Ovanstående försöker logga alla undantag när de passerar utan att definitivt hantera dem. Tyvärr, före Java 7, throw ex; uttalande fick kompilatorn att tro att alla Exception kunde kastas. Det kan tvinga dig att förklara den bifogade metoden som ett throws Exception . Från Java 7 och framåt vet kompilatorn att uppsättningen undantag som kan (kastas) där är mindre.

Pitfall - Catching InterruptException

Som redan påpekats i andra fallgropar, fånga alla undantag genom att använda

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

Kommer med många olika problem. Men ett pertikulärt problem är att det kan leda till dödlås eftersom det bryter avbrottssystemet när man skriver flertrådiga applikationer.

Om du startar en tråd måste du vanligtvis också kunna stoppa den abrupt av olika skäl.

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() kommer att höja en InterruptException i den tråden än vad som är avsett att stänga av tråden. Men tänk om tråden behöver rensa upp vissa resurser innan den slutar helt? För detta kan det fånga InterruptException och göra lite sanering.

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

Men om du har ett catch-all-uttryck i din kod, kommer InterruptException också att fångas av den och avbrottet fortsätter inte. Vilket i det här fallet kan leda till ett dödläge när överordnade tråden väntar på obestämd tid för att denna stav ska sluta med 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(); 
        }
    }
}

Så det är bättre att fånga undantag individuellt, men om du insisterar på att använda en catch-all, åtminstone fånga InterruptException individuellt i förväg.

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

Fallgrop - Använd undantag för normal flödeskontroll

Det finns ett mantra som vissa Java-experter inte kommer att recitera:

"Undantag bör endast användas i undantagsfall."

(Till exempel: http://programmers.stackexchange.com/questions/184654 )

Kärnan i detta är att det är en dålig idé (i Java) att använda undantag och undantagshantering för att implementera normal flödeskontroll. Jämför till exempel dessa två sätt att hantera en parameter som kan vara noll.

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

I det här exemplet behandlar vi (efter design) fallet där word är null som om det är ett tomt ord. De två versionerna handlar om null antingen med hjälp av konventionell om ... annars och eller försök ... fånga . Hur ska vi bestämma vilken version som är bättre?

Det första kriteriet är läsbarhet. Medan läsbarheten är svår att kvantifiera objektivt, skulle de flesta programmerare enas om att den väsentliga betydelsen av den första versionen är lättare att urskilja. För att verkligen förstå den andra formen, måste du förstå att en NullPointerException inte kan kastas med Math.min eller String.substring metoderna.

Det andra kriteriet är effektivitet. I utgivningar av Java före Java 8 är den andra versionen betydligt (storleksorder) långsammare än den första versionen. I synnerhet innebär konstruktionen av ett undantagsobjekt att fånga och spela in stapelramarna, i händelse av att stapelspåret krävs.

Å andra sidan finns det många situationer där användning av undantag är mer läsbar, effektivare och (ibland) mer korrekt än att använda villkorad kod för att hantera "exceptionella" händelser. Det finns faktiskt sällsynta situationer där det är nödvändigt att använda dem för "icke-exceptionella" händelser; dvs händelser som inträffar relativt ofta. För det senare är det värt att titta på sätt att minska omkostnaderna för att skapa undantagsobjekt.

Fallgrop - Överdrivna eller olämpliga stacktraces

En av de mer irriterande saker som programmerare kan göra är att sprida samtal till printStackTrace() genom sin kod.

Problemet är att printStackTrace() kommer att skriva stacktrace till standardutdata.

  • För en applikation som är avsedd för slutanvändare som inte är Java-programmerare är en stacktrace i bästa fall informativ och alarmerande i värsta fall.

  • För en applikation på serversidan är chansen stor att ingen kommer att titta på standardutgången.

En bättre idé är att inte ringa printStackTrace direkt, eller om du kallar det, gör det på ett sätt som stackspåret skrivs till en loggfil eller felfil snarare än till slutanvändarens konsol.

Ett sätt att göra detta är att använda ett loggningsramverk och passera undantagsobjektet som en parameter för logghändelsen. Men till och med att logga undantaget kan vara skadligt om det görs oskadligt. Tänk på följande:

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

Om undantaget kastas i method2 , kommer du sannolikt att se två kopior av samma stacktrace i loggfilen, motsvarande samma fel.

Kort sagt, antingen logga undantaget eller kasta det ytterligare (eventuellt inslaget med ett annat undantag). Gör inte båda.

Fallgrop - Direkt underklassificering "Kastbar"

Throwable har två direkta underklasser, Exception och Error . Även om det är möjligt att skapa en ny klass som utvidgar Throwable direkt, är detta otillräckligt eftersom många applikationer antar att endast Exception och Error finns.

Throwable är det ingen praktisk fördel med att direkt underklassera Throwable , eftersom den resulterande klassen i själva verket helt enkelt är ett kontrollerat undantag. Exception underklassning kommer istället att resultera i samma beteende, men kommer tydligare att förmedla din avsikt.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow