Нам нужно было решить похожую проблему. Мы хотели взять поток, размер которого превышает системную память (итерация по всем объектам в базе данных), и максимально рандомизировать порядок - мы подумали, что можно буферизовать 10 000 элементов и рандомизировать их.
Целью была функция, принимающая поток.
Среди предлагаемых здесь решений есть несколько вариантов:
- Используйте различные дополнительные библиотеки, отличные от java 8
- Начните с чего-то, что не является потоком - например, со списка произвольного доступа.
- Иметь поток, который можно легко разделить в сплитераторе
Изначально нашим инстинктом было использовать собственный сборщик, но это означало, что он будет отключен от потоковой передачи. Приведенное выше решение для кастомного коллектора очень хорошее, и мы его почти использовали.
Вот решение, которое обманывает, используя тот факт, что Stream
s может дать вам, Iterator
который вы можете использовать в качестве аварийного люка, чтобы позволить вам делать что-то еще, что потоки не поддерживают. Iterator
Преобразуется обратно в поток с использованием другого немного Java 8 StreamSupport
колдовства.
public class BatchingIterator<T> implements Iterator<List<T>> {
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());
}
}
}
Простой пример использования этого мог бы выглядеть так:
@Test
public void getsBatches() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.forEach(System.out::println);
}
Приведенные выше отпечатки
[A, B, C]
[D, E, F]
В нашем случае мы хотели перетасовать пакеты, а затем сохранить их в виде потока - это выглядело так:
@Test
public void howScramblingCouldBeDone() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.map(list -> {
Collections.shuffle(list); return list; })
.flatMap(List::stream)
.forEach(System.out::println);
}
Это выводит что-то вроде (оно рандомизировано, поэтому каждый раз разное)
A
C
B
E
D
F
Секрет в том, что всегда есть поток, поэтому вы можете либо работать с потоком пакетов, либо делать что-то с каждым пакетом, а затем flatMap
обратно в поток. Более того, все вышеперечисленное выполняется только как заключительное forEach
или collect
или другое завершающее выражение PULL данные через поток.
Оказывается, iterator
это особый тип завершающей операции над потоком, и он не заставляет весь поток запускаться и поступать в память! Спасибо ребятам из Java 8 за блестящий дизайн!