Abbiamo avuto un problema simile da risolvere. Volevamo prendere un flusso che fosse più grande della memoria di sistema (iterando attraverso tutti gli oggetti in un database) e randomizzare l'ordine nel miglior modo possibile - abbiamo pensato che sarebbe stato ok bufferizzare 10.000 elementi e randomizzarli.
L'obiettivo era una funzione che assumeva un flusso.
Tra le soluzioni qui proposte, sembra esserci una serie di opzioni:
- Usa varie librerie aggiuntive non Java 8
- Inizia con qualcosa che non è un flusso, ad esempio un elenco di accesso casuale
- Avere un flusso che può essere diviso facilmente in uno spliterator
Il nostro istinto originariamente era quello di utilizzare un raccoglitore personalizzato, ma questo significava abbandonare lo streaming. La soluzione di raccolta personalizzata sopra è molto buona e l'abbiamo quasi utilizzata.
Ecco una soluzione che imbroglia usando il fatto che Stream
s può darti un Iterator
che puoi usare come un portello di fuga per farti fare qualcosa in più che i flussi non supportano. Il Iterator
è tornato convertito a un flusso utilizzando un altro po 'di Java 8 StreamSupport
stregoneria.
/**
* An iterator which returns batches of items taken from another iterator
*/
public class BatchingIterator<T> implements Iterator<List<T>> {
/**
* Given a stream, convert it to a stream of batches no greater than the
* batchSize.
* @param originalStream to convert
* @param batchSize maximum size of a batch
* @param <T> type of items in the stream
* @return a stream of batches taken sequentially from the original stream
*/
public static <T> Stream<List<T>> batchedStreamOf(Stream<T> originalStream, int batchSize) {
return asStream(new BatchingIterator<>(originalStream.iterator(), batchSize));
}
private static <T> Stream<T> asStream(Iterator<T> iterator) {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator,ORDERED),
false);
}
private int batchSize;
private List<T> currentBatch;
private Iterator<T> sourceIterator;
public BatchingIterator(Iterator<T> sourceIterator, int batchSize) {
this.batchSize = batchSize;
this.sourceIterator = sourceIterator;
}
@Override
public boolean hasNext() {
prepareNextBatch();
return currentBatch!=null && !currentBatch.isEmpty();
}
@Override
public List<T> next() {
return currentBatch;
}
private void prepareNextBatch() {
currentBatch = new ArrayList<>(batchSize);
while (sourceIterator.hasNext() && currentBatch.size() < batchSize) {
currentBatch.add(sourceIterator.next());
}
}
}
Un semplice esempio di utilizzo di questo sarebbe simile a questo:
@Test
public void getsBatches() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.forEach(System.out::println);
}
Le stampe sopra
[A, B, C]
[D, E, F]
Per il nostro caso d'uso, volevamo mescolare i batch e quindi mantenerli come un flusso - sembrava così:
@Test
public void howScramblingCouldBeDone() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
// the lambda in the map expression sucks a bit because Collections.shuffle acts on the list, rather than returning a shuffled one
.map(list -> {
Collections.shuffle(list); return list; })
.flatMap(List::stream)
.forEach(System.out::println);
}
Questo produce qualcosa del tipo (è randomizzato, così diverso ogni volta)
A
C
B
E
D
F
La salsa segreta qui è che c'è sempre un flusso, quindi puoi operare su un flusso di batch o fare qualcosa per ogni batch e poi flatMap
tornare a un flusso. Ancora meglio, tutto quanto sopra viene eseguito solo come finale forEach
o collect
o altre espressioni di terminazione PULL i dati attraverso il flusso.
Si scopre che si iterator
tratta di un tipo speciale di operazione di terminazione su un flusso e non causa l'esecuzione e la memorizzazione dell'intero flusso! Grazie ai ragazzi di Java 8 per un design brillante!