是否有一种优雅的方式来处理块中的流?

时间:2014-12-20 19:33:09

标签: java java-8 java-stream chunking

我的确切方案是批量插入数据库,所以我想累积DOM对象然后每1000个,刷新它们。

我通过将代码放入累加器来检测丰满度然后刷新来实现它,但这似乎是错误的 - 刷新控件应该来自调用者。

我可以将流转换为List然后以迭代方式使用subList,但这看起来也很笨重。

有一个简洁的方法来处理每n个元素然后继续流,而只处理流一次?

8 个答案:

答案 0 :(得分:8)

优雅是旁观者的眼睛。如果您不介意在groupingBy中使用有状态函数,则可以执行以下操作:

AtomicInteger counter = new AtomicInteger();

stream.collect(groupingBy(x->counter.getAndIncrement()/chunkSize))
    .values()
    .forEach(database::flushChunk);

这不会赢得原始解决方案的任何性能或内存使用点,因为它在执行任何操作之前仍会实现整个流。

如果您想避免实现列表,流API将无法帮助您。你必须得到流的迭代器或分裂器,并做这样的事情:

Spliterator<Integer> split = stream.spliterator();
int chunkSize = 1000;

while(true) {
    List<Integer> chunk = new ArrayList<>(size);
    for (int i = 0; i < chunkSize && split.tryAdvance(chunk::add); i++){};
    if (chunk.isEmpty()) break;
    database.flushChunk(chunk);
}

答案 1 :(得分:5)

使用库StreamEx解决方案看起来像

Stream<Integer> stream = IntStream.iterate(0, i -> i + 1).boxed().limit(15);
AtomicInteger counter = new AtomicInteger(0);
int chunkSize = 4;

StreamEx.of(stream)
        .groupRuns((prev, next) -> counter.incrementAndGet() % chunkSize != 0)
        .forEach(chunk -> System.out.println(chunk));

输出:

[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
[12, 13, 14]

groupRuns接受谓词,决定2个元素是否应该在同一个组中。

一旦找到第一个不属于它的元素,就会生成一个组。

答案 2 :(得分:4)

如果你的项目有guava依赖,你可以这样做:

StreamSupport.stream(Iterables.partition(simpleList, 1000).spliterator(), false).forEach(...);

请参阅https://google.github.io/guava/releases/23.0/api/docs/com/google/common/collect/Lists.html#partition-java.util.List-int-

答案 3 :(得分:3)

您可以通过<创建项目 块大小List<T>) / p>

  • 按块索引(元素索引/块大小)
  • 对项目进行分组
  • 按照索引排序块
  • 仅将地图缩小为有序元素

代码:

public static <T> Stream<List<T>> chunked(Stream<T> stream, int chunkSize) {
    AtomicInteger index = new AtomicInteger(0);

    return stream.collect(Collectors.groupingBy(x -> index.getAndIncrement() / chunkSize))
            .entrySet().stream()
            .sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue);
}

使用示例:

Stream<Integer> stream = IntStream.range(0, 100).mapToObj(Integer::valueOf);
Stream<List<Integer>> chunked = chunked(stream, 8);

答案 4 :(得分:1)

正如Misha正确地说的那样,优雅是旁观者的眼睛。我个人认为一个优雅的解决方案是让插入数据库的类执行此任务。与BufferedWriter类似。这样,它不依赖于您的原始数据结构,甚至可以在多个流之后使用。我不确定这是否正是您在累加器中使用您认为错误的代码的意思。我不认为这是错误的,因为像BufferedWriter这样的现有类是这样的。你可以通过这种方式在调用者处获得一些刷新控制,只需在编写器上调用flush()

类似下面的代码。

class BufferedDatabaseWriter implements Flushable {
    List<DomObject> buffer = new LinkedList<DomObject>();
    public void write(DomObject o) {
        buffer.add(o);
        if(buffer.length > 1000)
            flush();
    }
    public void flush() {
        //write buffer to database and clear it
    }
}

现在您的流处理如下:

BufferedDatabaseWriter writer = new BufferedDatabaseWriter();
stream.forEach(o -> writer.write(o));
//if you have more streams stream2.forEach(o -> writer.write(o));
writer.flush();

如果要使用多线程,可以运行flush异步。从流中获取不能同时进行,但我认为无论如何都有办法从流并行计算1000个元素。

您还可以扩展编写器以允许在构造函数中设置缓冲区大小,或者您可以使其实现AutoCloseable并使用ressources等尝试运行它。来自BufferedWriter的好东西。

答案 5 :(得分:1)

看起来像不,导致创建块意味着减少流,而减少意味着终止。如果您需要保持流的性质并在不收集所有数据的情况下处理数据块,请在此处给出我的代码(不适用于并行流):

private static <T> BinaryOperator<List<T>> processChunks(Consumer<List<T>> consumer, int chunkSize) {
    return (data, element) -> {
        if (data.size() < chunkSize) {
            data.addAll(element);
            return data;
        } else {
            consumer.accept(data);
            return element; // in fact it's new data list
        }
    };
}

private static <T> Function<T, List<T>> createList(int chunkSize) {
    AtomicInteger limiter = new AtomicInteger(0);
    return element -> {
        limiter.incrementAndGet();
        if (limiter.get() == 1) {
            ArrayList<T> list = new ArrayList<>(chunkSize);
            list.add(element);
            return list;
        } else if (limiter.get() == chunkSize) {
            limiter.set(0);
        }
        return Collections.singletonList(element);
    };
}

以及使用方法

Consumer<List<Integer>> chunkProcessor = (list) -> list.forEach(System.out::println);

    int chunkSize = 3;

    Stream.generate(StrTokenizer::getInt).limit(13)
            .map(createList(chunkSize))
            .reduce(processChunks(chunkProcessor, chunkSize))
            .ifPresent(chunkProcessor);

static Integer i = 0;

static Integer getInt()
{
    System.out.println("next");
    return i++;
}

它将打印

  

下一个   下一个   下一个   下一个   0   1个   2   下一个   下一个   下一个   3   4   5   下一个   下一个   下一个   6   7   8   下一个   下一个   下一个   9   10   11   12

背后的想法是使用“模式”在地图操作中创建列表

  

[1 ,,],[2],[3],[4 ,, ......

并使用reduce进行合并(+处理)。

  

[1,2,3],[4,5,6],...

并且不要忘记使用

处理最后一个“修剪过的”块
.ifPresent(chunkProcessor);

答案 6 :(得分:0)

上面的大多数答案都没有利用流的好处,例如节省内存。您可以尝试使用迭代器解决问题

Stream<List<T>> chunk(Stream<T> stream, int size) {
  Iterator<T> iterator = stream.iterator();
  Iterator<List<T>> listIterator = new Iterator<>() {

    public boolean hasNext() {
      return iterator.hasNext();
    }

    public List<T> next() {
      List<T> result = new ArrayList<>(size);
      for (int i = 0; i < size && iterator.hasNext(); i++) {
        result.add(iterator.next());
      }
      return result;
    }
  };
  return StreamSupport.stream(((Iterable<List<T>>) () -> listIterator).spliterator(), false);
}

答案 7 :(得分:0)

你可以使用这个类,https://github.com/1wpro2/jdk-patch/blob/main/FixedSizeSpliterator.java

传入块大小作为 THRESHOLD

new FixedSizeSpliterator(T[] values, int threshold)