Java Language
Java-valkuilen - Uitzonderingsgebruik
Zoeken…
Invoering
Verschillende misbruik van Java-programmeertaal kan een programma uitvoeren om onjuiste resultaten te genereren, ondanks dat het correct is gecompileerd. Het hoofddoel van dit onderwerp is om veel voorkomende valkuilen op te sommen die verband houden met de afhandeling van uitzonderingen , en om de juiste manier voor te stellen om dergelijke valkuilen te voorkomen.
Valkuil - Uitzonderingen negeren of pletten
Dit voorbeeld gaat over het opzettelijk negeren of "pletten" van uitzonderingen. Of om preciezer te zijn, het gaat erom een uitzondering te vangen en te verwerken op een manier die deze negeert. Voordat we echter beschrijven hoe we dit moeten doen, moeten we er eerst op wijzen dat het pletten van uitzonderingen over het algemeen niet de juiste manier is om hiermee om te gaan.
Uitzonderingen worden meestal (door iets) gegooid om andere delen van het programma te melden dat er een belangrijke (dwz "uitzonderlijke") gebeurtenis heeft plaatsgevonden. Over het algemeen (hoewel niet altijd) betekent een uitzondering dat er iets mis is gegaan. Als u uw programma codeert om de uitzondering te pletten, is er een redelijke kans dat het probleem in een andere vorm opnieuw verschijnt. Om het nog erger te maken, gooit u de informatie in het uitzonderingsobject en de bijbehorende stacktrac weg wanneer u de uitzondering squasht. Dat zal het waarschijnlijk moeilijker maken om erachter te komen wat de oorspronkelijke oorzaak van het probleem was.
In de praktijk komt het pletten van uitzonderingen vaak voor wanneer u de auto-correctiefunctie van een IDE gebruikt om een compilatiefout te "repareren" die wordt veroorzaakt door een onverwerkte uitzondering. U ziet bijvoorbeeld code zoals deze:
try {
inputStream = new FileInputStream("someFile");
} catch (IOException e) {
/* add exception handling code here */
}
Het is duidelijk dat de programmeur de suggestie van de IDE heeft aanvaard om de compilatiefout te laten verdwijnen, maar de suggestie was ongepast. (Als het geopende bestand is mislukt, zou het programma er waarschijnlijk iets aan moeten doen. Met de bovenstaande "correctie" kan het programma later mislukken; bijvoorbeeld met een NullPointerException
omdat inputStream
nu null
.)
Dat gezegd hebbende, hier is een voorbeeld van het opzettelijk pletten van een uitzondering. (Neem voor argumentatie aan dat we hebben vastgesteld dat een onderbreking tijdens het tonen van de selfie onschadelijk is.) De opmerking vertelt de lezer dat we de uitzondering opzettelijk hebben geplet en waarom we dat deden.
try {
selfie.show();
} catch (InterruptedException e) {
// It doesn't matter if showing the selfie is interrupted.
}
Een andere conventionele manier om te benadrukken dat we opzettelijk een uitzondering pletten zonder te zeggen waarom, is dit aan te geven met de naam van de uitzonderingsvariabele, zoals deze:
try {
selfie.show();
} catch (InterruptedException ignored) { }
Sommige IDE's (zoals IntelliJ IDEA) geven geen waarschuwing over het lege catch-blok als de variabelenaam is ingesteld op ignored
.
Valkuil - Gooi vangen, uitzondering, fout of RuntimeException
Een veel voorkomende gedachte patroon voor onervaren Java-programmeurs is dat er uitzonderingen zijn "een probleem" of "last" en de beste manier om te gaan met dit is te vangen ze allemaal 1 zo snel mogelijk. Dit leidt tot code zoals deze:
....
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (Exception ex) {
System.out.println("Could not open file " + fileName);
}
De bovenstaande code heeft een aanzienlijk gebrek. De catch
gaat eigenlijk meer uitzonderingen vangen dan de programmeur verwacht. Stel dat de waarde van de fileName
null
is vanwege een bug elders in de toepassing. Hierdoor zal de constructor FileInputStream
een NullPointerException
gooien. De handler vangt dit op en rapporteert aan de gebruiker:
Could not open file null
wat nutteloos en verwarrend is. Erger nog, stel dat het de "verwerk de invoer" -code was die de onverwachte uitzondering trok (aangevinkt of niet aangevinkt!). Nu krijgt de gebruiker het misleidende bericht voor een probleem dat niet is opgetreden tijdens het openen van het bestand en mogelijk helemaal niet gerelateerd is aan I / O.
De kern van het probleem is dat de programmeur een handler voor Exception
heeft gecodeerd. Dit is bijna altijd een fout:
- Catching-
Exception
zal alle aangevinkte uitzonderingen vangen, en de meeste niet-aangevinkte uitzonderingen ook. - Het vangen van
RuntimeException
zal de meeste ongecontroleerde uitzonderingen vangen. - Catching
Error
vangt ongecontroleerde uitzonderingen op die interne JVM-fouten signaleren. Deze fouten kunnen over het algemeen niet worden hersteld en mogen niet worden opgevangen. - Catching
Throwable
vangt alle mogelijke uitzonderingen op.
Het probleem met het vangen van een te brede reeks uitzonderingen is dat de handler ze meestal niet allemaal correct kan verwerken. In het geval van de Exception
enzovoort, is het voor de programmeur moeilijk om te voorspellen wat er zou kunnen worden betrapt; dat wil zeggen wat te verwachten.
Over het algemeen is de juiste oplossing om te gaan met de uitzonderingen die worden gegenereerd. U kunt ze bijvoorbeeld vangen en in situ verwerken:
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (FileNotFoundException ex) {
System.out.println("Could not open file " + fileName);
}
of je kunt ze verklaren als thrown
door de omhullende methode.
Er zijn maar weinig situaties waarin het vangen van Exception
geschikt is. De enige die gewoonlijk ontstaat is zoiets als dit:
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 willen we echt met alle uitzonderingen omgaan, dus het vangen van Exception
(of zelfs Throwable
) is correct.
1 - Ook bekend als Pokemon Exception Handling .
Valkuil - Throwable Throwable, Exception, Error of RuntimeException
Hoewel het vangen van de Exception
Throwable
, Exception
, Error
en RuntimeException
slecht is, is het nog erger om ze te gooien.
Het basisprobleem is dat wanneer uw toepassing uitzonderingen moet verwerken, de aanwezigheid van uitzonderingen op het hoogste niveau het moeilijk maakt om onderscheid te maken tussen verschillende foutcondities. Bijvoorbeeld
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
}
Het probleem is dat we, omdat we een Exception
instantie hebben gegooid, deze moeten vangen. Zoals beschreven in een ander voorbeeld, is het vangen van Exception
echter slecht. In deze situatie wordt het moeilijk om onderscheid te maken tussen het "verwachte" geval van een Exception
die wordt gegenereerd als somethingBad
NullPointerException
true
, en het onverwachte geval waarin we een ongecontroleerde uitzondering zoals NullPointerException
.
Als de uitzondering op het hoogste niveau zich mag verspreiden, komen we andere problemen tegen:
- We moeten nu alle verschillende redenen onthouden waarom we het hoogste niveau hebben gegooid en ze discrimineren / hanteren.
- In het geval van
Exception
andThrowable
we deze uitzonderingen ook toevoegen aan dethrows
clausule van methoden als we willen dat de uitzondering zich voortplant. Dit is problematisch, zoals hieronder beschreven.
Kortom, gooi deze uitzonderingen niet. Gooi een meer specifieke uitzondering die de "uitzonderlijke gebeurtenis" die is gebeurd beter beschrijft. Definieer en gebruik indien nodig een aangepaste uitzonderingsklasse.
Het verklaren van Gooi of Uitzondering in de "worpen" van een methode is problematisch.
Het is verleidelijk om een lange lijst met gegooide uitzonderingen in de clausule van de methode throws
te vervangen door Exception
of zelfs `Throwable. Dit is een slecht idee:
- Het dwingt de beller om
Exception
te handelen (of door te geven). - We kunnen niet langer vertrouwen op de compiler om ons te informeren over specifieke gecontroleerde uitzonderingen die moeten worden afgehandeld.
- Het correct omgaan met de
Exception
is moeilijk. Het is moeilijk om te weten welke daadwerkelijke uitzonderingen kunnen worden gevangen, en als u niet weet wat zou kunnen worden gevangen, is het moeilijk om te weten welke herstelstrategie geschikt is. - Het omgaan met
Throwable
is nog moeilijker, omdat u nu ook te maken krijgt met mogelijke storingen waarvan u nooit meer kunt herstellen.
Dit advies betekent dat bepaalde andere patronen moeten worden vermeden. Bijvoorbeeld:
try {
doSomething();
} catch (Exception ex) {
report(ex);
throw ex;
}
Het bovenstaande probeert alle uitzonderingen te registreren terwijl ze voorbijgaan, zonder ze definitief af te handelen. Helaas, voorafgaand aan Java 7, de throw ex;
statement deed de compiler denken dat elke Exception
kon worden gegenereerd. Dat zou je kunnen dwingen om de omhullende methode te verklaren als throws Exception
. Vanaf Java 7 weet de compiler dat de set uitzonderingen die daar (opnieuw) kunnen worden gegenereerd kleiner is.
Valkuil - Vangen onderbroken uitzondering
Zoals al in andere valkuilen is aangegeven, alle uitzonderingen opvangen met behulp van
try {
// Some code
} catch (Exception) {
// Some error handling
}
Komt met veel verschillende problemen. Maar een specifiek probleem is dat het kan leiden tot deadlocks omdat het het interrupt-systeem breekt bij het schrijven van multi-threaded applicaties.
Als u een thread start, moet u deze meestal ook om verschillende redenen abrupt kunnen stoppen.
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
De t.interrupt()
zal een InterruptedException in die thread genereren, dan is bedoeld om de thread af te sluiten. Maar wat als de thread een aantal bronnen moet opruimen voordat deze volledig is gestopt? Hiervoor kan het de InterruptedException vangen en wat opruimen.
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();
}
}
}
Maar als u een allesomvattende uitdrukking in uw code heeft, wordt de InterruptedException er ook door gevangen en zal de onderbreking niet doorgaan. Wat in dit geval kan leiden tot een impasse, omdat de bovenliggende thread voor onbepaalde tijd wacht totdat dit einde stopt met 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();
}
}
}
Het is dus beter om uitzonderingen afzonderlijk te vangen, maar als u erop staat om een catch-all te gebruiken, vang de InterruptedException dan minstens vooraf afzonderlijk op.
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();
}
}
}
Valkuil - Uitzonderingen gebruiken voor normale flowcontrol
Er is een mantra die sommige Java-experts niet zullen reciteren:
"Uitzonderingen mogen alleen worden gebruikt voor uitzonderlijke gevallen."
(Bijvoorbeeld: http://programmers.stackexchange.com/questions/184654 )
De essentie hiervan is dat het een slecht idee is (in Java) om uitzonderingen en uitzonderingsafhandeling te gebruiken om een normale stroomregeling te implementeren. Vergelijk bijvoorbeeld deze twee manieren om met een parameter om te gaan die nul kan zijn.
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 dit voorbeeld behandelen we (per ontwerp) het geval waarin een word
null
, alsof het een leeg woord is. De twee versies behandelen null
hetzij met behulp van conventionele als ... anders en of probeer ... vangen . Hoe moeten we beslissen welke versie beter is?
Het eerste criterium is leesbaarheid. Hoewel leesbaarheid moeilijk objectief te kwantificeren is, zouden de meeste programmeurs het erover eens zijn dat de essentiële betekenis van de eerste versie gemakkelijker te onderscheiden is. Om de tweede vorm echt te begrijpen, moet je NullPointerException
begrijpen dat een NullPointerException
niet kan worden weggegooid door de methoden Math.min
of String.substring
.
Het tweede criterium is efficiëntie. In releases van Java voorafgaand aan Java 8 is de tweede versie aanzienlijk (orden van grootte) langzamer dan de eerste versie. In het bijzonder omvat de constructie van een uitzonderingsobject het vastleggen en opnemen van de stapelframes, voor het geval dat de stacktrace vereist is.
Aan de andere kant zijn er veel situaties waarin het gebruik van uitzonderingen leesbaarder, efficiënter en (soms) correcter is dan het gebruik van voorwaardelijke code om met "uitzonderlijke" gebeurtenissen om te gaan. Inderdaad, er zijn zeldzame situaties waarin het nodig is om ze te gebruiken voor "niet-uitzonderlijke" evenementen; dat wil zeggen gebeurtenissen die relatief vaak voorkomen. Voor dit laatste is het de moeite waard om te kijken naar manieren om de overheadkosten voor het maken van uitzonderingsobjecten te verminderen.
Valkuil - Overmatige of ongepaste stacktraces
Een van de meer vervelende dingen die programmeurs kunnen doen, is om oproepen naar printStackTrace()
door hun code te verspreiden.
Het probleem is dat de printStackTrace()
de stacktrace naar standaarduitvoer gaat schrijven.
Voor een toepassing die bedoeld is voor eindgebruikers die geen Java-programmeur zijn, is een stacktrace in het beste geval niet informatief en in het slechtste geval alarmerend.
Voor een server-side applicatie is de kans groot dat niemand naar de standaarduitvoer zal kijken.
Een beter idee is om printStackTrace
rechtstreeks aan te roepen, of als u het wel doet, op een manier dat de stacktracering naar een logbestand of een printStackTrace
wordt geschreven in plaats van naar de console van de eindgebruiker.
Een manier om dit te doen is om een logboekraamwerk te gebruiken en het uitzonderingsobject door te geven als parameter van de logboekgebeurtenis. Zelfs het vastleggen van de uitzondering kan echter schadelijk zijn als het onoordeelkundig wordt gedaan. Stel je de volgende situatie voor:
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;
}
}
Als de uitzondering wordt weergegeven in method2
, ziet u waarschijnlijk twee exemplaren van dezelfde stacktrace in het logbestand, die overeenkomen met dezelfde fout.
Kortom, log de uitzondering in of gooi hem opnieuw door (mogelijk omwikkeld met een andere uitzondering). Doe niet beide.
Valkuil - Direct subklasse van `Throwable`
Throwable
heeft twee directe subklassen, Exception
en Error
. Hoewel het mogelijk is om een nieuwe klasse te maken die Throwable
rechtstreeks uitbreidt, is dit niet aan te raden, omdat veel toepassingen ervan uitgaan dat alleen Exception
en Error
bestaan.
Meer ter Throwable
is er geen praktisch voordeel van directe subclassificatie Throwable
, omdat de resulterende klasse in feite gewoon een gecontroleerde uitzondering is. Subklasse Exception
zal in plaats daarvan resulteren in hetzelfde gedrag, maar zal uw intentie duidelijker overbrengen.