Altri hanno riassunto abbastanza bene perché lanciare in anticipo . Consentitemi di concentrarmi sul perché prendere la parte tardiva invece, per la quale non ho visto una spiegazione soddisfacente per i miei gusti.
PERCHÉ LE ECCEZIONI?
Sembra esserci una certa confusione sul perché le eccezioni esistano in primo luogo. Vorrei condividere il grande segreto qui: il motivo delle eccezioni e la gestione delle eccezioni è ... ASTRAZIONE .
Hai visto il codice in questo modo:
static int divide(int dividend, int divisor) throws DivideByZeroException {
if (divisor == 0)
throw new DivideByZeroException(); // that's a checked exception indeed
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
try {
int res = divide(a, b);
System.out.println(res);
} catch (DivideByZeroException e) {
// checked exception... I'm forced to handle it!
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Non è così che dovrebbero essere usate le eccezioni. Codice come quello sopra esiste nella vita reale, ma sono più un'aberrazione e sono davvero l'eccezione (gioco di parole). La definizione di divisione, ad esempio, anche nella matematica pura, è condizionata: è sempre il "codice chiamante" che deve gestire il caso eccezionale di zero per limitare il dominio di input. È brutto. Fa sempre male al chiamante. Tuttavia, per tali situazioni il modello check-then-do è il modo naturale di procedere:
static int divide(int dividend, int divisor) {
// throws unchecked ArithmeticException for 0 divisor
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
if (b != 0) {
int res = divide(a, b);
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
In alternativa, puoi eseguire il comando completo sullo stile OOP in questo modo:
static class Division {
final int dividend;
final int divisor;
private Division(int dividend, int divisor) {
this.dividend = dividend;
this.divisor = divisor;
}
public boolean check() {
return divisor != 0;
}
public int eval() {
return dividend / divisor;
}
public static Division with(int dividend, int divisor) {
return new Division(dividend, divisor);
}
}
static void doDivide() {
int a = readInt();
int b = readInt();
Division d = Division.with(a, b);
if (d.check()) {
int res = d.eval();
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Come vedi, il codice chiamante ha l'onere del controllo preliminare, ma non fa alcuna gestione delle eccezioni dopo. Se ArithmeticException
mai viene dalla chiamata divide
o eval
, allora sei TU che devi fare la gestione delle eccezioni e correggere il tuo codice, perché hai dimenticato il check()
. Per le ragioni simili catturare un NullPointerException
è quasi sempre la cosa sbagliata da fare.
Ora ci sono alcune persone che affermano di voler vedere casi eccezionali nella firma del metodo / funzione, ovvero di estendere esplicitamente il dominio di output . Sono quelli che preferiscono le eccezioni verificate . Naturalmente, la modifica del dominio di output dovrebbe forzare l'adattamento di qualsiasi codice del chiamante diretto, e ciò sarebbe effettivamente ottenuto con eccezioni verificate. Ma non hai bisogno di eccezioni per questo! Ecco perché hai Nullable<T>
classi generiche , classi di casi , tipi di dati algebrici e tipi di unione . Alcune persone OO potrebbero persino preferire tornare null
per semplici casi di errore come questo:
static Integer divide(int dividend, int divisor) {
if (divisor == 0) return null;
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
Integer res = divide(a, b);
if (res != null) {
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Tecnicamente le eccezioni possono essere utilizzate per lo scopo di cui sopra, ma qui è il punto: non esistono eccezioni per tale uso . Le eccezioni sono l'astrazione pro. Le eccezioni riguardano il riferimento indiretto. Le eccezioni consentono di estendere il dominio "outcome" senza interrompere i contratti diretti con i clienti e rinviare la gestione degli errori a "altrove". Se il tuo codice genera eccezioni che vengono gestite nei chiamanti diretti dello stesso codice, senza alcun livello di astrazione nel mezzo, lo stai facendo SBAGLIATO
COME CATTURARE TARDI?
Allora eccoci qua. Ho discusso a fondo per dimostrare che l'utilizzo delle eccezioni negli scenari precedenti non è il modo in cui le eccezioni devono essere utilizzate. Esiste tuttavia un caso d'uso reale, in cui l'astrazione e l'indirizzamento offerto dalla gestione delle eccezioni è indispensabile. Comprendere tale utilizzo aiuterà anche a capire la raccomandazione del ritardo di cattura .
Quel caso d'uso è: Programmazione contro astrazioni di risorse ...
Sì, la logica aziendale dovrebbe essere programmata contro astrazioni , non implementazioni concrete. Il codice di "cablaggio" IOC di livello superiore creerà un'istanza delle implementazioni concrete delle astrazioni delle risorse e le trasmetterà alla logica aziendale. Niente di nuovo qui. Ma le implementazioni concrete di quelle astrazioni di risorse possono potenzialmente generare eccezioni specifiche per la propria implementazione , no?
Quindi chi può gestire tali eccezioni specifiche di implementazione? È quindi possibile gestire eccezioni specifiche delle risorse nella logica aziendale? No, non lo è. La logica aziendale è programmata contro le astrazioni, il che esclude la conoscenza di tali dettagli di eccezione specifici dell'implementazione.
"Ah!", Potresti dire: "ma è per questo che possiamo sottoclassare le eccezioni e creare gerarchie di eccezioni" (controlla Mr. Spring !). Lascia che te lo dica, è un errore. In primo luogo, ogni libro ragionevole su OOP afferma che l'eredità concreta è negativa, ma in qualche modo questa componente fondamentale di JVM, la gestione delle eccezioni, è strettamente legata all'eredità concreta. Ironia della sorte, Joshua Bloch non avrebbe potuto scrivere il suo libro Effective Java prima di poter avere l'esperienza con una JVM funzionante, vero? È più un libro "lezioni apprese" per la prossima generazione. In secondo luogo, e ancora più importante, se si rileva un'eccezione di alto livello, come si gestirà?PatientNeedsImmediateAttentionException
: dobbiamo darle una pillola o amputarle le gambe !? Che ne dici di un'istruzione switch su tutte le possibili sottoclassi? Ecco il tuo polimorfismo, ecco l'astrazione. Hai capito.
Quindi chi può gestire le eccezioni specifiche delle risorse? Deve essere colui che conosce le concrezioni! Colui che ha istanziato la risorsa! Il codice "cablaggio" ovviamente! Controllalo:
Logica aziendale codificata contro le astrazioni ... NESSUNA GESTIONE DEGLI ERRORI DELLE RISORSE CONCRETE!
static interface InputResource {
String fetchData();
}
static interface OutputResource {
void writeData(String data);
}
static void doMyBusiness(InputResource in, OutputResource out, int times) {
for (int i = 0; i < times; i++) {
System.out.println("fetching data");
String data = in.fetchData();
System.out.println("outputting data");
out.writeData(data);
}
}
Nel frattempo da qualche altra parte le implementazioni concrete ...
static class ConstantInputResource implements InputResource {
@Override
public String fetchData() {
return "Hello World!";
}
}
static class FailingInputResourceException extends RuntimeException {
public FailingInputResourceException(String message) {
super(message);
}
}
static class FailingInputResource implements InputResource {
@Override
public String fetchData() {
throw new FailingInputResourceException("I am a complete failure!");
}
}
static class StandardOutputResource implements OutputResource {
@Override
public void writeData(String data) {
System.out.println("DATA: " + data);
}
}
E infine il codice di cablaggio ... Chi gestisce le eccezioni concrete delle risorse? Quello che li conosce!
static void start() {
InputResource in1 = new FailingInputResource();
InputResource in2 = new ConstantInputResource();
OutputResource out = new StandardOutputResource();
try {
ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
}
catch (FailingInputResourceException e)
{
System.out.println(e.getMessage());
System.out.println("retrying...");
ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
}
}
Ora abbi pazienza con me. Il codice sopra è semplicistico. Si potrebbe dire che si dispone di un'applicazione / contenitore Web aziendale con più ambiti di risorse gestite dal contenitore IOC e sono necessari tentativi automatici e reinizializzazione delle risorse dell'ambito della sessione o della richiesta, ecc. La logica di cablaggio sugli ambiti di livello inferiore potrebbe ricevere fabbriche astratte per creare risorse, quindi non essere a conoscenza delle esatte implementazioni. Solo gli ambiti di livello superiore saprebbero davvero quali eccezioni possono generare quelle risorse di livello inferiore. Adesso aspetta!
Sfortunatamente, le eccezioni consentono solo l'indirizzamento indiretto sullo stack di chiamate e diversi ambiti con le loro diverse cardinalità di solito vengono eseguiti su più thread diversi. Non c'è modo di comunicare attraverso questo con eccezioni. Abbiamo bisogno di qualcosa di più potente qui. Risposta: passaggio del messaggio asincrono . Cattura ogni eccezione alla radice dell'ambito di livello inferiore. Non ignorare nulla, non lasciar passare nulla. Ciò chiuderà e eliminerà tutte le risorse create nello stack di chiamate dell'ambito corrente. Quindi propagare i messaggi di errore agli ambiti più in alto utilizzando le code / i canali dei messaggi nella routine di gestione delle eccezioni, fino a raggiungere il livello in cui sono note le concrezioni. Questo è il ragazzo che sa come gestirlo.
SUMMA SUMMARUM
Quindi, secondo la mia interpretazione, catturare in ritardo significa catturare le eccezioni nel posto più conveniente DOVE NON STAI RIPENDENDO ANCORA PIÙ . Non prendere troppo presto! Cattura le eccezioni nel livello in cui crei l'eccezione concreta generando istanze delle astrazioni delle risorse, il livello che conosce le concrezioni delle astrazioni. Il livello "cablaggio".
HTH. Buona programmazione!