Нам довелося вирішити подібну проблему. Ми хотіли взяти потік, який перевищував системну пам’ять (перебираючи всі об’єкти бази даних), і якнайкраще рандомізувати порядок - ми вважали, що було б добре буферувати 10000 елементів і рандомізувати їх.
Ціль була функцією, яка брала потік.
З запропонованих тут рішень існує цілий ряд варіантів:
- Використовуйте різні додаткові бібліотеки без Java
- Почніть з чогось, що не є потоком, наприклад, зі списку довільного доступу
- Майте потік, який можна легко розщепити в сплітераторі
Спочатку наш інстинкт полягав у використанні спеціального колектора, але це означало відмову від потокового передавання. Наведене вище спеціальне колекторне рішення дуже хороше, і ми його майже використали.
Ось рішення, яке обманює, використовуючи той факт, що Stream
s може дати вам рішення, Iterator
яке ви можете використовувати як евакуаційний люк щоб дозволити вам зробити щось додаткове, що потоки не підтримують. Iterator
Перетвориться назад в потік з використанням іншого трохи Java 8 StreamSupport
чаклунства.
/**
* 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());
}
}
}
Простий приклад використання цього може виглядати так:
@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)
// 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);
}
Це виводить щось на зразок (воно рандомізоване, таке різне кожного разу)
A
C
B
E
D
F
Секретний соус тут полягає в тому, що потік завжди є, тож ви можете або оперувати потоком партій, або зробити щось для кожної партії, а потім flatMap
повернути її в потік. Ще краще, все вищезазначене працює лише як остаточне forEach
абоcollect
або інших виразів узгоджувального PULL дані через потік.
Виявляється, iterator
це особливий тип завершувальної операції над потоком і не змушує весь потік запускатися і потрапляти в пам’ять! Дякую хлопцям Java 8 за чудовий дизайн!