Tuvimos un problema similar que resolver. Queríamos tomar una secuencia que fuera más grande que la memoria del sistema (iterando a través de todos los objetos en una base de datos) y aleatorizar el orden lo mejor posible; pensamos que estaría bien almacenar en búfer 10,000 elementos y aleatorizarlos.
El objetivo era una función que incluía una secuencia.
De las soluciones propuestas aquí, parece haber una variedad de opciones:
- Utilice varias bibliotecas adicionales que no sean de Java 8
- Comience con algo que no sea una transmisión, por ejemplo, una lista de acceso aleatorio
- Tener una corriente que se pueda dividir fácilmente en un spliterator
Nuestro instinto fue originalmente usar un colector personalizado, pero esto significó dejar de transmitir. La solución de recopilación personalizada anterior es muy buena y casi la usamos.
Aquí hay una solución que engaña al usar el hecho de que Stream
s puede brindarle una Iterator
que puede usar como una trampilla de escape para permitirle hacer algo adicional que las transmisiones no admiten. El Iterator
se convierte de nuevo a una secuencia usando otro poco de StreamSupport
hechicería de 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());
}
}
}
Un ejemplo simple de usar esto se vería así:
@Test
public void getsBatches() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.forEach(System.out::println);
}
Las impresiones de arriba
[A, B, C]
[D, E, F]
Para nuestro caso de uso, queríamos mezclar los lotes y luego mantenerlos como una secuencia; se veía así:
@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);
}
Esto genera algo como (es aleatorio, tan diferente cada vez)
A
C
B
E
D
F
La salsa secreta aquí es que siempre hay un flujo, por lo que puede operar en un flujo de lotes o hacer algo con cada lote y luego flatMap
volver a un flujo. Aún mejor, todo lo anterior sólo se ejecuta como las finales forEach
o collect
expresiones u otros terminación TIRE los datos a través de la corriente.
¡Resulta que iterator
es un tipo especial de operación de terminación en una secuencia y no hace que toda la secuencia se ejecute y llegue a la memoria! ¡Gracias a los chicos de Java 8 por un diseño brillante!