Erindite käsitlemine
Sissejuhatus
Erindi tekkimisel liigub see üles mööda kutsepinu (call stack), kuni keegi selle kinni püüab. Kui erindit kinni ei püüta, programm peatub. Selles peatükis tutvume, kuidas erindeid kinni püüda, kuidas deklareerida, et meetod võib erindi visata ning kuidas tagada, et mingi kindel koodijupp alati käivitub erindi tekkimisel ka juhul, kui programmi tavapärane töö pooleli jäi.
try/catch
Erindeid käsitletakse try/catch plokiga.
Üldine struktuur sellel on järgmine:
try {
// code that might throw
} catch (ExceptionType e) {
// code that runs if the exception occurs
}
try plokk sisaldab koodi, mis võib veaga lõppeda.
Kui erind tekib, katkestatakse try ploki täitmine koheselt ning ülejäänud koodi seal ei käivitata ning liigutakse edasi catch ploki juurde.
Näiteks:
public int divide(int a, int b) {
try {
return a / b;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero: " + e.getMessage());
return 0;
}
}
System.out.println(divide(10, 2)); // 5
System.out.println(divide(10, 0)); // Cannot divide by zero: / by zero
// 0
Kui catch plokis ei ole vaja erindiga midagi teha, nimeta erindi muutuja ignored-iks, et märku anda sellest, et catch plokk on teadlikult tühjaks jäetud:
try {
doSomething();
} catch (SomeException ignored) {}
See teeb koodi loetavamaks ja väldib segadust, kas erindi jäi kogemata käsitlemata.
Mitme erineva erindi püüdmine
catch plokke on võimalik ka aheldada, juhul kui kood võib lõppeda mitme erineva erindi tüübiga ning igaüht neist peab eraldi käsitlema.
Näiteks:
try {
String text = null;
int number = Integer.parseInt(text);
} catch (NumberFormatException e) {
System.out.println("Not a valid number");
} catch (NullPointerException e) {
System.out.println("Text was null");
}
Java kontrollib catch-plokke ülevalt ja kasutab esimest, mis sobib (ehk tüübid on samad).
Kui kahe või rohkema erindi puhul käsitletakse neid samamoodi, siis neid saab kombineerida püstkriipsuga (|):
try {
// ...
} catch (NumberFormatException | NullPointerException e) {
System.out.println("Invalid input: " + e.getMessage());
}
Järjekord on oluline
Spetsiifilisem erindi tüüp peab tulema enne üldisemat.
Exception püüab kinni kõik erindid, seega selle esimesena paigutamine muudaks teised catch plokid kättesaamatuks:
// Wrong
try {
// ...
} catch (Exception e) { // catches everything
System.out.println("Error");
} catch (ArithmeticException e) { // Error: unreachable catch block
System.out.println("Math error");
}
// Correct
try {
// ...
} catch (ArithmeticException e) { // specific first
System.out.println("Math error");
} catch (Exception e) { // general last
System.out.println("Error");
}
Üldiselt tuleks vältida Exception-tüübi püüdmist.
Exception on väga üldine tüüp ning selle püüdmine võib kinni püüda ka erindeid, mida sa ei oska ega tohiks käsitleda.
See võib varjata programmeerimisvigu ja raskendada silumist.
Selle asemel tuleks püüda võimalikult spetsiifilist erinditüüpi, mida oskad korrektselt käsitleda.
finally
try/catch plokile on võimalik juurde lisada ka finally plokk.
finally plokis olev kood käivitatakse peaaegu alati (JVM krahhi või System.exit() puhul ei käivitata), sõltumata sellest, kas erind tekkis või mitte.
Seda kasutatakse koristuskoodi jaoks, näiteks faili sulgemiseks või ressursi vabastamiseks.
try {
System.out.println("Trying...");
int result = 10 / 0;
System.out.println("This line is skipped");
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getMessage());
} finally {
System.out.println("This always runs");
}
Trying...
Caught: / by zero
This always runs
throws deklaratsioon
Kui meetod võib visata kontrollitud erindit, kuid seda ise kinni ei püüa, siis peab seda deklareerima throws märksõnaga.
Sisuliselt delegeeritakse kinni püüdmise vastustus mõne teise meetodi kätte - see ei tähenda, et erindiga enam tegelema ei pea.
Näiteks:
public void loadFile(String path) throws IOException {
FileReader reader = new FileReader(path);
// ...
}
Kui meetod viskab mitut erinevat erindit, siis neid kõike saab ühel real deklareerida:
public void process(String path) throws IOException, SQLException {
// ...
}
Erindite uuesti tõstatamine
On olukordi, kus meetod püüab erindi kinni, kuid ei saa seda täielikult käsitleda. Sel juhul on võimalik erind kinni püüda, teha sellega midagi (logimine näiteks) ja seejärel see uuesti visata, näiteks:
public void loadConfig(String path) throws IOException {
try {
FileReader reader = new FileReader(path);
} catch (IOException e) {
System.out.println("Failed to load config from: " + path);
throw e; // rethrow the same exception
}
}
Antud võtet kasutatakse reeglina kahel juhul:
- Erindite tõlkimise eesmärgil
- Erindite mähkimiseks
Erindite tõlkimine
Erindite tõlkimine tähendab ühe erindi teisendamist teiseks erindiks, mis sobib paremini antud abstraktsioonitasemega ja annab rohkem konteksti.
public void registerProduct(String name, double price) {
try {
addToStock(name, price);
} catch (StockException e) {
throw new ProductRegistrationException("Registration failed: " + name, e);
}
}
Tõlkimise puhul on tähtis esialgne tekkepõhjus ka edastada argumendina. See võimaldab koodi silumisel säilitada tervikliku pilti ehk kust probleem alguse sai:
} catch (ProductRegistrationException e) {
System.out.println(e.getMessage()); // Registration failed: Widget
System.out.println(e.getCause()); // original StockException
}
Erindite mähkimine
Erindite mähkimise all mõeldakse kontrollitud erindi teisendamist kontrollimata erindiks.
See on kasulik juhul, kui kontrollitud erind tekib kohas, kus seda pole mõistlik käsitleda ega meetodi signatuuris deklareerida, näiteks sügaval äriloogikas, kus throws IOException oleks vaid müra.
public void loadConfig(String path) {
try {
FileReader reader = new FileReader(path);
// ...
} catch (IOException e) {
throw new RuntimeException("Failed to load config: " + path, e);
}
}
Nüüd saab loadConfig meetodit kutsuda ilma throws IOException deklaratsioonita ning kutsuja ei pea seda eraldi käsitlema.
Esialgne IOException edastatakse siiski põhjusena edasi, mis võimaldab seda vajadusel silumise käigus näha:
} catch (RuntimeException e) {
System.out.println(e.getMessage()); // Failed to load config: settings.txt
System.out.println(e.getCause()); // java.io.FileNotFoundException: settings.txt
}
Erindite mähkimine peab olema teadlik otsus.
Kontrollitud erindid on loodud selleks, et sundida meetodi kutsujat nendega tegelema.
Nende vaikimisi RuntimeException-iks mähkimine eemaldab selle garantii ja võib varjata vigu, millega tegelikult peaks tegelema.
Mähkimine on põhjendatud näiteks juhul, kui:
- kolmanda osapoole teegi kontrollitud erind on äriloogikas ebaoluline
- erindist toibumist pole mõistlik oodata (nt puuduv konfiguratsioonifail)