Jeg tror du sannsynligvis har opplevd en situasjon der du kjører kode og ender opp med noe sånt som en NullPointerException , ClassCastException , eller enda verre... Dette etterfølges av en lang prosess med feilsøking, analysering, googling og så videre. Unntak er fantastiske som de er: de indikerer problemets natur og hvor det oppstod. Hvis du vil friske opp hukommelsen og lære litt mer, ta en titt på denne artikkelen: Unntak: avmerket, umerket og tilpasset .

Når det er sagt, kan det være situasjoner når du trenger å lage ditt eget unntak. Anta for eksempel at koden din trenger å be om informasjon fra en ekstern tjeneste som av en eller annen grunn ikke er tilgjengelig. Eller anta at noen fyller ut en søknad om bankkort og oppgir et telefonnummer som, enten ved et uhell eller ikke, allerede er knyttet til en annen bruker i systemet.

Selvfølgelig avhenger riktig oppførsel her fortsatt av kundens krav og arkitekturen til systemet, men la oss anta at du har fått i oppgave å sjekke om telefonnummeret allerede er i bruk og kaste et unntak hvis det er det.

La oss lage et unntak:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Deretter bruker vi den når vi utfører vår sjekk:


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 å forenkle eksemplet vårt, bruker vi flere hardkodede telefonnumre for å representere en database. Og til slutt, la oss prøve å bruke unntaket vårt:


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 nå er det på tide å trykke Shift+F10 (hvis du bruker IDEA), altså kjøre prosjektet. Dette er hva du vil se i konsollen:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Det angitte telefonnummeret er allerede i bruk av en annen kunde!
at exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Se på deg! Du har laget ditt eget unntak og til og med testet det litt. Gratulerer med denne prestasjonen! Jeg anbefaler å eksperimentere litt med koden for bedre å forstå hvordan den fungerer.

Legg til en annen kontroll – for eksempel kontroller om telefonnummeret inneholder bokstaver. Som du sikkert vet, brukes bokstaver ofte i USA for å gjøre telefonnumre lettere å huske, f.eks. 1-800-MY-APPLE. Sjekken din kan sikre at telefonnummeret bare inneholder numre.

Ok, så vi har opprettet et avkrysset unntak. Alt ville vært bra, men...

Programmeringsmiljøet er delt inn i to leire - de som er for sjekkede unntak og de som er imot dem. Begge sider kommer med sterke argumenter. Begge inkluderer topputviklere: Bruce Eckel kritiserer sjekkede unntak, mens James Gosling forsvarer dem. Det ser ut til at denne saken aldri vil bli avgjort permanent. Når det er sagt, la oss se på de største ulempene ved å bruke sjekkede unntak.

Den største ulempen med sjekkede unntak er at de må håndteres. Og her har vi to alternativer: enten håndtere det på plass ved hjelp av en try-catch , eller, hvis vi bruker det samme unntaket mange steder, bruke kast for å kaste opp unntakene, og behandle dem i klasser på toppnivå.

Dessuten kan vi ende opp med "boilerplate"-kode, dvs. kode som tar mye plass, men som ikke gjør mye tungt.

Problemer dukker opp i ganske store applikasjoner med mange unntak som håndteres: kastelisten på en metode på toppnivå kan lett vokse til å inkludere et dusin unntak.

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

Utviklere liker vanligvis ikke dette og velger i stedet et triks: de får alle de sjekkede unntakene til å arve en felles stamfar – ApplicationNameException . Nå må de også fange det ( avmerket !) unntaket i en handler:


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

Her står vi overfor et annet problem - hva skal vi gjøre i den siste fangstblokken ? Ovenfor har vi allerede behandlet alle de forventede situasjonene, så på dette tidspunktet betyr ApplicationNameException ingenting mer for oss enn " Unntak : det oppstod en uforståelig feil". Slik håndterer vi det:


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

Og til slutt vet vi ikke hva som skjedde.

Men kunne vi ikke kaste hvert unntak på en gang, slik?


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

Ja, det kunne vi. Men hva forteller "kaster Unntak" oss? At noe er ødelagt. Du må undersøke alt fra topp til bunn og bli koselig med debuggeren i lang tid for å forstå årsaken.

Du kan også støte på en konstruksjon som noen ganger kalles "unntakssvelging":


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

Det er ikke mye å legge til her som forklaring - koden gjør alt klart, eller rettere sagt, det gjør alt uklart.

Selvfølgelig kan du si at du ikke vil se dette i ekte kode. Vel, la oss kikke inn i tarmen (koden) til URL- klassen fra java.net- pakken. Følg meg hvis du vil vite det!

Her er en av konstruksjonene i URL- klassen:


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

Som du kan se, har vi et interessant sjekket unntak — MalformedURLException . Her er når det kan bli kastet (og jeg siterer):
"hvis ingen protokoll er spesifisert, eller en ukjent protokoll er funnet, eller spesifikasjonen er null, eller den analyserte URL-en ikke samsvarer med den spesifikke syntaksen til den tilknyttede protokollen."

Det er:

  1. Hvis ingen protokoll er spesifisert.
  2. En ukjent protokoll er funnet.
  3. Spesifikasjonen er null .
  4. URL-en samsvarer ikke med den spesifikke syntaksen til den tilknyttede protokollen.

La oss lage en metode som lager et URL- objekt:


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

Så snart du skriver disse linjene i IDE (jeg koder i IDEA, men dette fungerer selv i Eclipse og NetBeans), vil du se dette:

Dette betyr at vi enten må kaste et unntak, eller pakke inn koden i en try-catch- blokk. For nå foreslår jeg at du velger det andre alternativet for å visualisere hva som skjer:


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 ganske omfattende. Og vi hentydet til det ovenfor. Dette er en av de mest åpenbare grunnene til å bruke ukontrollerte unntak.

Vi kan opprette et ukontrollert unntak ved å utvide RuntimeException i Java.

Unntak som ikke er avmerket, arves fra klassen Error eller RuntimeException . Mange programmerere føler at disse unntakene kan håndteres i programmene våre fordi de representerer feil som vi ikke kan forvente å gjenopprette fra mens programmet kjører.

Når et ukontrollert unntak oppstår, er det vanligvis forårsaket av feil bruk av kode, ved å sende inn et argument som er null eller på annen måte ugyldig.

Vel, la oss 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);
   }
}
    

Merk at vi laget flere konstruktører for forskjellige formål. Dette lar oss gi unntaket vårt flere muligheter. For eksempel kan vi gjøre det slik at et unntak gir oss en feilkode. Til å begynne med, la oss lage en oppsummering for å representere feilkodene våre:


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

La oss nå legge til en annen konstruktør til unntaksklassen vår:


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

Og la oss ikke glemme å legge til et felt (vi glemte nesten):


private Integer errorCode;
    

Og selvfølgelig en metode for å få denne koden:


public Integer getErrorCode() {
   return errorCode;
}
    

La oss se på hele klassen slik at vi kan sjekke 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! Vårt unntak er gjort! Som du kan se, er det ikke noe spesielt komplisert her. La oss sjekke det ut i aksjon:


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

Når vi kjører den lille applikasjonen vår, ser vi noe sånt som følgende i konsollen:

La oss nå dra nytte av den ekstra funksjonaliteten vi har lagt til. Vi legger til litt til den forrige koden:


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 jobbe med unntak på samme måte som du jobber med objekter. Selvfølgelig er jeg sikker på at du allerede vet at alt i Java er et objekt.

Og se på hva vi gjorde. Først endret vi metoden, som nå ikke kaster, men i stedet bare oppretter et unntak, avhengig av inngangsparameteren. Deretter, ved å bruke en switch-case -setning, genererer vi et unntak med ønsket feilkode og melding. Og i hovedmetoden får vi det opprettede unntaket, får feilkoden og kastet den.

La oss kjøre dette og se hva vi får på konsollen:

Se - vi skrev ut feilkoden som vi fikk fra unntaket og kastet deretter selve unntaket. Dessuten kan vi til og med spore nøyaktig hvor unntaket ble kastet. Etter behov kan du legge til all relevant informasjon i meldingen, opprette flere feilkoder og legge til nye funksjoner i unntakene dine.

Vel, hva synes du om det? Jeg håper alt ordnet seg for deg!

Generelt er unntak et ganske omfattende tema og ikke entydig. Det vil bli mange flere tvister rundt det. For eksempel er det bare Java som har sjekket unntak. Blant de mest populære språkene har jeg ikke sett et som bruker dem.

Bruce Eckel skrev veldig bra om unntak i kapittel 12 i boken sin "Thinking in Java" — jeg anbefaler at du leser den! Ta også en titt på det første bindet av Horstmanns «Core Java» — Den har også mye interessant i kapittel 7.

En liten oppsummering

  1. Skriv alt til en logg! Loggmeldinger i kastede unntak. Dette vil vanligvis hjelpe mye ved feilsøking og vil tillate deg å forstå hva som skjedde. Ikke la en fangstblokk stå tom, ellers vil den bare "svelge" unntaket og du vil ikke ha noen informasjon som hjelper deg å finne problemer.

  2. Når det gjelder unntak, er det dårlig praksis å fange dem alle på en gang (som en kollega av meg sa, "det er ikke Pokemon, det er Java"), så unngå catch (Unntak e) eller enda verre, catch ( Throwable t ) .

  3. Kast unntak så tidlig som mulig. Dette er god Java-programmeringspraksis. Når du studerer rammer som Spring, vil du se at de følger «fail fast»-prinsippet. Det vil si at de "feiler" så tidlig som mulig for å gjøre det mulig å raskt finne feilen. Selvfølgelig medfører dette visse ulemper. Men denne tilnærmingen bidrar til å skape mer robust kode.

  4. Når du ringer andre deler av koden, er det best å fange opp enkelte unntak. Hvis den kalte koden gir flere unntak, er det dårlig programmeringspraksis å bare fange foreldreklassen til disse unntakene. For eksempel, si at du kaller kode som kaster FileNotFoundException og IOException . I koden din som kaller denne modulen, er det bedre å skrive to catch-blokker for å fange opp hvert av unntakene, i stedet for en enkelt catch to catch Exception .

  5. Fange unntak bare når du kan håndtere dem effektivt for brukere og for feilsøking.

  6. Ikke nøl med å skrive dine egne unntak. Java har selvfølgelig mange ferdige, noe for enhver anledning, men noen ganger må du likevel finne opp ditt eget «hjul». Men du bør tydelig forstå hvorfor du gjør dette og være sikker på at standardsettet med unntak ikke allerede har det du trenger.

  7. Når du lager dine egne unntaksklasser, vær forsiktig med navngivningen! Du vet sikkert allerede at det er ekstremt viktig å navngi klasser, variabler, metoder og pakker riktig. Unntak er intet unntak! :) Avslutt alltid med ordet Unntak , og navnet på unntaket skal tydelig formidle hvilken type feil det representerer. For eksempel FileNotFoundException .

  8. Dokumenter unntakene dine. Vi anbefaler å skrive en @throws Javadoc-tag for unntak. Dette vil være spesielt nyttig når koden din gir grensesnitt av noe slag. Og du vil også finne det lettere å forstå din egen kode senere. Hva tenker du, hvordan kan du finne ut hva MalformedURLEexception handler om? Fra Javadoc! Ja, tanken på å skrive dokumentasjon er ikke særlig tiltalende, men tro meg, du vil takke deg selv når du kommer tilbake til din egen kode et halvt år senere.

  9. Slipp ressurser og ikke overse konstruksjonen prøv-med-ressurser .

  10. Her er den generelle oppsummeringen: bruk unntak med omhu. Å kaste et unntak er en ganske "dyr" operasjon i form av ressurser. I mange tilfeller kan det være lettere å unngå å kaste unntak og i stedet returnere for eksempel en boolsk variabel som om operasjonen lyktes, ved å bruke en enkel og "billigere" if-else .

    Det kan også være fristende å knytte applikasjonslogikk til unntak, noe du tydeligvis ikke bør gjøre. Som vi sa i begynnelsen av artikkelen, er unntak for eksepsjonelle situasjoner, ikke forventede, og det finnes ulike verktøy for å forhindre dem. Spesielt er det Valgfritt å forhindre en NullPointerException , eller Scanner.hasNext og lignende for å forhindre en IOException , som read()- metoden kan kaste.