繁体   English   中英

Java 8 Streams:复杂的流处理

[英]java 8 streams: complex stream processing

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

但是什么流API可以让我插入此方法? 我是否必须创建自己的收集器,以便在收集时将物品发射到其他流? 但这会改变数据流方向,从拉到推,对吧?

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

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

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

另一个想法:

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

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

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

您用作示例的复杂操作全部遵循对流中一个元素的操作模式,具体取决于流中的其他元素。 Java流经过专门设计,不允许在没有收集或精简的情况下进行这些类型的操作。 流操作不允许直接访问其他成员,通常,具有副作用的非终端操作是个坏主意。

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

集合和流虽然具有一些表面上的相似性,但它们具有不同的目标。 馆藏主要涉及对其元素的有效管理和访问。 相比之下,流不提供直接访问或操纵其元素的方法,而与声明性地描述其源以及将在该源上聚合执行的计算操作有关。

进一步来说:

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

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

如果流操作的行为参数是有状态的,则流管线结果可能不确定或不正确。 有状态的lambda(或其他实现适当功能接口的对象)是一种有状态的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

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

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

还请注意,可以进行安全的有状态中间操作。 需要对它们进行设计,以使它们明确不依赖于处理元素的顺序。 Stream.distinctStream.sorted就是很好的例子。 它们需要保持工作状态,但是设计目的是不管处理元素的顺序如何。

因此,要回答您的问题,可以在Java中完成这些类型的操作,但是它们不简单,安全(出于上一段给出的原因),也不自然适合于语言设计。 我建议使用缩减或收集或(请参见Tagir Valeev的答案)分离器创建新的流。 或者使用传统迭代。

您可以调用并返回任何标准流操作,例如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的方式。 特别地,当给定并行流时,诸如这两种操作将失败。

正确的方法(尽管不是很容易)是编写自己的Spliterator 常用算法如下:

  1. 使用stream.spliterator stream.spliterator()获取现有的Stream Spliterator
  2. 编写自己的Spliterator,在进行一些其他操作时可能会消耗现有分离器的元素。
  3. 通过StreamSupport.stream(spliterator, stream.isParallel())基于您的分隔器创建一个新流
  4. close()调用委托给原始流,如.onClose(stream::close)

编写良好的并行化良好的分离器通常是一项非常艰巨的任务。 但是,如果您不关心并行化,则可以将AbstractSpliterator子类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简化此类自定义流操作的headTail 这是使用StreamEx进行相同操作的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);

此问题/更新2中表达的tobias_k答案和思想的基础上,我们可能只返回捕获其局部变量的适当谓词和Map函数。 (因此,这些函数是有状态的,这对于流而言并不理想,但是流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;
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM