Нам довелося вирішити подібну проблему. Ми хотіли взяти потік, який перевищував системну пам’ять (перебираючи всі об’єкти бази даних), і якнайкраще рандомізувати порядок - ми вважали, що було б добре буферувати 10000 елементів і рандомізувати їх.
Ціль була функцією, яка брала потік.
З запропонованих тут рішень існує цілий ряд варіантів:
- Використовуйте різні додаткові бібліотеки без Java
- Почніть з чогось, що не є потоком, наприклад, зі списку довільного доступу
- Майте потік, який можна легко розщепити в сплітераторі
Спочатку наш інстинкт полягав у використанні спеціального колектора, але це означало відмову від потокового передавання. Наведене вище спеціальне колекторне рішення дуже хороше, і ми його майже використали.
Ось рішення, яке обманює, використовуючи той факт, що Streams може дати вам рішення, 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 за чудовий дизайн!