2014-06-19 14 views
70

Ho difficoltà a comprendere appieno il ruolo che il combiner soddisfa nel metodo Stream reduce.Perché è necessario un combinatore per ridurre il metodo che converte il tipo in java 8

Ad esempio, il seguente codice doesnt compilazione:

int length = asList("str1", "str2").stream() 
      .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length()); 

errore di compilazione dice: (argomento disadattamento; int non può essere convertito in java.lang.String)

ma questo codice fa compile:

int length = asList("str1", "str2").stream() 
    .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length(), 
       (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); 

Comprendo che il metodo di combinazione è utilizzato in flussi paralleli - quindi nel mio esempio si sommano due valori intermedi accumulati.

Ma non capisco perché il primo esempio non si compili senza il combinatore o come il combinatore risolva la conversione di stringa in int poiché si sommano solo due interi.

Qualcuno può far luce su questo?

+0

domanda correlata: https://stackoverflow.com/questions/24202473/does-a-sequential-stream-in-java-8-use-the-combiner-parameter-on-calling-collect – nosid

+1

aha, è per flussi paralleli ... Io chiamo astrazione leaky! – Andy

risposta

45

Le versioni a due e tre argomenti di reduce che si è tentato di utilizzare non accettano lo stesso tipo per accumulator.

I due argomenti reduce è defined as:

T reduce(T identity, 
     BinaryOperator<T> accumulator) 

Nel tuo caso, T è String, quindi BinaryOperator<T> dovrebbe accettare due argomenti stringa e restituire una stringa. Ma si passa ad esso un int e una stringa, che si traduce nell'errore di compilazione che hai ottenuto - argument mismatch; int cannot be converted to java.lang.String. In realtà, penso che passare 0 come valore di identità sia sbagliato qui, dal momento che una stringa è prevista (T).

Si noti inoltre che questa versione di riduzione elabora un flusso di Ts e restituisce una T, quindi non è possibile utilizzarlo per ridurre un flusso di String a un int.

I tre tesi reduce è defined as:

<U> U reduce(U identity, 
      BiFunction<U,? super T,U> accumulator, 
      BinaryOperator<U> combiner) 

Nel caso U è intero e T è String, quindi questo metodo ridurrà un flusso di stringa in un intero.

Per l'accumulatore BiFunction<U,? super T,U> è possibile passare parametri di due tipi diversi (U e? Super T), che nel caso sono numeri interi e stringa. Inoltre, il valore di identità U accetta un intero nel tuo caso, quindi passarlo 0 va bene.

Un altro modo per ottenere ciò che si vuole:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) 
      .reduce(0, (accumulatedInt, len) -> accumulatedInt + len); 

Qui il tipo di flusso corrisponde al tipo di ritorno di reduce, in modo da poter utilizzare la versione a due parametri di reduce.

Naturalmente non c'è bisogno di usare reduce affatto:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) 
      .sum(); 
+7

Come seconda opzione del tuo ultimo codice, potresti anche usare 'mapToInt (String :: length)' su 'mapToInt (s -> s.length())', non sono sicuro se uno sarebbe meglio dell'altro, ma Preferisco il primo per la leggibilità. – skiwi

+2

Molti troveranno questa risposta in quanto non capiscono perché è necessario il 'combinatore ', perché non è sufficiente avere l'accumulatore. In tal caso: il combinatore è necessario solo per flussi paralleli, per combinare i risultati "accumulati" dei fili. – ddekany

109

Eran's answer descritte le differenze tra i due-arg e le versioni a tre-Arg di reduce in quanto il primo riduce Stream<T>-T mentre il quest'ultimo riduce Stream<T> a U. Tuttavia, non ha effettivamente spiegato la necessità della funzione combinatore aggiuntiva quando si riduce Stream<T> a U.

Uno dei principi di progettazione dell'API di Streams è che l'API non deve differire tra flussi sequenziali e paralleli o, in altri termini, una particolare API non dovrebbe impedire il corretto svolgimento di uno stream in sequenza o in parallelo. Se i tuoi lambda hanno le proprietà giuste (associative, non interferenti, ecc.), Un flusso eseguito sequenzialmente o in parallelo dovrebbe dare gli stessi risultati.

T reduce(I, (T, T) -> T) 

L'implementazione sequenziale è semplice:

di prima prendere in considerazione la versione a due-arg della riduzione Let. Il valore identitario I viene "accumulato" con l'elemento zeroth stream per fornire un risultato. Questo risultato viene accumulato con il primo elemento stream per fornire un altro risultato, che a sua volta viene accumulato con il secondo elemento stream e così via. Dopo che l'ultimo elemento è stato accumulato, viene restituito il risultato finale.

L'implementazione parallela inizia dividendo il flusso in segmenti. Ogni segmento viene elaborato dal proprio thread nel modo sequenziale descritto sopra. Ora, se abbiamo thread N, abbiamo N risultati intermedi. Questi devono essere ridotti a un solo risultato. Poiché ogni risultato intermedio è di tipo T, e ne abbiamo diversi, possiamo usare la stessa funzione di accumulatore per ridurre quei risultati intermedi N a un unico risultato.

Ora consideriamo un'ipotetica operazione di riduzione a due arg che riduce Stream<T> a U. In altre lingue questa operazione viene chiamata "fold" o "fold-left" quindi è quello che chiamerò qui. Nota questo non esiste in Java.

U foldLeft(I, (U, T) -> U) 

(Si noti che il valore di identità I è di tipo U.)

La versione sequenziale di foldLeft è proprio come la versione sequenziale di reduce tranne che i valori intermedi sono di tipo U invece di tipo T Ma è altrimenti lo stesso. (Un'ipotetica operazione foldRight sarebbe simile eccetto che le operazioni verrebbero eseguite da destra a sinistra anziché da sinistra a destra.)

Consideriamo ora la versione parallela di foldLeft. Iniziamo dividendo il flusso in segmenti. Possiamo quindi fare in modo che ciascuno dei thread N riduca i valori T nel suo segmento in N valori intermedi di tipo U. Ora che cosa? Come possiamo ottenere da N i valori di tipo U fino a un singolo risultato di tipo U?

Nei manca è un'altra funzione che combina molteplici risultati intermedi di tipo U in un unico risultato di tipo U. Se abbiamo una funzione che combina due valori U in uno, che è sufficiente a ridurre qualsiasi numero di valori giù a uno - proprio come la riduzione originale sopra.Così, l'operazione di riduzione che dà un risultato di tipo diverso ha bisogno di due funzioni:

U reduce(I, (U, T) -> U, (U, U) -> U) 

Oppure, utilizzando la sintassi Java:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) 

In sintesi, per fare parallela riduzione a un diverso tipo di risultato, abbiamo bisogno di due funzioni: una che accumula elementi T in valori U intermedi e un secondo che combina i valori intermedi U in un singolo risultato U. Se non stiamo cambiando i tipi, risulta che la funzione dell'accumulatore è la stessa della funzione combinatore. Ecco perché la riduzione allo stesso tipo ha solo la funzione di accumulatore e la riduzione ad un tipo diverso richiede funzioni separate di accumulatore e combinatore.

Infine, Java non fornisce operazioni foldLeft e foldRight perché implicano un particolare ordine di operazioni che è intrinsecamente sequenziale. Ciò si scontra con il principio di progettazione sopra riportato di fornire API che supportano allo stesso modo operazioni sequenziali e parallele.

+4

plus1 Questo rende le cose molto chiare, grazie! – naikus

+6

Quindi cosa si può fare se è necessario un 'foldLeft' perché il calcolo dipende dal risultato precedente e non può essere parallelizzato? – amoebe

+2

@amoebe È possibile implementare il proprio foldLeft usando 'forEachOrdered'. Lo stato intermedio deve essere mantenuto in una variabile catturata, però. –

0

Non c'è ridurre versione che prende due tipi diversi senza un combinatore dal momento che non può essere eseguita in parallelo (non so perché questo è un requisito). Il fatto che accumulatore deve essere associativa rende questa interfaccia praticamente inutile dal momento che:

list.stream().reduce(identity, 
        accumulator, 
        combiner); 

produce gli stessi risultati come:

list.stream().map(i -> accumulator(identity, i)) 
      .reduce(identity, 
        combiner); 
+0

Un simile trucco di "mappa" a seconda di particolari 'accumulator' e' combiner' potrebbe rallentare le cose. –

+0

Oppure, velocizzalo notevolmente poiché ora puoi semplificare 'accumulator' lasciando cadere il primo parametro. – quiz123

+0

La riduzione parallela è possibile, dipende dal calcolo. Nel tuo caso, devi essere consapevole della complessità del combinatore ma anche dell'accumulatore sull'identità rispetto ad altre istanze. – LoganMzz

57

Visto che mi piace scarabocchi e frecce per chiarire i concetti ... facciamo inizio!

da stringa a stringa (flusso sequenziale)

Supponiamo avendo 4 stringhe: il vostro obiettivo è quello di concatenare le tali stringhe in una sola. Fondamentalmente inizi con un tipo e finisci con lo stesso tipo.

È possibile raggiungere questo obiettivo con

String res = Arrays.asList("one", "two","three","four") 
     .stream() 
     .reduce("", 
       (accumulatedStr, str) -> accumulatedStr + str); //accumulator 

e questo aiuta di visualizzare ciò che sta accadendo:

enter image description here

La funzione accumulatore converte, passo dopo passo, gli elementi nella vostra (rosso) flusso al valore finale ridotto (verde). La funzione accumulatore trasforma semplicemente un oggetto String in un altro String.

da stringa a int (flusso parallelo)

Supponiamo aventi le stesse 4 corde: il nuovo obiettivo è quello di riassumere le loro lunghezze, e si desidera parallelizzare il vostro flusso.

Quello che vi serve è qualcosa di simile:

int length = Arrays.asList("one", "two","three","four") 
     .parallelStream() 
     .reduce(0, 
       (accumulatedInt, str) -> accumulatedInt + str.length(),     //accumulator 
       (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner 

e questo è uno schema di quello che sta succedendo

enter image description here

Qui la funzione di accumulatore (un BiFunction) consente di trasformare il vostro String dati a un dato int. Essendo il flusso parallelo, è diviso in due parti (rosse), ognuna delle quali viene elaborata indipendentemente l'una dall'altra e produce altrettanti risultati parziali (arancioni). La definizione di un combinatore è necessaria per fornire una regola per l'unione dei risultati parziali int nello stato finale (verde) int.

da stringa a int (flusso sequenziale)

Che cosa succede se non si vuole per parallelizzare il vostro flusso? Bene, un combinatore deve essere fornito comunque, ma non sarà mai invocato, dato che non verranno prodotti risultati parziali.

+2

Grazie per questo. Non avevo nemmeno bisogno di leggere. Mi auguro che avrebbero appena aggiunto una funzione di piega pazzesca. –

+0

@LodewijkBogaards lieto che abbia aiutato! [JavaDoc] (https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-U-java.util.function.BiFunction-java.util.function. BinaryOperator-) qui è piuttosto criptico –

+0

Mi è piaciuta la tua spiegazione .. Grazie .. – AnkitRox

Problemi correlati