Eccezioni in Java 8 Lambda Expressions

1. Panoramica

In Java 8, Lambda Expressions ha iniziato a facilitare la programmazione funzionale fornendo un modo conciso per esprimere il comportamento. Tuttavia, le interfacce funzionali fornite dal JDK non gestiscono molto bene le eccezioni e il codice diventa prolisso e macchinoso quando si tratta di gestirle.

In questo articolo, esploreremo alcuni modi per gestire le eccezioni durante la scrittura di espressioni lambda.

2. Gestione delle eccezioni non controllate

Per prima cosa, capiamo il problema con un esempio.

Abbiamo una lista e vogliamo dividere una costante, diciamo 50 con ogni elemento di questa lista e stampare i risultati:

List integers = Arrays.asList(3, 9, 7, 6, 10, 20); integers.forEach(i -> System.out.println(50 / i));

Questa espressione funziona ma c'è un problema. Se uno qualsiasi degli elementi nell'elenco è 0 , otteniamo un'eccezione ArithmeticException: / per zero . Risolviamolo utilizzando un tradizionale blocco try-catch in modo da registrare qualsiasi eccezione di questo tipo e continuare l'esecuzione per gli elementi successivi:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { System.out.println(50 / i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } });

L'uso di try-catch risolve il problema, ma la concisione di un'espressione Lambda si perde e non è più una piccola funzione come dovrebbe essere.

Per affrontare questo problema, possiamo scrivere un wrapper lambda per la funzione lambda . Diamo un'occhiata al codice per vedere come funziona:

static Consumer lambdaWrapper(Consumer consumer) { return i -> { try { consumer.accept(i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

All'inizio, abbiamo scritto un metodo wrapper che sarà responsabile della gestione dell'eccezione e quindi passato l'espressione lambda come parametro a questo metodo.

Il metodo wrapper funziona come previsto, ma potresti obiettare che sostanzialmente rimuove il blocco try-catch dall'espressione lambda e lo sposta su un altro metodo e non riduce il numero effettivo di righe di codice scritte.

Questo è vero in questo caso in cui il wrapper è specifico per un caso d'uso particolare, ma possiamo fare uso di generici per migliorare questo metodo e usarlo per una varietà di altri scenari:

static  Consumer consumerWrapper(Consumer consumer, Class clazz) { return i -> { try { consumer.accept(i); } catch (Exception ex) { try { E exCast = clazz.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw ex; } } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach( consumerWrapper( i -> System.out.println(50 / i), ArithmeticException.class));

Come possiamo vedere, questa iterazione del nostro metodo wrapper accetta due argomenti, l'espressione lambda e il tipo di eccezione da catturare. Questo wrapper lambda è in grado di gestire tutti i tipi di dati, non solo numeri interi , e catturare qualsiasi tipo specifico di eccezione e non l' eccezione della superclasse .

Inoltre, nota che abbiamo cambiato il nome del metodo da lambdaWrapper a consumerWrapper . È perché questo metodo gestisce solo le espressioni lambda per l' interfaccia funzionale di tipo Consumer . Possiamo scrivere metodi wrapper simili per altre interfacce funzionali come Function , BiFunction , BiConsumer e così via.

3. Gestione delle eccezioni verificate

Modifichiamo l'esempio della sezione precedente e invece di stampare sulla console, scriviamo su un file.

static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }

Si noti che il metodo sopra può generare l' IOException.

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));

Alla compilazione, otteniamo l'errore:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Poiché IOException è un'eccezione controllata, dobbiamo gestirla in modo esplicito . Abbiamo due opzioni.

Primo, possiamo semplicemente lanciare l'eccezione al di fuori del nostro metodo e occuparci di essa da qualche altra parte.

In alternativa, possiamo gestirlo all'interno del metodo che utilizza un'espressione lambda.

Esploriamo entrambe le opzioni.

3.1. Generazione di eccezioni selezionate dalle espressioni Lambda

Vediamo cosa succede quando dichiariamo l' IOException sul metodo main :

public static void main(String[] args) throws IOException { List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i)); }

Tuttavia, durante la compilazione otteniamo lo stesso errore di IOException non gestita .

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Questo perché le espressioni lambda sono simili alle classi interne anonime.

Nel nostro caso, il metodo writeToFile è l'implementazione dell'interfaccia funzionale Consumer .

Diamo uno sguardo alla definizione del consumatore :

@FunctionalInterface public interface Consumer { void accept(T t); }

Come possiamo vedere, il metodo accept non dichiara alcuna eccezione selezionata. Questo è il motivo per cui writeToFile non è autorizzato a lanciare IOException.

Il modo più semplice sarebbe usare un blocco try-catch , racchiudere l'eccezione selezionata in un'eccezione non controllata e rilanciarla:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { writeToFile(i); } catch (IOException e) { throw new RuntimeException(e); } }); 

Questo ottiene il codice da compilare ed eseguire. Tuttavia, questo approccio introduce lo stesso problema che abbiamo già discusso nella sezione precedente: è prolisso e scomodo.

Possiamo andare meglio di così.

Creiamo un'interfaccia funzionale personalizzata con un unico metodo di accettazione che genera un'eccezione.

@FunctionalInterface public interface ThrowingConsumer { void accept(T t) throws E; }

E ora, implementiamo un metodo wrapper in grado di rilanciare l'eccezione:

static  Consumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }

Infine, siamo in grado di semplificare il modo in cui utilizziamo il metodo writeToFile :

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

Questa è ancora una sorta di soluzione alternativa, ma il risultato finale sembra abbastanza pulito ed è decisamente più facile da mantenere .

Entrambi, ThrowingConsumer e throwingConsumerWrapper sono generici e possono essere facilmente riutilizzati in punti diversi della nostra applicazione.

3.2. Gestione di un'eccezione selezionata nell'espressione Lambda

In questa sezione finale, modificheremo il wrapper per gestire le eccezioni controllate.

Poiché la nostra interfaccia ThrowingConsumer utilizza generici, possiamo gestire facilmente qualsiasi eccezione specifica.

static  Consumer handlingConsumerWrapper( ThrowingConsumer throwingConsumer, Class exceptionClass) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { try { E exCast = exceptionClass.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw new RuntimeException(ex); } } }; }

Vediamo come usarlo in pratica:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(handlingConsumerWrapper( i -> writeToFile(i), IOException.class));

Si noti che il codice precedente gestisce solo IOException, mentre qualsiasi altro tipo di eccezione viene rilanciato come RuntimeException .

4. Conclusione

In questo articolo, abbiamo mostrato come gestire un'eccezione specifica nell'espressione lambda senza perdere la concisione con l'aiuto dei metodi wrapper. Abbiamo anche imparato come scrivere alternative di lancio per le interfacce funzionali presenti in JDK per generare o gestire un'eccezione controllata.

Un altro modo sarebbe esplorare l'hack dei lanci subdoli.

Il codice sorgente completo dell'interfaccia funzionale e dei metodi wrapper può essere scaricato da qui e le classi di test da qui, su Github.

Se stai cercando soluzioni di lavoro pronte all'uso, vale la pena dare un'occhiata al progetto ThrowingFunction.