Java 8 Stream与批处理


93

我有一个包含项目列表的大文件。

我想创建一批项目,并用这一批发出一个HTTP请求(所有项目都需要作为HTTP请求中的参数)。我可以使用for循环非常轻松地完成此操作,但是作为Java 8爱好者,我想尝试使用Java 8的Stream框架编写此代码(并获得延迟处理的好处)。

例:

List<String> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < data.size(); i++) {
  batch.add(data.get(i));
  if (batch.size() == BATCH_SIZE) process(batch);
}

if (batch.size() > 0) process(batch);

我想做一些事情 lazyFileStream.group(500).map(processBatch).collect(toList())

最好的方法是什么?


抱歉,我不太清楚如何执行分组,但是Files#lines会懒惰地读取文件的内容。
Toby

1
所以您基本上需要一个逆数flatMap(+一个额外的flatMap再次折叠流)?我认为标准库中不存在这样的便捷方法。您要么必须找到一个第三方库,要么要基于分离器和/或发出流的收集器来编写自己的库
the8472年

3
也许您可以Stream.generatereader::readLine和结合使用limit,但是问题是流与Exceptions配合得不好。而且,这可能无法很好地并行化。我认为for循环仍然是最佳选择。
tobias_k 2015年

我刚刚添加了示例代码。我不认为flatMap是要走的路。怀疑我可能必须编写一个自定义的分隔器
Andy Dang

1
我为此类问题创造了“流滥用”一词。
凯尔文2015年

Answers:


13

注意!此解决方案在运行forEach之前读取整个文件。

您可以使用jOOλ来做到这一点,该库可扩展Java 8流以用于单线程,顺序流用例:

Seq.seq(lazyFileStream)              // Seq<String>
   .zipWithIndex()                   // Seq<Tuple2<String, Long>>
   .groupBy(tuple -> tuple.v2 / 500) // Map<Long, List<String>>
   .forEach((index, batch) -> {
       process(batch);
   });

在幕后zipWithIndex()的只是:

static <T> Seq<Tuple2<T, Long>> zipWithIndex(Stream<T> stream) {
    final Iterator<T> it = stream.iterator();

    class ZipWithIndex implements Iterator<Tuple2<T, Long>> {
        long index;

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public Tuple2<T, Long> next() {
            return tuple(it.next(), index++);
        }
    }

    return seq(new ZipWithIndex());
}

...而groupBy()API的便利之处在于:

default <K> Map<K, List<T>> groupBy(Function<? super T, ? extends K> classifier) {
    return collect(Collectors.groupingBy(classifier));
}

(免责声明:我为jOOλ背后的公司工作)


哇。这正是我在寻找的东西。我们的系统正常进程中的数据流序列因此这将是一个不错的选择移动到Java 8
安迪党

16
请注意,此解决方案不必要地将整个输入流存储到中间设备Map(与Ben Manes解决方案不同)
Tagir Valeev 2016年

123

为了完整起见,这是一个番石榴解决方案。

Iterators.partition(stream.iterator(), batchSize).forEachRemaining(this::process);

在问题中,该集合可用,因此不需要流,并且可以将其编写为,

Iterables.partition(data, batchSize).forEach(this::process);

2
这对我来说似乎最容易理解。感谢分享!
格林奇

11
Lists.partition是我应该提到的另一种变化。
本·马内斯

2
这很懒,对吧?它不会Stream在处理相关批处理之前将整个调用到内存中
orirab

1
@orirab是的。批次之间是懒惰的,因为batchSize每次迭代都会消耗元素。
Ben Manes


57

纯Java-8实现也是可行的:

int BATCH = 500;
IntStream.range(0, (data.size()+BATCH-1)/BATCH)
         .mapToObj(i -> data.subList(i*BATCH, Math.min(data.size(), (i+1)*BATCH)))
         .forEach(batch -> process(batch));

请注意,与JOOl不同,它可以并行良好地工作(假设您data是随机访问列表)。


1
如果您的数据实际上是流怎么办?(让我们说文件中的行,甚至是网络中的行)。
Omry Yadan'2

6
@OmryYadan,问题是关于从具有输入List(见data.size()data.get()在这个问题)。我正在回答所问的问题。如果您还有其他问题,请改问(尽管我也已经问过流问题)。
Tagir Valeev

1
如何并行处理批次?
soup_boy

非常创新
Sylvester

36

纯Java 8解决方案

我们可以创建一个自定义收集器来优雅地做到这一点,它使用a batch size和a Consumer处理每个批次:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.Objects.requireNonNull;


/**
 * Collects elements in the stream and calls the supplied batch processor
 * after the configured batch size is reached.
 *
 * In case of a parallel stream, the batch processor may be called with
 * elements less than the batch size.
 *
 * The elements are not kept in memory, and the final result will be an
 * empty list.
 *
 * @param <T> Type of the elements being collected
 */
class BatchCollector<T> implements Collector<T, List<T>, List<T>> {

    private final int batchSize;
    private final Consumer<List<T>> batchProcessor;


    /**
     * Constructs the batch collector
     *
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     */
    BatchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        batchProcessor = requireNonNull(batchProcessor);

        this.batchSize = batchSize;
        this.batchProcessor = batchProcessor;
    }

    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return (ts, t) -> {
            ts.add(t);
            if (ts.size() >= batchSize) {
                batchProcessor.accept(ts);
                ts.clear();
            }
        };
    }

    public BinaryOperator<List<T>> combiner() {
        return (ts, ots) -> {
            // process each parallel list without checking for batch size
            // avoids adding all elements of one to another
            // can be modified if a strict batching mode is required
            batchProcessor.accept(ts);
            batchProcessor.accept(ots);
            return Collections.emptyList();
        };
    }

    public Function<List<T>, List<T>> finisher() {
        return ts -> {
            batchProcessor.accept(ts);
            return Collections.emptyList();
        };
    }

    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

(可选)然后创建一个助手实用程序类:

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collector;

public class StreamUtils {

    /**
     * Creates a new batch collector
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     * @param <T> the type of elements being processed
     * @return a batch collector instance
     */
    public static <T> Collector<T, List<T>, List<T>> batchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        return new BatchCollector<T>(batchSize, batchProcessor);
    }
}

用法示例:

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> output = new ArrayList<>();

int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> output.addAll(xs);

input.stream()
     .collect(StreamUtils.batchCollector(batchSize, batchProcessor));

如果有人想看一下,我也将代码发布在GitHub上:

链接到Github


1
除非无法将流中的所有元素都放入内存,否则这是一个很好的解决方案。同样,它在无限的流上也不起作用-收集方法是终极的,这意味着它会等待直到流完成,然后再批量处理结果,而不是生成批处理流。
Alex Ackerman

2
@AlexAckerman无限的数据流将意味着装订器永远不会被调用,但是仍将调用累加器,因此仍将处理项目。而且,它只需要在任何时间将项目的批处理大小存储在内存中。
Solubris

@Solubris,你是对的!不好意思,谢谢您指出这一点-如果有人对collect方法的工作原理有相同的想法,则我不会删除引用的注释。
Alex Ackerman

发送给使用者的列表应进行复制以使其修改安全,例如:batchProcessor.accept(copyOf(ts))
Solubris

19

我为这种情况编写了一个自定义的Spliterator。它将填充输入流中给定大小的列表。这种方法的优点是它将执行延迟处理,并且可以与其他流功能一起使用。

public static <T> Stream<List<T>> batches(Stream<T> stream, int batchSize) {
    return batchSize <= 0
        ? Stream.of(stream.collect(Collectors.toList()))
        : StreamSupport.stream(new BatchSpliterator<>(stream.spliterator(), batchSize), stream.isParallel());
}

private static class BatchSpliterator<E> implements Spliterator<List<E>> {

    private final Spliterator<E> base;
    private final int batchSize;

    public BatchSpliterator(Spliterator<E> base, int batchSize) {
        this.base = base;
        this.batchSize = batchSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<E>> action) {
        final List<E> batch = new ArrayList<>(batchSize);
        for (int i=0; i < batchSize && base.tryAdvance(batch::add); i++)
            ;
        if (batch.isEmpty())
            return false;
        action.accept(batch);
        return true;
    }

    @Override
    public Spliterator<List<E>> trySplit() {
        if (base.estimateSize() <= batchSize)
            return null;
        final Spliterator<E> splitBase = this.base.trySplit();
        return splitBase == null ? null
                : new BatchSpliterator<>(splitBase, batchSize);
    }

    @Override
    public long estimateSize() {
        final double baseSize = base.estimateSize();
        return baseSize == 0 ? 0
                : (long) Math.ceil(baseSize / (double) batchSize);
    }

    @Override
    public int characteristics() {
        return base.characteristics();
    }

}

真的很有帮助。如果有人想按某些自定义条件进行批处理(例如,以字节为单位的集合大小),则可以委派自定义谓词,并将其作为条件使用在for循环中(然后,imho while循环更易读)

我不确定实施是否正确。例如,如果是基本流,则SUBSIZED从中返回的拆分trySplit可能比拆分之前包含更多的项目(如果拆分发生在批处理的中间)。
麦芽

@Malt如果我的理解Spliterators是正确的,那么trySplit应该始终将数据划分为两个大致相等的部分,以便结果永远不会比原始数据大?
布鲁斯·汉密尔顿

@BruceHamilton不幸的是,根据文档,零件不能大致相等。它们必须相等:if this Spliterator is SUBSIZED, then estimateSize() for this spliterator before splitting must be equal to the sum of estimateSize() for this and the returned Spliterator after splitting.
麦芽

是的,这与我对Spliterator拆分的理解一致。但是,我很难理解“从trySplit返回的拆分可以比拆分之前包含更多的项目”,您能否详细说明一下那里的意思?
布鲁斯·汉密尔顿

13

我们有一个类似的问题要解决。我们想要一个大于系统内存的流(遍历数据库中的所有对象)并尽可能地随机化顺序-我们认为可以缓存10,000个项目并将它们随机化是可以的。

目标是接受流的功能。

在这里提出的解决方案中,似乎有多种选择:

  • 使用各种非Java 8附加库
  • 从不是流的内容开始-例如随机访问列表
  • 拥有可以在拆分器中轻松拆分的流

我们的本能最初是使用自定义收集器,但是这意味着退出流式处理。上面的定制收集器解决方案非常好,我们几乎使用了它。

这是一个解决方案,它利用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其返回到流中。更好的是,上述所有的只运行作为最终forEachcollect或其他终止表达式PULL通过流中的数据。

事实证明,这iterator是在流上终止操作的一种特殊类型,不会导致整个流运行并进入内存!感谢Java 8的出色设计!


而且非常好,您可以在收集每个批次时对每个批次进行完全迭代,并持续使用List-您不能推迟批次内元素的迭代,因为消费者可能希望跳过整个批次,并且如果您不消耗批次,元素,那么它们就不会跳得太远。(尽管实际上要容易
得多

9

您也可以使用RxJava

Observable.from(data).buffer(BATCH_SIZE).forEach((batch) -> process(batch));

要么

Observable.from(lazyFileStream).buffer(500).map((batch) -> process(batch)).toList();

要么

Observable.from(lazyFileStream).buffer(500).map(MyClass::process).toList();

8

您还可以看一下cyclops-react,我是该库的作者。它实现了jOOλ接口(以及扩展的JDK 8 Streams),但是与JDK 8 Parallel Streams不同,它着重于异步操作(例如可能阻塞异步I / O调用)。相比之下,JDK并行流专注于CPU绑定操作的数据并行性。它通过在后台管理基于Future的任务的集合而工作,但是向最终用户提供了标准的扩展Stream API。

此示例代码可以帮助您入门

LazyFutureStream.parallelCommonBuilder()
                .react(data)
                .grouped(BATCH_SIZE)                  
                .map(this::process)
                .run();

这里有一个关于批处理教程

还有一个更通用的教程

要使用自己的线程池(可能更适合于阻塞I / O),可以使用

     LazyReact reactor = new LazyReact(40);

     reactor.react(data)
            .grouped(BATCH_SIZE)                  
            .map(this::process)
            .run();

3

纯Java 8示例也可用于并行流。

如何使用:

Stream<Integer> integerStream = IntStream.range(0, 45).parallel().boxed();
CsStreamUtil.processInBatch(integerStream, 10, batch -> System.out.println("Batch: " + batch));

方法的声明和实现:

public static <ElementType> void processInBatch(Stream<ElementType> stream, int batchSize, Consumer<Collection<ElementType>> batchProcessor)
{
    List<ElementType> newBatch = new ArrayList<>(batchSize);

    stream.forEach(element -> {
        List<ElementType> fullBatch;

        synchronized (newBatch)
        {
            if (newBatch.size() < batchSize)
            {
                newBatch.add(element);
                return;
            }
            else
            {
                fullBatch = new ArrayList<>(newBatch);
                newBatch.clear();
                newBatch.add(element);
            }
        }

        batchProcessor.accept(fullBatch);
    });

    if (newBatch.size() > 0)
        batchProcessor.accept(new ArrayList<>(newBatch));
}

2

公平地说,请看一下优雅的Vavr解决方案:

Stream.ofAll(data).grouped(BATCH_SIZE).forEach(this::process);

1

使用Spliterator的简单示例

    // read file into stream, try-with-resources
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        //skip header
        Spliterator<String> split = stream.skip(1).spliterator();
        Chunker<String> chunker = new Chunker<String>();
        while(true) {              
            boolean more = split.tryAdvance(chunker::doSomething);
            if (!more) {
                break;
            }
        }           
    } catch (IOException e) {
        e.printStackTrace();
    }
}

static class Chunker<T> {
    int ct = 0;
    public void doSomething(T line) {
        System.out.println(ct++ + " " + line.toString());
        if (ct % 100 == 0) {
            System.out.println("====================chunk=====================");               
        }           
    }       
}

布鲁斯的答案比较全面,但是我一直在寻找快速而又肮脏的东西来处理一堆文件。


1

这是一个懒惰的纯Java解决方案。

public static <T> Stream<List<T>> partition(Stream<T> stream, int batchSize){
    List<List<T>> currentBatch = new ArrayList<List<T>>(); //just to make it mutable 
    currentBatch.add(new ArrayList<T>(batchSize));
    return Stream.concat(stream
      .sequential()                   
      .map(new Function<T, List<T>>(){
          public List<T> apply(T t){
              currentBatch.get(0).add(t);
              return currentBatch.get(0).size() == batchSize ? currentBatch.set(0,new ArrayList<>(batchSize)): null;
            }
      }), Stream.generate(()->currentBatch.get(0).isEmpty()?null:currentBatch.get(0))
                .limit(1)
    ).filter(Objects::nonNull);
}

1

您可以使用apache.commons:

ListUtils.partition(ListOfLines, 500).stream()
                .map(partition -> processBatch(partition)
                .collect(Collectors.toList());

分区工作很轻松,但是对列表进行分区后,您可以获得使用流的好处(例如,使用并行流,添加过滤器等)。其他答案提出了更详尽的解决方案,但有时可读性和可维护性更为重要(有时不是:-))


不确定谁投票了,但是很高兴理解为什么。.我给不能回答使用番石榴的人们补充了其他答案
Tal Joffe

您正在此处处理列表,而不是流。
Drakemor

@Drakemor我正在处理子列表流。注意stream()函数调用
Tal Joffe

但是首先,您将其变成子列表列表,这些列表对于真正的流数据将无法正常工作。这是对分区的引用: commons.apache.org/proper/commons-collections/apidocs/org/…–
Drakemor

1
TBH:我不能完全理解你的观点,但是我想我们可以同意不同意。我已经编辑了答案,以反映我们在这里的谈话。感谢您的讨论
Tal Joffe

1

使用Reactor可以轻松完成 :

Flux.fromStream(fileReader.lines().onClose(() -> safeClose(fileReader)))
            .map(line -> someProcessingOfSingleLine(line))
            .buffer(BUFFER_SIZE)
            .subscribe(apiService::makeHttpRequest);

0

使用Java 8com.google.common.collect.Lists,您可以执行以下操作:

public class BatchProcessingUtil {
    public static <T,U> List<U> process(List<T> data, int batchSize, Function<List<T>, List<U>> processFunction) {
        List<List<T>> batches = Lists.partition(data, batchSize);
        return batches.stream()
                .map(processFunction) // Send each batch to the process function
                .flatMap(Collection::stream) // flat results to gather them in 1 stream
                .collect(Collectors.toList());
    }
}

T是输入列表U中项目的类型和输出列表中项目的类型

您可以像这样使用它:

List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
    userKeys,
    10, // Batch Size
    partialKeys -> service.getUsers(partialKeys)
);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.