java 8 streams:复杂的流处理

时间:2016-08-02 11:50:09

标签: java java-8 java-stream

我想创建一个方法,在流上执行一些复杂的操作(例如,替换第7个元素,删除最后一个元素,删除相邻的重复项等),而不缓存整个流。

但是什么流api让我插入这个方法?我是否必须创建自己的收藏家,同时收集向其他流发送项目?但这会改变数据流方向从拉到推,对吧?

这种方法的可能签名是什么?

Stream<T> process(Stream<T> in)

可能是不可能的(在单线程代码中),因为只有在收集整个输入流之后才能返回结果

另一个想法:

void process(Stream<T> in, Stream<T> out)

似乎也有点缺陷,因为java不允许发出以将项目插入现有流(提供为out参数)。

那么如何在java中进行一些复杂的流处理呢?

4 个答案:

答案 0 :(得分:4)

您作为示例使用的复杂操作都遵循流中一个元素的操作模式,具体取决于流中的其他元素。 Java流专门设计为在没有收集或减少的情况下不允许这些类型的操作。 Streams操作不允许直接访问其他成员,一般来说,带有副作用的非终端操作是个坏主意。

请注意Stream javadoc中的以下内容:

  

集合和流虽然有一些肤浅的相似之处,却有不同的目标。馆藏主要关注其元素的有效管理和访问。相反,流不提供直接访问或操纵其元素的手段,而是涉及声明性地描述它们的源以及将在该源上聚合执行的计算操作。

更具体地说:

  

大多数流操作接受描述用户指定行为的参数...为了保持正确的行为,这些行为参数:

     

必须是非干扰的(它们不会修改流源);和   在大多数情况下,必须是无状态的(它们的结果不应该依赖于在执行流管道期间可能发生变化的任何状态)。

  

如果流操作的行为参数是有状态的,则流管道结果可能是不确定的或不正确的。有状态lambda(或实现适当功能接口的其他对象)的结果取决于在流管道执行期间可能发生变化的任何状态

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.htmlhttp://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

详细描述了itermediate和终端无状态和有状态操作的所有复杂性

这种方法既有优点也有缺点。一个显着的优点是它允许并行处理流。一个显着的缺点是在Java中很难用其他语言中的操作(例如跳过流中的每个第三个元素)。

请注意,您将看到许多代码(包括SO上的已接受答案),这些代码忽略了流操作的行为参数应该是无状态的建议。为了工作,此代码依赖于未由语言规范定义的Java实现的行为:即,按顺序处理流。规范中有 nothing 以相反顺序或随机顺序停止Java处理元素的实现。这样的实现将使任何有状态流操作立即表现不同。无状态操作将继续表现完全相同。因此,总而言之,有状态操作依赖于Java的实现的细节,而不是规范

另请注意,可以进行安全的有状态中间操作。它们需要设计成使它们特别不依赖于处理元素的顺序。 Stream.distinctStream.sorted就是很好的例子。他们需要维持状态才能工作,但无论处理元素的顺序如何,它们都可以工作。

所以为了回答你的问题,这些类型的操作可以用Java来完成,但它们并不简单,安全(出于前一段中给出的原因)或者自然适合语言设计。我建议使用简化或收集或(参见Tagir Valeev的答案)分裂器来创建新流。或者使用传统的迭代。

答案 1 :(得分:1)

您可以调用并返回任何标准流操作,例如filtermapreduce等,并让它们执行一些复杂的操作,例如:一个需要外部数据的人。例如,filterAdjacentDuplicatesreplaceNthElement可以像这样实现:

public static <T> Stream<T> filterAdjacentDupes(Stream<T> stream) {
    AtomicReference<T> last = new AtomicReference<>();
    return stream.filter(t -> ! t.equals(last.getAndSet(t)));
}

public static <T> Stream<T> replaceNthElement(Stream<T> stream, int n, T repl) {
    AtomicInteger count = new AtomicInteger();
    return stream.map(t -> count.incrementAndGet() == n ? repl : t);
}

使用示例:

List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
replaceNthElement(filterAdjacentDupes(lst.stream()), 3, "BAR").forEach(System.out::println);
// Output: foo bar BAR foo

但是,正如评论中所述,这并不是应该如何使用Stream API。特别是,当给定并行流时,这两个操作将失败。

答案 2 :(得分:1)

正确(但不是很简单)的方法是编写自己的Spliterator。常用算法如下:

  1. 使用stream.spliterator()
  2. 获取现有的流Spliterator
  3. 编写自己的Spliterator,在推进可能会执行一些额外操作时可能会消耗现有的Spliterator。
  4. 通过StreamSupport.stream(spliterator, stream.isParallel())
  5. 根据您的分裂器创建新流
  6. 委托close()调用.onClose(stream::close)等原始信息流。
  7. 编写良好并行化的好分裂器通常是非常重要的任务。但是,如果您不关心并行化,则可以将AbstractSpliterator子类化为更简单的子类。这是一个如何编写新的Stream操作的示例,该操作删除给定位置的元素:

    public static <T> Stream<T> removeAt(Stream<T> src, int idx) {
        Spliterator<T> spltr = src.spliterator();
        Spliterator<T> res = new AbstractSpliterator<T>(Math.max(0, spltr.estimateSize()-1), 
                spltr.characteristics()) {
            long cnt = 0;
    
            @Override
            public boolean tryAdvance(Consumer<? super T> action) {
                if(cnt++ == idx && !spltr.tryAdvance(x -> {}))
                    return false;
                return spltr.tryAdvance(action);
            }
        };
        return StreamSupport.stream(res, src.isParallel()).onClose(src::close);
    }
    

    这是最小的实现,可以改进它以显示更好的性能和并行性。

    在我的StreamEx库中,我尝试通过headTail简化添加此类自定义流操作的过程。以下是使用StreamEx

    执行相同操作的方法
    public static <T> StreamEx<T> removeAt(StreamEx<T> src, int idx) {
        // head is the first stream element
        // tail is the stream of the rest elements
        // want to remove first element? ok, just remove tail
        // otherwise call itself with decremented idx and prepend the head element to the result
        return src.headTail(
           (head, tail) -> idx == 0 ? tail : removeAt(tail, idx-1).prepend(head));
    }
    

    您甚至可以使用chain()方法支持链接:

    public static <T> Function<StreamEx<T>, StreamEx<T>> removeAt(int idx) {
        return s -> removeAt(s, idx);
    }
    

    用法示例:

    StreamEx.of("Java 8", "Stream", "API", "is", "not", "great")
            .chain(removeAt(4)).forEach(System.out::println);
    

    最后请注意,即使没有headTail,也有一些方法可以使用StreamEx解决您的问题。要删除特定索引,您可以使用不断增加的数字进行压缩,然后过滤和删除索引,如下所示:

    StreamEx.of(stream)
            .zipWith(IntStreamEx.ints().boxed())
            .removeValues(pos -> pos == idx)
            .keys();
    

    为了折叠相邻的重复,有专门的collapse方法(甚至可以很好地并行化!):

    StreamEx.of(stream).collapse(Object::equals);
    

答案 3 :(得分:0)

基于在this question/update 2中表达的tobias_k答案和想法,我们可能只返回捕获其局部变量的正确谓词和映射函数。 (因此这些函数是有状态的,对于流不是理想的,但流API中的distinct()方法也可能是有状态的。)

以下是修改后的代码:

public class Foo {
    public static void run() {
        List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
        lst.stream()
                .filter(Foo.filterAdjacentDupes())
                .map(Foo.replaceNthElement(3, "BAR"))
                .forEach(System.out::println);
        // Output: foo bar BAR foo
    }

    public static <T> Predicate<T> filterAdjacentDupes() {
        final AtomicReference<T> last = new AtomicReference<>();
        return t -> ! t.equals(last.getAndSet(t));
    }

    public static <T> UnaryOperator<T> replaceNthElement(int n, T repl) {
        final AtomicInteger count = new AtomicInteger();
        return t -> count.incrementAndGet() == n ? repl : t;
    }
}