Jeg tror nok, du har oplevet en situation, hvor du kører kode og ender med noget som en NullPointerException , ClassCastException , eller endnu værre... Dette efterfølges af en lang proces med fejlretning, analyse, google og så videre. Undtagelser er vidunderlige som de er: de angiver problemets art og hvor det opstod. Hvis du vil genopfriske din hukommelse og lære lidt mere, så tag et kig på denne artikel: Undtagelser: markeret, ikke markeret og tilpasset .

Når det er sagt, kan der være situationer, hvor du skal oprette din egen undtagelse. Antag for eksempel, at din kode skal anmode om oplysninger fra en fjerntjeneste, som af en eller anden grund ikke er tilgængelig. Eller antag, at nogen udfylder en ansøgning om et bankkort og giver et telefonnummer, der, uanset om det er ved et uheld eller ej, allerede er knyttet til en anden bruger i systemet.

Den korrekte adfærd her afhænger naturligvis stadig af kundens krav og systemets arkitektur, men lad os antage, at du har fået til opgave at tjekke, om telefonnummeret allerede er i brug og smide en undtagelse, hvis det er.

Lad os oprette en undtagelse:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Dernæst bruger vi det, når vi udfører vores kontrol:


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

For at forenkle vores eksempel vil vi bruge flere hårdkodede telefonnumre til at repræsentere en database. Og endelig, lad os prøve at bruge vores undtagelse:


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

Og nu er det tid til at trykke Shift+F10 (hvis du bruger IDEA), altså køre projektet. Dette er, hvad du vil se i konsollen:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Det angivne telefonnummer er allerede i brug af en anden kunde!
at exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Se på dig! Du oprettede din egen undtagelse og testede den endda lidt. Tillykke med denne præstation! Jeg anbefaler at eksperimentere lidt med koden for bedre at forstå, hvordan den virker.

Tilføj endnu et flueben - kontroller for eksempel, om telefonnummeret indeholder bogstaver. Som du sikkert ved, bruges bogstaver ofte i USA for at gøre telefonnumre nemmere at huske, fx 1-800-MY-APPLE. Din kontrol kan sikre, at telefonnummeret kun indeholder numre.

Okay, så vi har oprettet en markeret undtagelse. Alt ville være fint og godt, men...

Programmeringsfællesskabet er opdelt i to lejre - dem, der går ind for kontrollerede undtagelser, og dem, der er imod dem. Begge sider fremfører stærke argumenter. Begge inkluderer topudviklere: Bruce Eckel kritiserer kontrollerede undtagelser, mens James Gosling forsvarer dem. Det ser ud til, at denne sag aldrig vil blive afgjort permanent. Når det er sagt, lad os se på de største ulemper ved at bruge kontrollerede undtagelser.

Den største ulempe ved kontrollerede undtagelser er, at de skal håndteres. Og her har vi to muligheder: enten håndtere det på plads ved hjælp af en try-catch , eller, hvis vi bruger den samme undtagelse mange steder, brug kast til at kaste undtagelserne op og behandle dem i klasser på øverste niveau.

Desuden kan vi ende med "boilerplate"-kode, dvs. kode, der fylder meget, men som ikke udfører meget tunge løft.

Der dukker problemer op i ret store applikationer, hvor mange undtagelser bliver håndteret: listen over udsendelser på en metode på øverste niveau kan nemt vokse til at omfatte et dusin undtagelser.

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

Udviklere bryder sig normalt ikke om dette og vælger i stedet et trick: de får alle deres kontrollerede undtagelser til at arve en fælles forfader - ApplicationNameException . Nu skal de også fange den ( markeret !) undtagelse i en handler:


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

Her står vi over for et andet problem - hvad skal vi gøre i den sidste catch- blok? Ovenfor har vi allerede behandlet alle de forventede situationer, så på dette tidspunkt betyder ApplicationNameException intet mere for os end " Undtagelse : der opstod en uforståelig fejl". Sådan håndterer vi det:


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

Og i sidste ende ved vi ikke, hvad der skete.

Men kunne vi ikke kaste alle undtagelser på én gang, sådan her?


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

Ja, det kunne vi. Men hvad fortæller "throws Exception" os? At noget er gået i stykker. Du bliver nødt til at undersøge alt fra top til bund og hygge dig med debuggeren i lang tid for at forstå årsagen.

Du kan også støde på en konstruktion, der nogle gange kaldes "undtagelsesslukning":


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

Der er ikke meget at tilføje her som forklaring - koden gør alt klart, eller rettere sagt, det gør alt uklart.

Selvfølgelig kan du sige, at du ikke vil se dette i ægte kode. Nå, lad os kigge ind i tarmene (koden) af URL- klassen fra java.net -pakken. Følg mig, hvis du vil vide det!

Her er en af ​​konstruktionerne i URL- klassen:


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

Som du kan se, har vi en interessant kontrolleret undtagelse — MalformedURLException . Her er, hvornår det kan blive kastet (og jeg citerer):
"hvis ingen protokol er specificeret, eller en ukendt protokol er fundet, eller spec er null, eller den parsede URL ikke overholder den specifikke syntaks for den tilknyttede protokol."

Det er:

  1. Hvis der ikke er angivet nogen protokol.
  2. En ukendt protokol er fundet.
  3. Specifikationen er null .
  4. URL'en overholder ikke den specifikke syntaks for den tilknyttede protokol.

Lad os oprette en metode, der opretter et URL- objekt:


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

Så snart du skriver disse linjer i IDE'en (jeg koder i IDEA, men det virker selv i Eclipse og NetBeans), vil du se dette:

Det betyder, at vi enten skal smide en undtagelse eller pakke koden ind i en try-catch- blok. Indtil videre foreslår jeg, at du vælger den anden mulighed for at visualisere, hvad der sker:


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, er koden allerede ret omfattende. Og det hentydede vi til ovenfor. Dette er en af ​​de mest åbenlyse grunde til at bruge ukontrollerede undtagelser.

Vi kan oprette en umarkeret undtagelse ved at udvide RuntimeException i Java.

Ikke-markerede undtagelser arves fra klassen Error eller RuntimeException -klassen. Mange programmører føler, at disse undtagelser kan håndteres i vores programmer, fordi de repræsenterer fejl, som vi ikke kan forvente at komme tilbage fra, mens programmet kører.

Når der opstår en ukontrolleret undtagelse, skyldes det normalt, at koden bruges forkert, og at der sendes et argument ind, der er null eller på anden måde ugyldigt.

Nå, lad os skrive 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);
   }
}
    

Bemærk, at vi har lavet flere konstruktører til forskellige formål. Dette lader os give vores undtagelse flere muligheder. For eksempel kan vi gøre det sådan, at en undtagelse giver os en fejlkode. Til at begynde med, lad os lave en opregning for at repræsentere vores fejlkoder:


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

Lad os nu tilføje endnu en konstruktør til vores undtagelsesklasse:


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

Og lad os ikke glemme at tilføje et felt (vi glemte næsten):


private Integer errorCode;
    

Og selvfølgelig en metode til at få denne kode:


public Integer getErrorCode() {
   return errorCode;
}
    

Lad os se på hele klassen, så vi kan tjekke den og sammenligne:

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! Vores undtagelse er overstået! Som du kan se, er der ikke noget særligt kompliceret her. Lad os tjekke det ud i aktion:


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

Når vi kører vores lille applikation, vil vi se noget i stil med følgende i konsollen:

Lad os nu drage fordel af den ekstra funktionalitet, vi har tilføjet. Vi tilføjer lidt til den forrige kode:


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 arbejde med undtagelser på samme måde, som du arbejder med objekter. Selvfølgelig er jeg sikker på, at du allerede ved, at alt i Java er et objekt.

Og se hvad vi gjorde. Først ændrede vi metoden, som nu ikke kaster, men i stedet blot opretter en undtagelse, afhængigt af input-parameteren. Dernæst genererer vi ved hjælp af en switch-case -sætning en undtagelse med den ønskede fejlkode og meddelelse. Og i hovedmetoden får vi den oprettede undtagelse, får fejlkoden og smed den.

Lad os køre dette og se, hvad vi får på konsollen:

Se - vi udskrev fejlkoden, som vi fik fra undtagelsen, og smed derefter selve undtagelsen. Hvad mere er, kan vi endda spore præcis, hvor undtagelsen blev kastet. Efter behov kan du tilføje alle relevante oplysninger til meddelelsen, oprette yderligere fejlkoder og tilføje nye funktioner til dine undtagelser.

Nå, hvad synes du om det? Jeg håber alt lykkedes for dig!

Generelt er undtagelser et ret omfattende emne og ikke entydigt. Der vil være mange flere stridigheder om det. For eksempel er det kun Java, der har kontrolleret undtagelser. Blandt de mest populære sprog har jeg ikke set et, der bruger dem.

Bruce Eckel skrev meget godt om undtagelser i kapitel 12 i sin bog "Thinking in Java" — jeg anbefaler, at du læser den! Tag også et kig på det første bind af Horstmanns "Core Java" — Det har også en masse interessante ting i kapitel 7.

En lille opsummering

  1. Skriv alt til en log! Log meddelelser i smidte undtagelser. Dette vil normalt hjælpe meget ved fejlfinding og vil give dig mulighed for at forstå, hvad der skete. Lad ikke en catch- blok stå tom, ellers vil den bare "sluge" undtagelsen, og du vil ikke have nogen information, der hjælper dig med at jage problemer.

  2. Når det kommer til undtagelser, er det dårlig praksis at fange dem alle på én gang (som en kollega af mig sagde, "det er ikke Pokemon, det er Java"), så undgå catch (undtagelse e) eller værre, catch ( Throwable t ) .

  3. Kast undtagelser så tidligt som muligt. Dette er god Java-programmeringspraksis. Når du studerer rammer som Spring, vil du se, at de følger "fail fast"-princippet. Det vil sige, at de "fejler" så tidligt som muligt for at gøre det muligt hurtigt at finde fejlen. Dette medfører naturligvis visse gener. Men denne tilgang hjælper med at skabe mere robust kode.

  4. Når du kalder andre dele af koden, er det bedst at fange visse undtagelser. Hvis den kaldte kode giver flere undtagelser, er det dårlig programmeringspraksis kun at fange forældreklassen for disse undtagelser. Lad os for eksempel sige, at du kalder kode, der kaster FileNotFoundException og IOException . I din kode, der kalder dette modul, er det bedre at skrive to catch-blokke for at fange hver af undtagelserne, i stedet for en enkelt catch to catch Exception .

  5. Fang kun undtagelser, når du kan håndtere dem effektivt for brugere og til fejlretning.

  6. Tøv ikke med at skrive dine egne undtagelser. Java har selvfølgelig en masse færdige, noget til enhver lejlighed, men nogle gange skal man alligevel opfinde sit eget "hjul". Men du bør klart forstå, hvorfor du gør dette, og være sikker på, at standardsættet af undtagelser ikke allerede har det, du har brug for.

  7. Når du opretter dine egne undtagelsesklasser, skal du være forsigtig med navngivningen! Du ved sikkert allerede, at det er ekstremt vigtigt at navngive klasser, variabler, metoder og pakker korrekt. Undtagelser er ingen undtagelse! :) Slut altid med ordet Undtagelse , og navnet på undtagelsen skal tydeligt formidle den type fejl, den repræsenterer. For eksempel FileNotFoundException .

  8. Dokumenter dine undtagelser. Vi anbefaler at skrive et @throws Javadoc-tag for undtagelser. Dette vil især være nyttigt, når din kode giver grænseflader af enhver art. Og du vil også finde det lettere at forstå din egen kode senere. Hvad synes du, hvordan kan du bestemme, hvad MalformedURLException handler om? Fra Javadoc! Ja, tanken om at skrive dokumentation er ikke særlig tiltalende, men tro mig, du vil takke dig selv, når du seks måneder senere vender tilbage til din egen kode.

  9. Frigiv ressourcer og forsøm ikke prøv-med-ressourcer -konstruktionen.

  10. Her er den overordnede oversigt: brug undtagelser med omtanke. At smide en undtagelse er en temmelig "dyr" operation i form af ressourcer. I mange tilfælde kan det være lettere at undgå at smide undtagelser og i stedet returnere f.eks. en boolsk variabel, som om operationen lykkedes, ved at bruge en simpel og "billigere" if-else .

    Det kan også være fristende at binde applikationslogik til undtagelser, hvilket du tydeligvis ikke bør gøre. Som vi sagde i begyndelsen af ​​artiklen, er undtagelser for ekstraordinære situationer, ikke forventede, og der er forskellige værktøjer til at forhindre dem. Især er der Valgfrit at forhindre en NullPointerException , eller Scanner.hasNext og lignende for at forhindre en IOException , som read()- metoden kan kaste.