Jag tror att du förmodligen har upplevt en situation där du kör kod och slutar med något som en NullPointerException , ClassCastException , eller ännu värre... Detta följs av en lång process av felsökning, analys, googling och så vidare. Undantag är underbara som de är: de indikerar problemets natur och var det uppstod. Om du vill fräscha upp ditt minne och lära dig lite mer, ta en titt på den här artikeln: Undantag: markerad, omarkerad och anpassad .

Som sagt, det kan finnas situationer när du behöver skapa ett eget undantag. Anta till exempel att din kod behöver begära information från en fjärrtjänst som av någon anledning inte är tillgänglig. Eller anta att någon fyller i en ansökan om ett bankkort och tillhandahåller ett telefonnummer som, vare sig av misstag eller inte, redan är kopplat till en annan användare i systemet.

Naturligtvis beror det korrekta beteendet här fortfarande på kundens krav och systemets arkitektur, men låt oss anta att du har fått i uppdrag att kontrollera om telefonnumret redan används och kasta ett undantag om det är det.

Låt oss skapa ett undantag:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

Därefter använder vi det när vi utför vår kontroll:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

För att förenkla vårt exempel kommer vi att använda flera hårdkodade telefonnummer för att representera en databas. Och slutligen, låt oss försöka använda vårt undantag:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

Och nu är det dags att trycka på Shift+F10 (om du använder IDEA), dvs köra projektet. Det här är vad du kommer att se i konsolen:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Det angivna telefonnumret används redan av en annan kund!
vid undantag.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Se på dig! Du skapade ditt eget undantag och till och med testade det lite. Grattis till denna prestation! Jag rekommenderar att du experimenterar lite med koden för att bättre förstå hur den fungerar.

Lägg till ytterligare en kontroll — kontrollera till exempel om telefonnumret innehåller bokstäver. Som du säkert vet används bokstäver ofta i USA för att göra telefonnummer lättare att komma ihåg, t.ex. 1-800-MY-APPLE. Din kontroll kan säkerställa att telefonnumret endast innehåller nummer.

Okej, så vi har skapat ett markerat undantag. Allt skulle vara bra och bra, men...

Programmeringsgemenskapen är uppdelad i två läger - de som är för kontrollerade undantag och de som motsätter sig dem. Båda sidor för starka argument. Båda inkluderar förstklassiga utvecklare: Bruce Eckel kritiserar kontrollerade undantag, medan James Gosling försvarar dem. Det ser ut som att den här frågan aldrig kommer att lösas permanent. Som sagt, låt oss titta på de största nackdelarna med att använda markerade undantag.

Den största nackdelen med kontrollerade undantag är att de måste hanteras. Och här har vi två alternativ: antingen hantera det på plats med en try-catch , eller, om vi använder samma undantag på många ställen, använd kast för att kasta upp undantagen och bearbeta dem i klasser på högsta nivå.

Dessutom kan vi sluta med "boilerplate"-kod, dvs kod som tar mycket plats, men som inte gör så mycket tunga lyft.

Problem uppstår i ganska stora applikationer med många undantag som hanteras: listan över kast på en toppnivåmetod kan lätt växa till att omfatta ett dussin undantag.

public OurCoolClass() kastar FirstException, SecondException, ThirdException, ApplicationNameException...

Utvecklare brukar inte gilla detta och väljer istället ett trick: de får alla sina markerade undantag att ärva en gemensam förfader – ApplicationNameException . Nu måste de också fånga det ( markerade !) undantaget i en hanterare:


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

Här står vi inför ett annat problem - vad ska vi göra i det sista fångstblocket ? Ovan har vi redan bearbetat alla förväntade situationer, så vid det här laget betyder ApplicationNameException inget mer för oss än " Undantag : något obegripligt fel inträffade". Så här hanterar vi det:


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

Och i slutändan vet vi inte vad som hände.

Men kunde vi inte kasta alla undantag på en gång, så här?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Ja, det kunde vi. Men vad säger "kastar Undantag" oss? Att något är trasigt. Du måste undersöka allt från topp till botten och bli mysig med debuggern under en lång tid för att förstå orsaken.

Du kan också stöta på en konstruktion som ibland kallas "undantagssväljning":


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

Det finns inte mycket att tillägga här som förklaring - koden gör allt klart, eller snarare, det gör allt oklart.

Naturligtvis kan du säga att du inte kommer att se detta i riktig kod. Nåväl, låt oss titta in i tarmarna (koden) för URL -klassen från java.net -paketet. Följ mig om du vill veta!

Här är en av konstruktionerna i URL- klassen:


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Som du kan se har vi ett intressant kontrollerat undantag — MalformedURLException . Här är när det kan kastas (och jag citerar):
"om inget protokoll är specificerat, eller ett okänt protokoll hittas, eller spec är null, eller om den analyserade URL:en inte överensstämmer med den specifika syntaxen för det associerade protokollet."

Det är:

  1. Om inget protokoll anges.
  2. Ett okänt protokoll har hittats.
  3. Specifikationen är null .
  4. URL:en överensstämmer inte med den specifika syntaxen för det associerade protokollet.

Låt oss skapa en metod som skapar ett URL- objekt:


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

Så fort du skriver dessa rader i IDE (jag kodar i IDEA, men det här fungerar även i Eclipse och NetBeans), kommer du att se detta:

Det betyder att vi antingen måste kasta ett undantag eller linda in koden i ett try-catch- block. För nu föreslår jag att du väljer det andra alternativet för att visualisera vad som händer:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

Som du kan se är koden redan ganska utförlig. Och vi anspelade på det ovan. Detta är en av de mest uppenbara anledningarna till att använda okontrollerade undantag.

Vi kan skapa ett omarkerat undantag genom att utöka RuntimeException i Java.

Omarkerade undantag ärvs från klassen Error eller klassen RuntimeException . Många programmerare anser att dessa undantag kan hanteras i våra program eftersom de representerar fel som vi inte kan förvänta oss att återställa från medan programmet körs.

När ett okontrollerat undantag inträffar orsakas det vanligtvis av att koden används felaktigt, att ett argument som är null eller på annat sätt ogiltigt skickas in.

Nåväl, låt oss skriva koden:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Observera att vi har gjort flera konstruktörer för olika ändamål. Detta låter oss ge vårt undantag fler möjligheter. Vi kan till exempel göra det så att ett undantag ger oss en felkod. Till att börja med, låt oss göra en uppräkning för att representera våra felkoder:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

Låt oss nu lägga till ytterligare en konstruktör till vår undantagsklass:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

Och låt oss inte glömma att lägga till ett fält (vi glömde nästan):


private Integer errorCode;
    

Och naturligtvis en metod för att få den här koden:


public Integer getErrorCode() {
   return errorCode;
}
    

Låt oss titta på hela klassen så att vi kan kontrollera den och jämföra:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Ta-da! Vårt undantag är klart! Som du kan se är det inget särskilt komplicerat här. Låt oss kolla in det i aktion:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

När vi kör vår lilla applikation ser vi något i stil med följande i konsolen:

Låt oss nu dra nytta av den extra funktionalitet vi har lagt till. Vi lägger till lite till föregående kod:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Du kan arbeta med undantag på samma sätt som du arbetar med objekt. Naturligtvis är jag säker på att du redan vet att allt i Java är ett objekt.

Och titta på vad vi gjorde. Först ändrade vi metoden, som nu inte kastar, utan istället skapar ett undantag, beroende på ingångsparametern. Därefter, med hjälp av en switch-case -sats, genererar vi ett undantag med önskad felkod och meddelande. Och i huvudmetoden får vi det skapade undantaget, får felkoden och kastade den.

Låt oss köra detta och se vad vi får på konsolen:

Titta — vi skrev ut felkoden som vi fick från undantaget och kastade sedan själva undantaget. Dessutom kan vi till och med spåra exakt var undantaget kastades. Vid behov kan du lägga till all relevant information i meddelandet, skapa ytterligare felkoder och lägga till nya funktioner till dina undantag.

Tja, vad tycker du om det? Jag hoppas att allt löste sig för dig!

I allmänhet är undantag ett ganska omfattande ämne och inte entydigt. Det kommer att bli många fler tvister om det. Till exempel är det bara Java som har markerat undantag. Bland de mest populära språken har jag inte sett något som använder dem.

Bruce Eckel skrev mycket bra om undantag i kapitel 12 i sin bok "Thinking in Java" — jag rekommenderar att du läser den! Ta också en titt på den första volymen av Horstmanns "Core Java" — Den har också en hel del intressanta saker i kapitel 7.

En liten sammanfattning

  1. Skriv allt till en logg! Logga meddelanden i kastade undantag. Detta kommer vanligtvis att hjälpa mycket vid felsökning och gör att du kan förstå vad som hände. Lämna inte ett fångstblock tomt, annars kommer det bara att "svälja" undantaget och du kommer inte ha någon information som hjälper dig att hitta problem.

  2. När det kommer till undantag är det dålig praxis att fånga dem alla på en gång (som en kollega till mig sa, "det är inte Pokemon, det är Java"), så undvik catch (Undantag e) eller ännu värre, catch ( Throwable t ) .

  3. Kasta undantag så tidigt som möjligt. Detta är bra Java-programmering. När du studerar ramverk som Spring kommer du att se att de följer principen "fail fast". Det vill säga att de "misslyckas" så tidigt som möjligt för att göra det möjligt att snabbt hitta felet. Naturligtvis medför detta vissa olägenheter. Men detta tillvägagångssätt hjälper till att skapa mer robust kod.

  4. När du anropar andra delar av koden är det bäst att fånga vissa undantag. Om den anropade koden ger flera undantag, är det dålig programmeringspraxis att bara fånga den överordnade klassen för dessa undantag. Säg till exempel att du anropar kod som kastar FileNotFoundException och IOException . I din kod som anropar den här modulen är det bättre att skriva två catch-block för att fånga upp vart och ett av undantagen, istället för en enda catch to catch Exception .

  5. Fånga undantag endast när du kan hantera dem effektivt för användare och för felsökning.

  6. Tveka inte att skriva dina egna undantag. Java har förstås en hel del färdiga, något för alla tillfällen, men ibland behöver man ändå uppfinna sitt eget "hjul". Men du bör tydligt förstå varför du gör detta och vara säker på att standarduppsättningen av undantag inte redan har vad du behöver.

  7. När du skapar dina egna undantagsklasser, var försiktig med namngivningen! Du vet förmodligen redan att det är extremt viktigt att korrekt namnge klasser, variabler, metoder och paket. Undantag är inget undantag! :) Avsluta alltid med ordet Undantag , och namnet på undantaget bör tydligt förmedla vilken typ av fel det representerar. Till exempel FileNotFoundException .

  8. Dokumentera dina undantag. Vi rekommenderar att du skriver en @throws Javadoc-tagg för undantag. Detta kommer att vara särskilt användbart när din kod tillhandahåller gränssnitt av något slag. Och du kommer också att ha lättare att förstå din egen kod senare. Vad tycker du, hur kan du avgöra vad MalformedURLException handlar om? Från Javadoc! Ja, tanken på att skriva dokumentation är inte särskilt tilltalande, men tro mig, du kommer att tacka dig själv när du återvänder till din egen kod ett halvår senare.

  9. Släpp resurser och försumma inte prova-med-resurser- konstruktionen.

  10. Här är den övergripande sammanfattningen: använd undantag klokt. Att kasta ett undantag är en ganska "dyr" operation resursmässigt. I många fall kan det vara lättare att undvika att kasta undantag och istället returnera, säg, en boolesk variabel som om operationen lyckades, med en enkel och "billigare" if-else .

    Det kan också vara frestande att knyta applikationslogik till undantag, vilket du uppenbarligen inte bör göra. Som vi sa i början av artikeln är undantag för exceptionella situationer, inte förväntade, och det finns olika verktyg för att förhindra dem. I synnerhet finns det Valfritt att förhindra en NullPointerException , eller Scanner.hasNext och liknande för att förhindra en IOException , som metoden read() kan kasta.