Java 潜在无限流的功能性解压实现

Java 潜在无限流的功能性解压实现,java,functional-programming,java-8,java-stream,Java,Functional Programming,Java 8,Java Stream,我已经实现了如下功能性的unzip()操作: public static <T, U, V> Tuple2<Stream<U>, Stream<V>> unzip( Stream<T> stream, Function<T, Tuple2<U, V>> unzipper) { return stream.map(unzipper) .reduce(new

我已经实现了如下功能性的
unzip()
操作:

public static <T, U, V> Tuple2<Stream<U>, Stream<V>> unzip(
        Stream<T> stream, 
        Function<T, Tuple2<U, V>> unzipper) {

    return stream.map(unzipper)
        .reduce(new Tuple2<>(Stream.<U>empty(), Stream.<V>empty()),
            (unzipped, tuple) -> new Tuple2<>(
                Stream.concat(unzipped.$1(), Stream.of(tuple.$1())),
                Stream.concat(unzipped.$2(), Stream.of(tuple.$2()))),
            (unzipped1, unzipped2) -> new Tuple2<>(
                Stream.concat(unzipped1.$1(), unzipped2.$1()),
                Stream.concat(unzipped1.$2(), unzipped2.$2())));
}
如果我有人流:

Stream<Person> people = Stream.of(
    new Person("Joe", 52), 
    new Person("Alan", 34), 
    new Person("Peter", 42));
这是正确的

所以我的问题是:有没有一种方法可以让
解压()
处理许多元素(可能是无限的)?

注意:为了完整起见,下面是我的不可变
Tuple2
类:

public final class Tuple2<A, B> {

    private final A $1;

    private final B $2;

    public Tuple2(A $1, B $2) {
        this.$1 = $1;
        this.$2 = $2;
    }

    public A $1() {
        return $1;
    }

    public B $2() {
        return $2;
    }
}
公共最终类Tuple2{
私人决赛A$1;
私人决赛B$2;
公共元组2(A$1,B$2){
这.$1=$1;
这.$2=$2;
}
公共服务A$1(){
退还$1;
}
公共B$2(){
退还$2;
}
}

您的解决方案不仅容易出现潜在的
StackOverflowerError
s,而且即使不存在
StackOverflowerError
的风险,也远远不能处理潜在的无限流。关键是,您正在构造一个流,但它是一个串联的单元素流,源流的每个元素对应一个流。换句话说,在返回
unzip
方法时,您拥有一个完全物化的数据结构,这将比收集到
ArrayList
或简单的
toArray()
操作的结果消耗更多的内存

然而,当您想在之后执行
收集
时,支持潜在无限流的想法无论如何都是没有意义的,因为收集意味着在不短路的情况下处理所有元素

一旦你放弃了支持无限流的想法,专注于收集操作,就会有一个更简单的解决方案。从中提取代码,将
Pair
替换为
Tuple2
,并将累加器逻辑从“条件”更改为“两者”,我们得到:

public static <T, A1, A2, R1, R2> Collector<T, ?, Tuple2<R1,R2>> both(
    Collector<T, A1, R1> first, Collector<T, A2, R2> second) {

    Supplier<A1> s1=first.supplier();
    Supplier<A2> s2=second.supplier();
    BiConsumer<A1, T> a1=first.accumulator();
    BiConsumer<A2, T> a2=second.accumulator();
    BinaryOperator<A1> c1=first.combiner();
    BinaryOperator<A2> c2=second.combiner();
    Function<A1,R1> f1=first.finisher();
    Function<A2,R2> f2=second.finisher();
    return Collector.of(
        ()->new Tuple2<>(s1.get(), s2.get()),
        (p,t)->{ a1.accept(p.$1(), t); a2.accept(p.$2(), t); },
        (p1,p2)->new Tuple2<>(c1.apply(p1.$1(), p2.$1()), c2.apply(p1.$2(), p2.$2())),
        p -> new Tuple2<>(f1.apply(p.$1()), f2.apply(p.$2())));
}
像这样使用它

Tuple2<List<String>, List<Integer>> namesAndAges=
    Stream.of(new Person("Joe", 52), new Person("Alan", 34), new Person("Peter", 42))
        .collect(both(
            p -> new Tuple2<>(p.name, p.age), Collectors.toList(), Collectors.toList()));

这可以作为基于
concat
的解决方案的替代品。它还将完全存储流元素,但它将使用
stream.Builder
,它针对增量填充和消耗一次的用例进行了优化(在
stream
操作中)。这比收集到
ArrayList
(至少在参考实现中)更有效,因为它使用“spined buffer”,在增加容量时不需要复制。对于具有潜在未知大小的流,这是最有效的解决方案(对于具有已知大小的流,
toArray()
将执行得更好)。

非常相关:为了有效地做到这一点,我们需要在两个流中克隆给定流,并在每个流上独立迭代。我所看到的唯一方法(以及支持多线程)是预先收集列表中的所有元素…@Tunaki感谢这个链接,关键是不收集就可以完成,因为输入流可能是无限的,解压它应该仍然可以工作。同样,不,我认为这是不可能的。Java 8流不像其他语言中的流那样可能实现这一点。@LouisWasserman Yep,我认为这是不可能的,至少我正在尝试的方式是这样的。如果我使用一个集合来保存输入流元素,然后从中创建两个流,我可能会耗尽内存。如果我在减少输入流的同时通过连接创建新流(如我的问题中所述),我将得到一个堆栈溢出异常。我看没有出路了……谢谢@Holger,给出了如此完整的答案。最初,我使用
collect()
编写unzip,但我切换到
reduce()
,因为我得到了一个
IllegalStateException
(流已被消耗)。不过,您的所有实现都工作得很好。你说得对,我不想收集到
ArrayList
,而是通过反复调用
concat()
创建自己的链表:)我的坏消息
public static <T, A1, A2, R1, R2> Collector<T, ?, Tuple2<R1,R2>> both(
    Collector<T, A1, R1> first, Collector<T, A2, R2> second) {

    Supplier<A1> s1=first.supplier();
    Supplier<A2> s2=second.supplier();
    BiConsumer<A1, T> a1=first.accumulator();
    BiConsumer<A2, T> a2=second.accumulator();
    BinaryOperator<A1> c1=first.combiner();
    BinaryOperator<A2> c2=second.combiner();
    Function<A1,R1> f1=first.finisher();
    Function<A2,R2> f2=second.finisher();
    return Collector.of(
        ()->new Tuple2<>(s1.get(), s2.get()),
        (p,t)->{ a1.accept(p.$1(), t); a2.accept(p.$2(), t); },
        (p1,p2)->new Tuple2<>(c1.apply(p1.$1(), p2.$1()), c2.apply(p1.$2(), p2.$2())),
        p -> new Tuple2<>(f1.apply(p.$1()), f2.apply(p.$2())));
}
Tuple2<List<String>, List<Integer>> namesAndAges=
    Stream.of(new Person("Joe", 52), new Person("Alan", 34), new Person("Peter", 42))
        .collect(both(
            Collectors.mapping(p->p.name, Collectors.toList()),
            Collectors.mapping(p->p.age,  Collectors.toList())));
List<String> names = namesAndAges.$1(); // ["Joe", "Alan", "Peter"]
List<Integer> ages = namesAndAges.$2(); // [52, 34, 42]
public static <T, T1, T2, A1, A2, R1, R2> Collector<T, ?, Tuple2<R1,R2>> both(
    Function<? super T, ? extends Tuple2<? extends T1, ? extends T2>> f,
    Collector<T1, A1, R1> first, Collector<T2, A2, R2> second) {

    return Collectors.mapping(f, both(
            Collectors.mapping(Tuple2::$1, first),
            Collectors.mapping(Tuple2::$2, second)));
}
Tuple2<List<String>, List<Integer>> namesAndAges=
    Stream.of(new Person("Joe", 52), new Person("Alan", 34), new Person("Peter", 42))
        .collect(both(
            p -> new Tuple2<>(p.name, p.age), Collectors.toList(), Collectors.toList()));
public static <T, U, V> Tuple2<Stream<U>, Stream<V>> unzip(
    Stream<T> stream,  Function<T, Tuple2<U, V>> unzipper) {

    return stream.map(unzipper)
        .collect(Collector.of(()->new Tuple2<>(Stream.<U>builder(), Stream.<V>builder()),
            (unzipped, tuple) -> {
                unzipped.$1().accept(tuple.$1()); unzipped.$2().accept(tuple.$2());
            },
            (unzipped1, unzipped2) -> {
                unzipped2.$1().build().forEachOrdered(unzipped1.$1());
                unzipped2.$2().build().forEachOrdered(unzipped1.$2());
                return unzipped1;
            },
            tuple -> new Tuple2<>(tuple.$1().build(), tuple.$2().build())
        ));
}