从一个长流创建流的流

时间:2015-06-24 08:43:03

标签: java java-8 java-stream

我想根据Stream的内容将单个Stream拆分为Streams Streams。结果Stream应该包含原始流的一部分'数据。

我真正的应用程序更复杂(它将日志行分组在时间间隔列表中),但我的问题是如何处理流,所以在这里我要问一个简化的例子。

示例问题

我希望能够根据重复的相同数字将Stream<Integer>拆分为Stream<Stream<Integer>>,只留下奇数的流。

例如,以下流包含:

{1,1,1,2,2,2,3,6,7,7,1,1}

需要产生包含以下内容的流流:

{{1,1,1},{3},{7,7},{1,1}}

通过使用过滤器开始(或结束),我可以做出偶数:

Stream<Integer> input = ...;
Straem<Stream<Integer>> output = input.filter(this::isOdd).someOtherOperation();

这是不受欢迎的,因为它意味着每次评估每个输入值,这是可以接受的,但我宁愿避免这种情况。

解决方案的想法

我当前的解决方案会迭代流的内容并创建List<List<Integer>>并将其转换为Stream<Stream<Integer>>。但是这意味着完整的结果会保留在内存中(这对我的应用程序来说是不受欢迎的)。

我也认为我可以通过编写自己从流中读取的Iterator来解决这个问题,但我不确定这是如何工作的。

问题

如何根据原始Stream的内容将Stream转换为Streams Stream,而不将完整结果存储为{{1}首先是List

4 个答案:

答案 0 :(得分:5)

您可能希望实施自己的aggregating spliterator来执行此操作。 proton-pack库中已有类似内容(第一个链接重定向到proton-pack中实现的链接)。

请注意,您获得了Stream<List<Integer>>(您可能会尝试直接修改实现以获得Stream<Stream<Integer>>,但您始终需要缓冲少量元素;具体取决于窗口的大小;要测试是否应该创建一个新窗口)。例如:

StreamUtils.aggregate(Stream.of(1, 1, 1, 2, 2, 2, 3, 6, 7, 7, 1, 1), 
                      Objects::equals)
           .forEach(System.out::println);

输出:

[1, 1, 1]
[2, 2, 2]
[3]
[6]
[7, 7]
[1, 1]

答案 1 :(得分:3)

您可以使用我的StreamEx库。它有groupRuns来完成工作:

List<Integer> input = Arrays.asList(1, 1, 1, 2, 2, 2, 3, 6, 7, 7, 1, 1);
Stream<Stream<Integer>> streams = StreamEx.of(input).filter(this::isOdd)
    .groupRuns(Integer::equals)
    .map(List::stream);

用法示例:

streams.map(s -> StreamEx.of(s).joining(",")).forEach(System.out::println);

输出:

1,1,1
3
7,7
1,1

与protonpack库类似,里面有自定义分裂器,但是使用StreamEx可以利用并行处理(质子包根本不会分裂)。

在顺序处理中,一次最多只有一个中间列表驻留在内存中(其他中间列表符合GC条件)。如果您仍然担心内存消耗(例如,您有很长的组),那么自StreamEx 0.3.3以来有另一种方法可以解决此任务:

Stream<Stream<Integer>> streams = StreamEx.of(input).filter(this::isOdd)
        .runLengths()
        .mapKeyValue(StreamEx::constant);

runLengths方法返回条目流,其中key是元素,value是相邻重复元素的数量。之后使用StreamEx.constant,这是Stream.generate(() -> value).limit(length)的快捷方式。因此,即使对于很长的组,您也会有一个恒定的中间内存消耗。当然这个版本也是并行友好的。

更新: StreamEx 0.3.3已经发布,因此第二个解决方案现在也符合条件。

答案 2 :(得分:2)

我担心这是不可行的,至少不是很好的方式。即使您将元素映射到流中并减少它们,这些内部流也必须知道它们包含哪些元素,因此它们必须存储一些内容。

最简单的解决方案是使用groupingBy,但它会将所有结果存储在地图中:

List<Integer> input = asList(1, 1, 1, 2, 2, 2, 3, 6, 7, 7, 1, 1);
Map<Integer, List<Integer>> grouped = input.stream().collect(groupingBy(i -> i));
Stream<Stream<Integer>> streamOfStreams = grouped.values().stream().map(list -> list.stream());

您可以尝试使用reduce操作,但它需要您实现自己的Stream of Streams,您必须在其中存储每个流包含的元素。更不用说实施它需要付出很多努力。

我能想到的最佳解决方案是迭代列表两次:

public static void main(String[] args) {
    List<Integer> input = asList(1, 1, 1, 2, 2, 2, 3, 6, 7, 7, 1, 1);

    input.stream().distinct().filter(i -> isOdd(i)).forEach(i -> {
        List<Integer> subList = input.stream().filter(j -> Objects.equals(j, i)).collect(toList());
        System.out.println(subList); // do something with the stream instead of collecting to list
    });
}

private static boolean isOdd(Integer i) {
    return (i & 1) == 1;
}

但请注意,它的时间复杂度为O(n^2)

修改

此解决方案仅包含本地元素组。它只存储当前的本地组。

public static void main(String[] args) {
    Stream<Integer> input = Stream.of(1, 1, 1, 2, 2, 2, 3, 6, 7, 7, 1, 1);

    Iterator<Integer> iterator = input.iterator();
    int first;
    int second = iterator.next();

    List<Integer> buffer = new ArrayList<>();
    buffer.add(second);

    do {
        first = second;
        second = iterator.next();

        if (Objects.equals(first, second)) {
            buffer.add(second);
        } else {
            doSomethingWithTheGroup(buffer);
            buffer = new ArrayList<>(); // let GC remove the previous buffer
            buffer.add(second);
        }
    } while (iterator.hasNext());
    doSomethingWithTheGroup(buffer);
}

private static void doSomethingWithTheGroup(List<Integer> buffer) {
    System.out.println(buffer);
}

private static boolean isOdd(Integer i) {
    return (i & 1) == 1;
}

输出:

[1, 1, 1]
[2, 2, 2]
[3]
[6]
[7, 7]
[1, 1]

答案 3 :(得分:-1)

与@Jaroslaw一样,我也使用Map来保存不同的Streams。但是, 可以使地图保留从输入构建的Streams,而不是预先收集的。使用Stream.concatStream.of,您可以向流中添加一个元素:

    Map<Integer, Stream<Integer>> streamMap = new HashMap<>();

    int[] arr = {1,1,1,2,2,2,3,6,7,7,1,1};
    Arrays.stream(arr)
    .filter(this::isOdd)
    .forEach(i -> {
        Stream<Integer> st = streamMap.get(i);
        if (st == null)  st = Stream.of(i);
        else st = Stream.concat(st, Stream.of(i));
        streamMap.put(i, st);
    });

    streamMap.entrySet().stream().forEach(e -> {
        System.out.print(e.getKey() + "={");
        e.getValue().forEach(System.out::print);
        System.out.println("}");
    });

输出:

1={11111}
3={3}
7={77}