Är kontrollerade undantag bra eller dåliga?

Java stöder kontrollerade undantag. Denna kontroversiella språkfunktion älskas av vissa och hatas av andra, till den punkt där de flesta programmeringsspråk undviker kontrollerade undantag och endast stöder deras okontrollerade motsvarigheter.

I det här inlägget undersöker jag kontroversen kring kontrollerade undantag. Jag introducerar först undantagskonceptet och beskriver kort Java-språkstöd för undantag för att hjälpa nybörjare att bättre förstå kontroversen.

Vad är undantag?

I en idealisk värld skulle datorprogram aldrig stöta på några problem: filer skulle existera när de ska existera, nätverksanslutningar skulle aldrig stängas oväntat, det skulle aldrig vara ett försök att åberopa en metod via nullreferensen, heltal-division -nollförsök skulle inte inträffa, och så vidare. Men vår värld är långt ifrån idealisk; dessa och andra undantag från perfekt programkörning är utbredda.

Tidiga försök att känna igen undantag inkluderade att returnera specialvärden som indikerar misslyckande. Till exempel fopen()returnerar C-språkets funktion NULLnär den inte kan öppna en fil. PHP: s mysql_query()funktion återgår också FALSEnär ett SQL-fel inträffar. Du måste leta någon annanstans efter den faktiska felkoden. Även om det är enkelt att implementera, finns det två problem med denna "return special value" -metod för att känna igen undantag:

  • Särskilda värden beskriver inte undantaget. Vad betyder NULLeller FALSEegentligen betyder? Allt beror på författaren av funktionaliteten som returnerar specialvärdet. Dessutom, hur relaterar du ett speciellt värde till programmets sammanhang när undantaget inträffade så att du kan presentera ett meningsfullt meddelande till användaren?
  • Det är för lätt att ignorera ett speciellt värde. Till exempel int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);är det problematiskt eftersom detta C-kodfragment körs för fgetc()att läsa ett tecken från filen även när det fopen()returneras NULL. I det här fallet fgetc()kommer det inte att lyckas: vi har ett fel som kan vara svårt att hitta.

Det första problemet löses genom att använda klasser för att beskriva undantag. Ett klassnamn identifierar typen av undantag och dess fält sammanställer lämpligt programkontext för att bestämma (via metodsamtal) vad som gick fel. Det andra problemet löses genom att kompilatorn tvingar programmeraren att antingen svara på ett undantag direkt eller indikera att undantaget ska hanteras någon annanstans.

Vissa undantag är mycket allvarliga. Till exempel kan ett program försöka tilldela lite minne när inget ledigt minne är tillgängligt. Gränslös rekursion som uttömmer stacken är ett annat exempel. Sådana undantag kallas fel .

Undantag och Java

Java använder klasser för att beskriva undantag och fel. Dessa klasser är organiserade i en hierarki som är rotad i java.lang.Throwableklassen. (Anledningen till att Throwableman valde att namnge denna specialklass kommer att framgå inom kort.) Direkt nedan Throwablefinns java.lang.Exceptionrespektive java.lang.Errorklasser som beskriver undantag respektive fel.

Till exempel innehåller Java-biblioteket java.net.URISyntaxException, som utökar Exceptionoch indikerar att en sträng inte kunde analyseras som en enhetlig resursidentifierarreferens. Observera att det URISyntaxExceptionföljer en namngivning där ett undantagsklassnamn slutar med ordet Exception. En liknande konvention gäller för felklassnamn, till exempel java.lang.OutOfMemoryError.

Exceptionär underklassad av java.lang.RuntimeException, vilket är superklassen för de undantag som kan kastas under den normala driften av Java Virtual Machine (JVM). Till exempel java.lang.ArithmeticExceptionbeskriver aritmetiska misslyckanden såsom försök att dela heltal med heltal 0. Beskriver också java.lang.NullPointerExceptionförsök att komma åt objektmedlemmar via nollreferensen.

Ett annat sätt att titta på RuntimeException

Avsnitt 11.1.1 i Java 8 Language Specification anger: RuntimeExceptionär superklassen för alla undantag som kan kastas av många anledningar under utvärdering av uttryck, men från vilka återhämtning fortfarande kan vara möjlig.

När ett undantag eller fel uppstår skapas ett objekt från lämplig Exceptioneller Errorunderklass och skickas till JVM. Handlingen att passera objektet är känd som att kasta undantaget . Java tillhandahåller throwuttalandet för detta ändamål. Skapar till exempel throw new IOException("unable to read file");ett nytt java.io.IOExceptionobjekt som har initierats till den angivna texten. Detta objekt kastas därefter till JVM.

Java tillhandahåller tryuttalandet för avgränsning av kod från vilket ett undantag kan kastas. Detta uttalande består av nyckelord tryföljt av ett avstängningsblock. Följande kodfragment visar tryoch throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

I detta kodfragment kommer exekveringen in i tryblocket och åberopar method(), vilket kastar en instans av NullPointerException.

JVM tar emot kastbar och söker upp metod-samtalsstacken för en hanterare för att hantera undantaget. Undantag som inte härrör från RuntimeExceptionhanteras ofta; runtimeundantag och fel hanteras sällan.

Varför fel hanteras sällan

Fel hanteras sällan eftersom det ofta inte finns något som ett Java-program kan göra för att återhämta sig från felet. Till exempel, när ledigt minne är slut kan ett program inte tilldela ytterligare minne. Men om allokeringsfelet beror på att hålla kvar mycket minne som ska frigöras, kan en hander försöka frigöra minnet med hjälp av JVM. Även om en hanterare kan tyckas vara användbar i detta felkontext, kanske försöket inte lyckas.

En hanterare beskrivs av ett catchblock som följer tryblocket. I catchblocket ger en rubrik som visar vilken typ av undantag som det är beredd att hantera. Om kastbar typ ingår i listan skickas kastbar till catchblocket vars kod körs. Koden svarar på orsaken till misslyckande på ett sådant sätt att programmet fortsätter eller eventuellt avslutas:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

I det här kodfragmentet har jag lagt till ett catchblock i tryblocket. När NullPointerExceptionobjektet kastas från method()lokaliserar JVM och skickar exekvering till catchblocket, vilket matar ut ett meddelande.

Slutligen blockerar

Ett tryblock eller dess sista catchblock kan följas av ett finallyblock som används för att utföra saneringsuppgifter, till exempel att släppa förvärvade resurser. Jag har inget mer att säga om finallyeftersom det inte är relevant för diskussionen.

Undantag som beskrivs av Exceptionoch dess underklasser utom RuntimeExceptionoch dess underklasser kallas kontrollerade undantag . För varje throwuttalande undersöker kompilatorn undantagsobjektets typ. Om typen anger markerad kontrollerar kompilatorn källkoden för att säkerställa att undantaget hanteras i metoden där den kastas eller deklareras hanteras längre upp i metod-samtalsstacken. Alla andra undantag kallas okontrollerade undantag .

Java låter dig förklara att ett markerat undantag hanteras längre upp i metod-samtalsstacken genom att lägga till en throwsklausul (nyckelord throwsföljt av en kommaavgränsad lista över kontrollerade undantagsklassnamn) till en metodrubrik:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Eftersom det IOExceptionär en markerad undantagstyp måste kastade instanser av detta undantag hanteras i metoden där de kastas eller deklareras hanteras längre upp i metod-samtalsstacken genom att lägga till en throwsklausul i varje berörda metodens rubrik. I det här fallet throws IOExceptionläggs en klausul till method()rubriken. Det kastade IOExceptionobjektet skickas till JVM, som lokaliserar och överför körning till catchhanteraren.

Argumenterar för och emot kontrollerade undantag

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

Dessutom har jag stött på argumentet om att applikationer måste hantera ett stort antal kontrollerade undantag som genereras från de flera bibliotek som de använder. Detta problem kan dock övervinnas genom en smart utformad fasad som utnyttjar Java: s kedjade undantagsfacilitet och undantagskastning för att kraftigt minska antalet undantag som måste hanteras samtidigt som det ursprungliga undantaget som kastades bevaras.

Slutsats

Är markerade undantag bra eller är de dåliga? Med andra ord, bör programmerare tvingas hantera kontrollerade undantag eller ges möjlighet att ignorera dem? Jag gillar tanken på att genomdriva mer robust programvara. Men jag tror också att Java: s mekanism för undantagshantering måste utvecklas för att göra det mer programmerarvänligt. Här är några sätt att förbättra denna mekanism: