Azt hiszem, valószínűleg tapasztalt már olyan helyzetet, amikor kódot futtat, és a végén valami NullPointerException , ClassCastException , vagy még rosszabb... Ezt egy hosszú hibakeresési, elemzési, guglizási stb. folyamat követi. A kivételek úgy csodálatosak, ahogy vannak: jelzik a probléma természetét és azt, hogy hol történt. Ha szeretné felfrissíteni a memóriáját, és csak egy kicsit többet szeretne megtudni, tekintse meg ezt a cikket: Kivételek: bejelölve, nem bejelölve és egyéni .

Ennek ellenére előfordulhatnak olyan helyzetek, amikor saját kivételt kell létrehoznia. Tegyük fel például, hogy a kódnak információt kell kérnie egy távoli szolgáltatástól, amely valamilyen okból nem érhető el. Vagy tegyük fel, hogy valaki kitölti a bankkártya igénylését, és megad egy telefonszámot, amely akár véletlenül, akár nem, már társítva van egy másik felhasználóhoz a rendszerben.

Természetesen a helyes viselkedés itt továbbra is az ügyfél igényeitől és a rendszer architektúrájától függ, de tegyük fel, hogy Ön azt a feladatot kapta, hogy ellenőrizze, hogy a telefonszám használatban van-e, és ha igen, tegyen kivételt.

Hozzunk létre egy kivételt:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Ezt követően az ellenőrzés során használjuk:


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

Példánk leegyszerűsítése érdekében több kódolt telefonszámot fogunk használni egy adatbázis ábrázolására. És végül próbáljuk meg kihasználni a kivételünket:


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

És most itt az ideje lenyomni a Shift+F10 billentyűkombinációt (ha IDEA-t használsz), azaz futtatni a projektet. Ezt fogja látni a konzolon:

kivétel.CreditCardIssue
kivétel.PhoneNumberAlreadyExistsException: A megadott telefonszámot egy másik ügyfél már használja!
kivételesen.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Nézz magadra! Létrehoztad a saját kivételedet, és még tesztelted is egy kicsit. Gratulálunk ehhez az eredményhez! Azt javaslom, hogy kísérletezzen egy kicsit a kóddal, hogy jobban megértse, hogyan működik.

Adjon hozzá egy másik ellenőrzést – például ellenőrizze, hogy a telefonszám tartalmaz-e betűket. Amint azt bizonyára Ön is tudja, az Egyesült Államokban gyakran használnak betűket a telefonszámok könnyebb megjegyezésére, pl. 1-800-MY-APPLE. A csekk biztosíthatja, hogy a telefonszám csak számokat tartalmazzon.

Rendben, létrehoztunk egy ellenőrzött kivételt. Minden rendben és jó lenne, de...

A programozói közösség két táborra oszlik: azokra, akik támogatják az ellenőrzött kivételeket, és azokra, akik ellenzik azokat. Mindkét fél erős érveket hoz fel. Mindkettőben vannak kiváló fejlesztők: Bruce Eckel kritizálja az ellenőrzött kivételeket, míg James Gosling védi őket. Úgy tűnik, ez az ügy soha nem fog véglegesen megoldódni. Ennek ellenére nézzük meg az ellenőrzött kivételek használatának fő hátrányait.

Az ellenőrzött kivételek fő hátránya, hogy kezelni kell őket. És itt két lehetőségünk van: vagy a helyén kezeljük try-catch segítségével , vagy ha sok helyen használjuk ugyanazt a kivételt, dobásokkal dobjuk fel a kivételeket, és dolgozzuk fel azokat a legfelső szintű osztályokban.

Ezenkívül előfordulhat, hogy "boilerplate" kódot kapunk, azaz olyan kódot, amely sok helyet foglal el, de nem végez túl nagy nehézségeket.

Problémák merülnek fel a meglehetősen nagy alkalmazásokban, sok kivételt kezelve: a legfelső szintű metódusok dobási listája könnyen kinőhet tucatnyi kivételre.

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

A fejlesztők általában nem szeretik ezt, és ehelyett egy trükköt választanak: az összes ellenőrzött kivételt egy közös őst örökölnek – ApplicationNameException . Most azt a ( ellenőrzött !) kivételt is el kell fogniuk egy kezelőben:


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

Itt egy másik problémával nézünk szembe – mit tegyünk az utolsó fogási blokkban? Fent már az összes várt helyzetet feldolgoztuk, így ezen a ponton az ApplicationNameException nem jelent mást számunkra, mint " Kivétel : valami érthetetlen hiba történt". Így kezeljük:


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

És végül nem tudjuk, mi történt.

De nem dobhatnánk ki minden kivételt egyszerre, így?


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

Igen, megtehetnénk. De mit üzen nekünk a "kivétel dobása"? Hogy valami elromlott. Mindent tetőtől talpig ki kell vizsgálnia, és sokáig kell kényelembe helyeznie a hibakeresőt, hogy megértse az okot.

Találkozhat olyan konstrukcióval is, amelyet néha "kivétel nyelésnek" neveznek:


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

Ehhez nincs sok hozzáfűznivaló magyarázatként – a kód mindent világossá tesz, vagy inkább homályossá tesz.

Természetesen azt mondhatja, hogy ezt nem fogja látni a valós kódban. Nos, nézzük meg a java.net csomag URL osztályának lényegét (a kódját) . Kövess, ha tudni akarod!

Íme az egyik konstrukció az URL osztályban:


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

Amint látja, van egy érdekes ellenőrzött kivételünk – MalformedURLException . Itt lehet dobni (és idézem):
"ha nincs megadva protokoll, vagy ismeretlen protokoll található, vagy a specifikáció nulla, vagy az elemzett URL nem felel meg a társított protokoll konkrét szintaxisának."

Azaz:

  1. Ha nincs megadva protokoll.
  2. Ismeretlen protokoll található.
  3. A specifikáció nulla .
  4. Az URL nem felel meg a társított protokoll sajátos szintaxisának.

Hozzunk létre egy módszert, amely létrehoz egy URL objektumot:


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

Amint beírod ezeket a sorokat az IDE-ben (IDEA-ban kódolok, de ez még Eclipse-ben és NetBeans-ben is működik), ezt fogod látni:

Ez azt jelenti, hogy vagy kivételt kell dobnunk, vagy a kódot egy try-catch blokkba kell csomagolnunk. Egyelőre azt javaslom, hogy válassza a második lehetőséget a történések megjelenítéséhez:


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

Amint látható, a kód már meglehetősen bőbeszédű. És erre fentebb utaltunk. Ez az egyik legnyilvánvalóbb ok a nem ellenőrzött kivételek használatára.

Ellenőrizetlen kivételt hozhatunk létre a RuntimeException kiterjesztésével Java-ban.

A nem ellenőrzött kivételek az Error osztályból vagy a RuntimeException osztályból öröklődnek . Sok programozó úgy érzi, hogy ezek a kivételek kezelhetők a programjainkban, mert olyan hibákat jelentenek, amelyekből a program futása közben nem számíthatunk helyreállásra.

Ha ellenőrizetlen kivétel történik, azt általában a kód helytelen használata, egy nulla vagy más módon érvénytelen argumentum átadása okozza.

Nos, írjuk a kódot:


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

Vegye figyelembe, hogy több konstruktőrt készítettünk különböző célokra. Ez lehetővé teszi, hogy kivételeinknek több képességet biztosítsunk. Például megtehetjük, hogy egy kivétel hibakódot adjon nekünk. Kezdésként készítsünk egy felsorolást a hibakódok ábrázolására:


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

Most adjunk hozzá egy másik konstruktort a kivételosztályunkhoz:


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

És ne felejtsünk el hozzáadni egy mezőt (majdnem elfelejtettük):


private Integer errorCode;
    

És természetesen egy módszer a kód beszerzésére:


public Integer getErrorCode() {
   return errorCode;
}
    

Nézzük meg az egész osztályt, hogy ellenőrizhessük és összehasonlíthassuk:

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! Kivételünk megtörtént! Amint látja, nincs itt semmi különösebben bonyolult. Nézzük meg működés közben:


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

Amikor elindítjuk a kis alkalmazásunkat, valami ilyesmit fogunk látni a konzolon:

Most pedig használjuk ki az általunk hozzáadott extra funkciókat. Kicsit kiegészítjük az előző kódot:


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

}
    

A kivételekkel ugyanúgy dolgozhat, mint az objektumokkal. Persze biztos vagyok benne, hogy már tudja, hogy a Java-ban minden objektum.

És nézd meg, mit csináltunk. Először a metóduson változtattunk, ami most nem dob, hanem egyszerűen kivételt hoz létre, a bemeneti paramétertől függően. Ezután egy switch-case utasítással kivételt generálunk a kívánt hibakóddal és üzenettel. És a fő módszerben megkapjuk a létrehozott kivételt, megkapjuk a hibakódot, és dobjuk.

Futtassuk ezt, és nézzük meg, mit kapunk a konzolon:

Nézd – kinyomtattuk a kivételtől kapott hibakódot, majd magát a kivételt dobtuk ki. Sőt, még azt is nyomon tudjuk követni, hogy pontosan hova dobták a kivételt. Igény szerint minden lényeges információt hozzáadhat az üzenethez, további hibakódokat hozhat létre, és új funkciókat adhat hozzá a kivételekhez.

Nos, mit gondolsz erről? Remélem minden sikerült neked!

Általában a kivételek meglehetősen kiterjedt téma, és nem egyértelműek. Sok vita lesz még ezzel kapcsolatban. Például csak a Java ellenőrizte a kivételeket. A legnépszerűbb nyelvek között nem láttam olyat, amelyik ezeket használná.

Bruce Eckel nagyon jól írt a kivételekről „Gondolkodás a Java-ban” című könyvének 12. fejezetében – javaslom, hogy olvassa el! Vessen egy pillantást Horstmann „Core Java” című művének első kötetére is – a 7. fejezetben szintén sok érdekesség található.

Egy kis összefoglaló

  1. Írj mindent egy naplóba! Üzenetek naplózása a dobott kivételekben. Ez általában sokat segít a hibakeresésben, és lehetővé teszi a történtek megértését. Ne hagyjon üresen egy fogási blokkot, különben csak "lenyeli" a kivételt, és nem lesz olyan információja, amely segítene a problémák levadászásában.

  2. Ami a kivételeket illeti, rossz gyakorlat egyszerre elkapni őket (ahogy egy kollégám mondta: "ez nem Pokemon, hanem Java"), ezért kerülje a fogást (e kivétel), vagy ami még rosszabb, elkapja ( Throwable t ) .

  3. A lehető leghamarabb dobja ki a kivételeket. Ez egy jó Java programozási gyakorlat. Ha olyan keretrendszereket tanulmányoz, mint a tavasz, látni fogja, hogy a „gyors bukás” elvét követik. Vagyis a lehető legkorábban "elbuknak", hogy gyorsan megtalálják a hibát. Ez persze bizonyos kellemetlenségekkel jár. De ez a megközelítés segít robusztusabb kód létrehozásában.

  4. A kód más részeinek meghívásakor a legjobb, ha megragad bizonyos kivételeket. Ha a hívott kód több kivételt is dob, akkor rossz programozási gyakorlat, ha csak a kivételek szülőosztályát fogja meg. Tegyük fel például, hogy olyan kódot hív meg, amely FileNotFoundException és IOException parancsot dob . A modult meghívó kódban jobb, ha két fogási blokkot ír az egyes kivételek elkapásához, ahelyett, hogy egyetlen fogást elkaphatna az Exception .

  5. Csak akkor fogadja el a kivételeket, ha hatékonyan tudja kezelni azokat a felhasználók és a hibakeresés érdekében.

  6. Ne habozzon megírni a saját kivételeit. Természetesen a Java-ban rengeteg kész van, minden alkalomra valami, de néha még mindig fel kell találni a saját "kereket". De világosan meg kell értenie, hogy miért teszi ezt, és meg kell bizonyosodnia arról, hogy a szokásos kivételkészlet nem tartalmazza azt, amire szüksége van.

  7. Amikor saját kivételosztályokat hoz létre, ügyeljen az elnevezésre! Valószínűleg már tudja, hogy rendkívül fontos az osztályok, változók, metódusok és csomagok helyes elnevezése. A kivételek sem kivételek! :) Mindig az Exception szóval fejezd be , és a kivétel nevének egyértelműen jeleznie kell a hiba típusát. Például FileNotFoundException .

  8. Dokumentálja a kivételeket. Javasoljuk, hogy írjon @throws Javadoc címkét a kivételekhez. Ez különösen akkor lesz hasznos, ha a kódja bármilyen interfészt biztosít. És később könnyebben megértheti saját kódját is. Mit gondol, hogyan lehet meghatározni, hogy miről szól a MalformedURLException ? Javadoc-ról! Igen, a dokumentáció írásának gondolata nem túl vonzó, de hidd el, meg fogod hálálni magadnak, ha hat hónappal később visszatérsz saját kódodhoz.

  9. Szabadítsa fel az erőforrásokat, és ne hagyja figyelmen kívül az erőforrásokkal próbálkozás konstrukciót.

  10. Íme az általános összefoglaló: bölcsen használjon kivételeket. A kivétel kidobása erőforrások szempontjából meglehetősen "drága" művelet. Sok esetben egyszerűbb lehet elkerülni a kivételek dobását, és ehelyett, mondjuk, egy logikai változót ad vissza, amely jelzi, hogy a művelet sikeres volt-e, egy egyszerű és „kevésbé költséges” if-else használatával .

    Az is csábító lehet, hogy az alkalmazási logikát a kivételekhez kötjük, amit nyilvánvalóan nem szabad megtennie. Ahogy a cikk elején is mondtuk, a kivételek a rendkívüli helyzetekre vonatkoznak, nem az elvárásokra, és ezek megelőzésére többféle eszköz létezik. Különösen létezik az Opcionális a NullPointerException megakadályozására , vagy a Scanner.hasNext és hasonlók az IOException megakadályozására , amelyet a read() metódus dobhat.