Ik denk dat je waarschijnlijk een situatie hebt meegemaakt waarin je code uitvoert en eindigt met zoiets als een NullPointerException , ClassCastException , of erger... Dit wordt gevolgd door een lang proces van debuggen, analyseren, googelen, enzovoort. Uitzonderingen zijn al geweldig: ze geven de aard van het probleem aan en waar het zich voordeed. Als je je geheugen wilt opfrissen en nog wat meer wilt leren, bekijk dan dit artikel: Uitzonderingen: aangevinkt, niet aangevinkt en aangepast .

Dat gezegd hebbende, kunnen er situaties zijn waarin u uw eigen uitzondering moet maken. Stel dat uw code informatie moet opvragen bij een externe service die om de een of andere reden niet beschikbaar is. Of stel dat iemand een aanvraagformulier voor een bankkaart invult en een telefoonnummer opgeeft dat al dan niet per ongeluk aan een andere gebruiker in het systeem is gekoppeld.

Natuurlijk hangt het juiste gedrag hier nog steeds af van de eisen van de klant en de architectuur van het systeem, maar laten we aannemen dat je de taak hebt gekregen om te controleren of het telefoonnummer al in gebruik is en een uitzondering te maken als dat zo is.

Laten we een uitzondering maken:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Vervolgens gebruiken we het wanneer we onze controle uitvoeren:


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

Om ons voorbeeld te vereenvoudigen, gebruiken we verschillende hardgecodeerde telefoonnummers om een ​​database weer te geven. En tot slot, laten we proberen onze uitzondering te gebruiken:


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

En nu is het tijd om op Shift+F10 te drukken (als u IDEA gebruikt), dwz het project uitvoeren. Dit is wat je ziet in de console:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Het opgegeven telefoonnummer is al in gebruik door een andere klant!
bij uitzondering.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Kijk naar jezelf! Je hebt je eigen uitzondering gemaakt en zelfs een beetje getest. Gefeliciteerd met deze prestatie! Ik raad aan om een ​​beetje met de code te experimenteren om beter te begrijpen hoe het werkt.

Voeg nog een controle toe - controleer bijvoorbeeld of het telefoonnummer letters bevat. Zoals u waarschijnlijk weet, worden in de Verenigde Staten vaak letters gebruikt om telefoonnummers gemakkelijker te onthouden, bijvoorbeeld 1-800-MY-APPLE. Uw controle kan ervoor zorgen dat het telefoonnummer alleen cijfers bevat.

Oké, dus we hebben een aangevinkte uitzondering gemaakt. Alles zou goed en wel zijn, maar...

De programmeergemeenschap is verdeeld in twee kampen: degenen die voorstander zijn van gecontroleerde uitzonderingen en degenen die ertegen zijn. Beide partijen voeren sterke argumenten aan. Beide bevatten eersteklas ontwikkelaars: Bruce Eckel bekritiseert gecontroleerde uitzonderingen, terwijl James Gosling ze verdedigt. Het ziet ernaar uit dat deze kwestie nooit definitief zal worden opgelost. Dat gezegd hebbende, laten we eens kijken naar de belangrijkste nadelen van het gebruik van aangevinkte uitzonderingen.

Het grootste nadeel van gecontroleerde uitzonderingen is dat ze moeten worden afgehandeld. En hier hebben we twee opties: of het op zijn plaats afhandelen met behulp van een try-catch , of, als we dezelfde uitzondering op veel plaatsen gebruiken, worpen gebruiken om de uitzonderingen op te gooien en ze te verwerken in klassen op het hoogste niveau.

We kunnen ook eindigen met standaardcode, dwz code die veel ruimte in beslag neemt, maar niet veel zwaar werk doet.

Er doen zich problemen voor in vrij grote toepassingen waarbij veel uitzonderingen worden afgehandeld: de lijst met worpen op een methode op het hoogste niveau kan gemakkelijk uitgroeien tot een dozijn uitzonderingen.

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

Ontwikkelaars houden hier meestal niet van en kiezen in plaats daarvan voor een truc: ze laten al hun aangevinkte uitzonderingen een gemeenschappelijke voorouder erven - ApplicationNameException . Nu moeten ze die ( gecontroleerde !) uitzondering ook opvangen in een handler:


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

Hier staan ​​we voor een ander probleem: wat moeten we doen in het laatste catch- blok? Hierboven hebben we alle verwachte situaties al verwerkt, dus op dit moment betekent ApplicationNameException niets meer voor ons dan " Uitzondering : er is een onbegrijpelijke fout opgetreden". Dit is hoe we het aanpakken:


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

En uiteindelijk weten we niet wat er is gebeurd.

Maar kunnen we niet alle uitzonderingen in één keer gooien, zoals dit?


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

Ja, dat kunnen we. Maar wat zegt "throws Exception" ons? Dat er iets kapot is. Je zult alles van top tot teen moeten onderzoeken en lang met de debugger moeten omgaan om de reden te begrijpen.

U kunt ook een constructie tegenkomen die soms "uitzondering slikken" wordt genoemd:


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

Er valt hier niet veel toe te voegen bij wijze van uitleg - de code maakt alles duidelijk, of beter gezegd, het maakt alles onduidelijk.

Je kunt natuurlijk zeggen dat je dit niet in echte code zult zien. Laten we eens kijken in de ingewanden (de code) van de URL- klasse van het java.net- pakket. Volg me als je het wilt weten!

Hier is een van de constructies in de URL- klasse:


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

Zoals u kunt zien, hebben we een interessante aangevinkte uitzondering: MalformedURLException . Dit is wanneer het kan worden gegenereerd (en ik citeer):
"als er geen protocol is opgegeven, of als een onbekend protocol is gevonden, of als de specificatie null is, of als de geparseerde URL niet voldoet aan de specifieke syntaxis van het bijbehorende protocol."

Dat is:

  1. Als er geen protocol is opgegeven.
  2. Er is een onbekend protocol gevonden.
  3. De specificatie is nul .
  4. De URL voldoet niet aan de specifieke syntaxis van het bijbehorende protocol.

Laten we een methode maken die een URL- object maakt:


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

Zodra je deze regels in de IDE schrijft (ik codeer in IDEA, maar dit werkt zelfs in Eclipse en NetBeans), zie je dit:

Dit betekent dat we een uitzondering moeten genereren of de code in een try-catch- blok moeten wikkelen. Voor nu stel ik voor om de tweede optie te kiezen om te visualiseren wat er gebeurt:


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

Zoals je kunt zien, is de code al behoorlijk uitgebreid. En daar hebben we hierboven op gezinspeeld. Dit is een van de meest voor de hand liggende redenen om ongecontroleerde uitzonderingen te gebruiken.

We kunnen een ongecontroleerde uitzondering maken door RuntimeException in Java uit te breiden.

Uitzonderingen die niet zijn aangevinkt, worden overgenomen van de Error- klasse of de RuntimeException- klasse. Veel programmeurs zijn van mening dat deze uitzonderingen in onze programma's kunnen worden afgehandeld omdat ze fouten vertegenwoordigen waarvan we niet kunnen verwachten dat ze worden hersteld terwijl het programma draait.

Wanneer een ongecontroleerde uitzondering optreedt, wordt dit meestal veroorzaakt door een onjuist gebruik van code, het doorgeven van een argument dat null of anderszins ongeldig is.

Nou, laten we de code schrijven:


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 op dat we meerdere constructors hebben gemaakt voor verschillende doeleinden. Hierdoor kunnen we onze uitzondering meer mogelijkheden geven. We kunnen er bijvoorbeeld voor zorgen dat een exception ons een foutcode geeft. Laten we om te beginnen een opsomming maken om onze foutcodes weer te geven:


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

Laten we nu nog een constructor toevoegen aan onze uitzonderingsklasse:


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

En laten we niet vergeten een veld toe te voegen (we waren het bijna vergeten):


private Integer errorCode;
    

En natuurlijk een methode om deze code te krijgen:


public Integer getErrorCode() {
   return errorCode;
}
    

Laten we naar de hele klas kijken, zodat we het kunnen controleren en vergelijken:

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! Onze uitzondering is gedaan! Zoals u kunt zien, is er hier niets bijzonder ingewikkelds. Laten we het in actie bekijken:


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

Wanneer we onze kleine applicatie uitvoeren, zien we zoiets als het volgende in de console:

Laten we nu profiteren van de extra functionaliteit die we hebben toegevoegd. We zullen een beetje toevoegen aan de vorige code:


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

}
    

U kunt op dezelfde manier met uitzonderingen werken als met objecten. Ik weet zeker dat je al weet dat alles in Java een object is.

En kijk eens wat we deden. Eerst hebben we de methode gewijzigd, die nu niet gooit, maar in plaats daarvan gewoon een uitzondering maakt, afhankelijk van de invoerparameter. Vervolgens genereren we met behulp van een switch-case- instructie een uitzondering met de gewenste foutcode en bericht. En in de hoofdmethode krijgen we de gemaakte uitzondering, krijgen we de foutcode en gooien we deze.

Laten we dit uitvoeren en kijken wat we op de console krijgen:

Kijk - we hebben de foutcode afgedrukt die we van de uitzondering hebben gekregen en hebben vervolgens de uitzondering zelf gegooid. Bovendien kunnen we zelfs precies volgen waar de uitzondering is gegenereerd. Indien nodig kunt u alle relevante informatie aan het bericht toevoegen, aanvullende foutcodes maken en nieuwe functies aan uw uitzonderingen toevoegen.

Nou, wat vind je daarvan? Ik hoop dat alles voor je gelukt is!

Uitzonderingen zijn over het algemeen een vrij uitgebreid onderwerp en niet eenduidig. Er zullen nog veel geschillen over zijn. Alleen Java heeft bijvoorbeeld gecontroleerde uitzonderingen. Van de meest populaire talen heb ik er geen gezien die ze gebruikt.

Bruce Eckel schreef heel goed over uitzonderingen in hoofdstuk 12 van zijn boek "Thinking in Java" - ik raad je aan het te lezen! Kijk ook eens naar het eerste deel van Horstmann's "Core Java" - het bevat ook veel interessante dingen in hoofdstuk 7.

Een kleine samenvatting

  1. Schrijf alles naar een logboek! Meld berichten in gegooide uitzonderingen. Dit helpt meestal veel bij het debuggen en stelt u in staat te begrijpen wat er is gebeurd. Laat een catch- blok niet leeg, anders "slikt" het de uitzondering gewoon in en heb je geen informatie om je te helpen bij het opsporen van problemen.

  2. Als het op uitzonderingen aankomt, is het een slechte gewoonte om ze allemaal tegelijk te vangen (zoals een collega van mij zei: "het is geen Pokemon, het is Java"), dus vermijd vangst (Uitzondering e) of erger nog, vangst (Throwable t) .

  3. Gooi uitzonderingen zo vroeg mogelijk. Dit is goede Java-programmeerpraktijk. Wanneer je frameworks zoals Spring bestudeert, zul je zien dat ze het "fail fast"-principe volgen. Dat wil zeggen, ze "falen" zo snel mogelijk om het mogelijk te maken de fout snel te vinden. Dit brengt natuurlijk bepaalde ongemakken met zich mee. Maar deze aanpak helpt bij het creëren van robuustere code.

  4. Bij het aanroepen van andere delen van de code is het het beste om bepaalde uitzonderingen op te vangen. Als de aangeroepen code meerdere uitzonderingen genereert, is het een slechte programmeerpraktijk om alleen de bovenliggende klasse van die uitzonderingen op te vangen. Stel dat u code aanroept die FileNotFoundException en IOException genereert . In uw code die deze module aanroept, is het beter om twee catch-blokken te schrijven om elk van de uitzonderingen op te vangen, in plaats van een enkele catch to catch Exception .

  5. Vang uitzonderingen alleen op als u ze effectief kunt afhandelen voor gebruikers en voor foutopsporing.

  6. Aarzel niet om uw eigen uitzonderingen te schrijven. Natuurlijk heeft Java veel kant-en-klare exemplaren, voor elke gelegenheid iets, maar soms moet je toch je eigen "wiel" uitvinden. Maar u moet goed begrijpen waarom u dit doet en er zeker van zijn dat de standaardreeks uitzonderingen niet al bevat wat u nodig hebt.

  7. Wees voorzichtig met de naamgeving wanneer u uw eigen uitzonderingsklassen maakt! U weet waarschijnlijk al dat het uiterst belangrijk is om klassen, variabelen, methoden en pakketten correct te benoemen. Uitzonderingen zijn geen uitzondering! :) Eindig altijd met het woord Exception , en de naam van de exception moet duidelijk aangeven welk type fout het vertegenwoordigt. Bijvoorbeeld FileNotFoundException .

  8. Documenteer uw uitzonderingen. We raden aan om een ​​@throws Javadoc-tag te schrijven voor uitzonderingen. Dit is vooral handig wanneer uw code interfaces van welke aard dan ook biedt. En u zult het later ook gemakkelijker vinden om uw eigen code te begrijpen. Wat denk je, hoe kun je bepalen waar MalformedURLException over gaat? Van Javadoc! Ja, de gedachte aan het schrijven van documentatie is niet erg aantrekkelijk, maar geloof me, je zult jezelf dankbaar zijn als je zes maanden later terugkeert naar je eigen code.

  9. Maak middelen vrij en verwaarloos de constructie van het proberen met middelen niet .

  10. Hier is de algemene samenvatting: gebruik uitzonderingen verstandig. Het genereren van een uitzondering is een vrij "dure" operatie in termen van middelen. In veel gevallen kan het eenvoudiger zijn om het genereren van uitzonderingen te vermijden en in plaats daarvan bijvoorbeeld een booleaanse variabele te retourneren die aangeeft of de bewerking is geslaagd, met behulp van een eenvoudige en "goedkopere" if-else .

    Het kan ook verleidelijk zijn om applicatielogica te koppelen aan uitzonderingen, wat u duidelijk niet zou moeten doen. Zoals we aan het begin van het artikel al zeiden, zijn er uitzonderingen voor uitzonderlijke situaties, niet verwachte situaties, en er zijn verschillende hulpmiddelen om ze te voorkomen. In het bijzonder is er Optional om een ​​NullPointerException te voorkomen , of Scanner.hasNext en dergelijke om een ​​IOException te voorkomen , die de methode read() kan genereren.