Excepții verificate și neverificate în Java

Excepții Java 1.0

Excepțiile verificate (Exception) trebuie prinse sau declarate cu throws; cele neverificate (RuntimeException) nu sunt cerute de compilator.

Sintaxă
Throwable
├── Error                    // probleme grave ale JVM (OutOfMemoryError)
└── Exception                // verificate: IOException, SQLException...
    └── RuntimeException     // neverificate: NullPointerException, IllegalArgumentException...

class StocInsuficientException extends Exception { ... }          // verificată
class DateInvalideException extends RuntimeException { ... }      // neverificată

Ce face Excepții verificate și neverificate

Toate excepțiile pornesc din Throwable. Cele care extind Exception, dar nu RuntimeException, sunt verificate (checked): compilatorul obligă fiecare metodă care le poate arunca fie să le prindă cu try/catch, fie să le declare cu throws. Ele descriu probleme externe, previzibile: fișier lipsă, rețea căzută, o regulă de business încălcată.

Subclasele lui RuntimeException sunt neverificate (unchecked) și indică de obicei greșeli de programare: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException. Le poți prinde, dar compilatorul nu te obligă. Subclasele lui Error (OutOfMemoryError, StackOverflowError) nu se tratează în mod normal.

Excepțiile proprii se creează extinzând una din cele două clase. Excepțiile sunt serializabile, deci cu -Xlint trebuie să declari un serialVersionUID. Alege Exception când apelantul poate și trebuie să reacționeze, și RuntimeException pentru erori de utilizare a API-ului.

Exemple

Exemplul 1: O excepție verificată proprie

StocInsuficientException extinde Exception, deci comanda trebuie să o declare cu throws, iar main trebuie să o prindă. Excepția poartă și date proprii (câte bucăți lipsesc).

class StocInsuficientException extends Exception {
    private static final long serialVersionUID = 1L;
    private final int lipsa;

    StocInsuficientException(String produs, int lipsa) {
        super("stoc insuficient pentru " + produs + ": lipsesc " + lipsa + " buc.");
        this.lipsa = lipsa;
    }

    int getLipsa() {
        return lipsa;
    }
}

class Depozit {
    private int stoc = 10;

    void comanda(String produs, int cantitate) throws StocInsuficientException {
        if (cantitate > stoc) {
            throw new StocInsuficientException(produs, cantitate - stoc);
        }
        stoc -= cantitate;
        System.out.println("livrat " + cantitate + " x " + produs + ", rămân " + stoc);
    }
}

public class Main {
    public static void main(String[] args) {
        Depozit d = new Depozit();
        int[] comenzi = {4, 5, 3};
        for (int c : comenzi) {
            try {
                d.comanda("borcane de zacuscă", c);
            } catch (StocInsuficientException e) {
                System.out.println("Eroare: " + e.getMessage() + " (comandăm " + e.getLipsa() + " de la furnizor)");
            }
        }
    }
}

Rezultat

livrat 4 x borcane de zacuscă, rămân 6
livrat 5 x borcane de zacuscă, rămân 1
Eroare: stoc insuficient pentru borcane de zacuscă: lipsesc 2 buc. (comandăm 2 de la furnizor)

Exemplul 2: Neverificate: compilatorul nu cere nimic

Metodele de mai jos pot arunca excepții neverificate fără throws. Programul le prinde doar ca să arate tipul și ierarhia fiecăreia.

import java.util.List;

public class Main {
    static void ruleaza(String descriere, Runnable cod) {
        try {
            cod.run();
            System.out.println(descriere + ": OK");
        } catch (RuntimeException e) {
            System.out.println(descriere + ": " + e.getClass().getSimpleName()
                + " (verificată? " + !(e instanceof RuntimeException) + ")");
        }
    }

    public static void main(String[] args) {
        String text = null;
        int[] note = {9, 10};
        ruleaza("lungimea unui null", () -> System.out.println(text.length()));
        ruleaza("indice în afara array-ului", () -> System.out.println(note[2]));
        ruleaza("împărțire la zero", () -> System.out.println(10 / (note.length - 2)));
        ruleaza("modificarea List.of", () -> List.of(1, 2).add(3));
        ruleaza("conversie corectă", () -> Integer.parseInt("15"));

        System.out.println("IOException e verificată? "
            + !RuntimeException.class.isAssignableFrom(java.io.IOException.class));
    }
}

Rezultat

lungimea unui null: NullPointerException (verificată? false)
indice în afara array-ului: ArrayIndexOutOfBoundsException (verificată? false)
împărțire la zero: ArithmeticException (verificată? false)
modificarea List.of: UnsupportedOperationException (verificată? false)
conversie corectă: OK
IOException e verificată? true

Exemplul 3: Excepție verificată într-o lambda

Interfețele din java.util.function nu declară throws, deci o excepție verificată din lambda trebuie prinsă acolo. O soluție obișnuită: împachetarea în UncheckedIOException.

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class Main {
    public static void main(String[] args) throws IOException {
        Files.writeString(Path.of("iasi.txt"), "Iași: 271692");
        Files.writeString(Path.of("cluj.txt"), "Cluj: 286598");

        List<String> continut = List.of("iasi.txt", "cluj.txt", "lipsa.txt").stream()
            .map(f -> {
                try {
                    return Files.readString(Path.of(f));
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            })
            .limit(2)
            .toList();
        System.out.println(continut);

        try {
            List.of("lipsa.txt").forEach(f -> {
                try {
                    Files.readString(Path.of(f));
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        } catch (UncheckedIOException e) {
            System.out.println("eroare împachetată: " + e.getCause().getClass().getSimpleName());
        }
    }
}

Rezultat

[Iași: 271692, Cluj: 286598]
eroare împachetată: NoSuchFileException

Greșeli frecvente

  • Uiți serialVersionUID într-o excepție proprie: cu -Xlint:all apare [serial] serializable class StocInsuficientException has no definition of serialVersionUID.
  • Declari throws Exception peste tot ca să scapi de erori. Apelanții nu mai știu ce poate merge prost; declară tipurile exacte.
  • Prinzi NullPointerException ca să ascunzi un bug. Excepțiile neverificate se repară în cod (verifici null, indicii), nu se înghit.
  • Prinzi o excepție verificată pe care codul din try nu o poate arunca: exception IOException is never thrown in body of corresponding try statement.

Comenzi înrudite

  • try, catch și finally: try încearcă un bloc de cod, catch prinde și tratează excepțiile aruncate în el, iar finally rulează mereu la ieșire.
  • throw și throws: throw aruncă o excepție acum; throws, în antetul metodei, anunță ce excepții verificate poate arunca metoda.
  • try-with-resources: try-with-resources deschide resurse (fișiere, conexiuni) în paranteze și le închide automat la final, chiar dacă apare o excepție.
  • Exception: Clasa de bază pentru erorile pe care un program le poate trata; getMessage() returnează mesajul dat la creare.
  • RuntimeException: Clasa de bază a excepțiilor neverificate: nu trebuie declarate cu throws și nici prinse obligatoriu.