Tínhamos um problema semelhante para resolver. Queríamos pegar um stream que fosse maior do que a memória do sistema (iterando por todos os objetos em um banco de dados) e randomizar a ordem da melhor maneira possível - pensamos que não haveria problema em armazenar 10.000 itens em buffer e randomizá-los.
O alvo era uma função que incorporava um fluxo.
Das soluções propostas aqui, parece haver uma gama de opções:
- Use várias bibliotecas adicionais não Java 8
- Comece com algo que não seja um stream - por exemplo, uma lista de acesso aleatório
- Tem um fluxo que pode ser facilmente dividido em um divisor
Nosso instinto era originalmente usar um coletor personalizado, mas isso significava sair do streaming. A solução de coletor personalizado acima é muito boa e quase a usamos.
Aqui está uma solução que engana, usando o fato de que Stream
s pode fornecer um Iterator
que você pode usar como uma saída de emergência para permitir que você faça algo extra que os fluxos não suportam. O Iterator
é convertido de volta para um fluxo usando outro bit de StreamSupport
feitiçaria Java 8 .
/**
* 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());
}
}
}
Um exemplo simples de uso seria assim:
@Test
public void getsBatches() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.forEach(System.out::println);
}
As estampas acima
[A, B, C]
[D, E, F]
Para nosso caso de uso, queríamos embaralhar os lotes e depois mantê-los como um fluxo - parecia com isto:
@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);
}
Isso produz algo como (é aleatório, tão diferente a cada vez)
A
C
B
E
D
F
O segredo aqui é que sempre há um fluxo, então você pode operar em um fluxo de lotes ou fazer alguma coisa com cada lote e depois flatMap
voltar para um fluxo. Mesmo melhor, todos os itens acima só funciona como a final forEach
ou collect
ou outras expressões de terminação PULL os dados através da corrente.
Acontece que iterator
é um tipo especial de operação de término em um fluxo e não faz com que todo o fluxo seja executado e entre na memória! Obrigado ao pessoal do Java 8 pelo design brilhante!