Sfide in Java 8

1. Panoramica

Java 8 ha introdotto alcune nuove funzionalità, che ruotavano principalmente attorno all'uso di espressioni lambda. In questo rapido articolo, daremo uno sguardo agli aspetti negativi di alcuni di essi.

E, sebbene questo non sia un elenco completo, è una raccolta soggettiva dei reclami più comuni e popolari relativi alle nuove funzionalità di Java 8.

2. Java 8 Stream e pool di thread

Prima di tutto, i flussi paralleli hanno lo scopo di rendere possibile una facile elaborazione parallela di sequenze e questo funziona abbastanza bene per scenari semplici.

Il flusso utilizza il ForkJoinPool predefinito e comune : suddivide le sequenze in blocchi più piccoli ed esegue le operazioni utilizzando più thread.

Tuttavia, c'è un problema. Non esiste un buon modo per specificare quale ForkJoinPool utilizzare e quindi, se uno dei thread si blocca, tutti gli altri, utilizzando il pool condiviso, dovranno attendere il completamento delle attività a lunga esecuzione.

Fortunatamente, c'è una soluzione alternativa per questo:

ForkJoinPool forkJoinPool = new ForkJoinPool(2); forkJoinPool.submit(() -> /*some parallel stream pipeline */) .get();

Questo creerà un nuovo ForkJoinPool separato e tutte le attività generate dal flusso parallelo utilizzeranno il pool specificato e non quello condiviso, predefinito.

Vale la pena notare che c'è un altro potenziale problema : "questa tecnica di inviare un'attività a un pool di fork-join, per eseguire il flusso parallelo in quel pool è un 'trucco' di implementazione e non è garantito che funzioni" , secondo Stuart Marks - Sviluppatore Java e OpenJDK di Oracle. Una sfumatura importante da tenere a mente quando si utilizza questa tecnica.

3. Diminuzione del debuggability

Il nuovo stile di codifica semplifica il nostro codice sorgente, ma può causare mal di testa durante il debug .

Prima di tutto, diamo un'occhiata a questo semplice esempio:

public static int getLength(String input) { if (StringUtils.isEmpty(input) { throw new IllegalArgumentException(); } return input.length(); } List lengths = new ArrayList(); for (String name : Arrays.asList(args)) { lengths.add(getLength(name)); }

Questo è un codice Java imperativo standard che si spiega da sé.

Se passiamo String vuota come input, di conseguenza, il codice genererà un'eccezione e nella console di debug possiamo vedere:

at LmbdaMain.getLength(LmbdaMain.java:19) at LmbdaMain.main(LmbdaMain.java:34)

Ora, riscriviamo lo stesso codice utilizzando l'API Stream e vediamo cosa succede quando viene passata una stringa vuota :

Stream lengths = names.stream() .map(name -> getLength(name));

Lo stack di chiamate sarà simile a:

at LmbdaMain.getLength(LmbdaMain.java:19) at LmbdaMain.lambda$0(LmbdaMain.java:37) at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.LongPipeline.reduce(LongPipeline.java:438) at java.util.stream.LongPipeline.sum(LongPipeline.java:396) at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526) at LmbdaMain.main(LmbdaMain.java:39)

Questo è il prezzo che paghiamo per sfruttare più livelli di astrazione nel nostro codice. Tuttavia, gli IDE hanno già sviluppato strumenti solidi per il debug di Java Stream.

4. Metodi che restituiscono null o facoltativo

Facoltativo è stato introdotto in Java 8 per fornire un modo indipendente dai tipi di esprimere opzionalità.

Facoltativo , indica esplicitamente che il valore restituito potrebbe non essere presente. Pertanto, la chiamata di un metodo può restituire un valore e viene utilizzato Optional per racchiudere quel valore all'interno, il che si è rivelato utile.

Sfortunatamente, a causa della compatibilità con le versioni precedenti di Java, a volte ci siamo ritrovati con le API Java che combinano due diverse convenzioni. Nella stessa classe, possiamo trovare metodi che restituiscono null e metodi che restituiscono Optionals.

5. Troppe interfacce funzionali

Nel pacchetto java.util.function , abbiamo una raccolta di tipi di destinazione per le espressioni lambda. Possiamo distinguerli e raggrupparli come:

  • Consumatore : rappresenta un'operazione che accetta alcuni argomenti e non restituisce alcun risultato
  • Funzione : rappresenta una funzione che accetta alcuni argomenti e produce un risultato
  • Operatore : rappresenta un'operazione su alcuni argomenti di tipo e restituisce un risultato dello stesso tipo degli operandi
  • Predicato : rappresenta un predicato ( funzione con valore booleano ) di alcuni argomenti
  • Fornitore : rappresenta un fornitore che non accetta argomenti e restituisce risultati

Inoltre, abbiamo tipi aggiuntivi per lavorare con le primitive:

  • IntConsumer
  • IntFunction
  • IntPredicate
  • IntSupplier
  • IntToDoubleFunction
  • IntToLongFunction
  • ... e le stesse alternative per le lunghe e le doppie

Inoltre, tipi speciali per funzioni con l'arità di 2:

  • BiConsumer
  • BiPredicate
  • BinaryOperator
  • BiFunction

Di conseguenza, l'intero pacchetto contiene 44 tipi funzionali, che possono certamente iniziare a creare confusione.

6. Eccezioni verificate ed espressioni Lambda

Le eccezioni verificate sono state una questione problematica e controversa già prima di Java 8. Dall'arrivo di Java 8, è sorto il nuovo problema.

Le eccezioni verificate devono essere rilevate immediatamente o dichiarate. Poiché le interfacce funzionali java.util.function non dichiarano la generazione di eccezioni, il codice che genera un'eccezione controllata fallirà durante la compilazione:

static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));

Un modo per superare questo problema è racchiudere l'eccezione controllata in un blocco try-catch e rilanciare RuntimeException :

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

Questo funzionerà. Tuttavia, la generazione di RuntimeException contraddice lo scopo dell'eccezione verificata e rende l'intero codice avvolto con codice boilerplate, che stiamo cercando di ridurre sfruttando le espressioni lambda. Una delle soluzioni hacker è fare affidamento sull'hack dei lanci subdoli.

Un'altra soluzione è scrivere un'interfaccia funzionale del consumatore, che può generare un'eccezione:

@FunctionalInterface public interface ThrowingConsumer { void accept(T t) throws E; }
static  Consumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }

Sfortunatamente, stiamo ancora racchiudendo l'eccezione selezionata in un'eccezione di runtime.

Infine, per una soluzione e una spiegazione approfondite del problema, possiamo esplorare il seguente approfondimento: Eccezioni in Java 8 Lambda Expressions.

8 . Conclusione

In questo breve articolo, abbiamo discusso alcuni degli aspetti negativi di Java 8.

Sebbene alcune di esse siano state scelte progettuali deliberate fatte da architetti del linguaggio Java e in molti casi esiste una soluzione alternativa o alternativa; dobbiamo essere consapevoli dei loro possibili problemi e limiti.