Java 8:从Stream <pair>

时间:2017-01-29 22:58:30

标签: java java-8 java-stream

所以我有一些使用Java 8流的代码,它可以工作。它完全符合我的需要,而且它清晰可读(函数式编程很少见)。在子程序结束时,代码在自定义对类型的列表上运行:

// All names Hungarian-Notation-ized for SO reading
class AFooAndABarWalkIntoABar
{
    public int      foo_int;
    public BarClass bar_object;
    ....
}

List<AFooAndABarWalkIntoABar> results = ....;

此处的数据必须作为数组传递到程序的其他部分,因此它们会被复制出来:

// extract either a foo or a bar from each "foo-and-bar" (fab)
int[] foo_array = results.stream()
    .mapToInt (fab -> fab.foo_int)
    .toArray();

BarClass[] bar_array = results.stream()
    .map (fab -> fab.bar_object)
    .toArray(BarClass[]::new);

完成了。现在每个阵列都可以做到。

除了......列表两次的循环困扰着我的灵魂。如果我们需要跟踪更多信息,他们可能会添加第三个字段,然后必须进行第三次传递以将3元组转换为三个数组,等等。所以我愚弄试图一次性完成它。

分配数据结构是微不足道的,但维护消费者使用的索引似乎很可怕:

int[] foo_array = new int[results.size()];
BarClass[] bar_array = new BarClass[results.size()];

// the trick is providing a stateful iterator across the array:
// - can't just use 'int', it's not effectively final
// - an actual 'final int' would be hilariously wrong
// - "all problems can be solved with a level of indirection"
class Indirection { int iterating = 0; }
final Indirection sigh = new Indirection();
// equivalent possibility is
//    final int[] disgusting = new int[]{ 0 };
// and then access disgusting[0] inside the lambda
// wash your hands after typing that code

results.stream().forEach (fab -> {
    foo_array[sigh.iterating] = fab.foo_int;
    bar_array[sigh.iterating] = fab.bar_object;
    sigh.iterating++;
});

这会产生与使用多个流循环的现有解决方案相同的阵列。它大约有一半时间都是这样做的。但是迭代器间接技巧似乎难以置信地丑陋,当然也排除了并行填充数组的可能性。

使用以适当容量创建的一对ArrayList实例,将使Consumer代码只为每个实例调用add,而不需要外部迭代器。但是ArrayList的toArray(T[])必须再次执行存储阵列的副本,而在int情况下,还有那个装箱/取消装箱。

(编辑:&#34的答案;可能的重复&#34;质疑所有谈论只维护流中的索引,并使用直接数组索引来获取filter / {期间的实际数据{1}}调用,并注意如果数据无法通过直接索引访问,它就无法正常工作。虽然这个问题有一个map并且可以直接索引&#34; #34;仅从&#34;的观点来看,List存在,技术上和#34;。例如,如果上面的结果集合是LinkedList,那么调用O(n)List#get使用非连续索引的N次将是......糟糕。)

我还缺少其他更好的可能性吗?我认为自定义get可能会这样做,但我无法弄清楚如何在那里保持状态,甚至从未达到过刮擦代码。

4 个答案:

答案 0 :(得分:5)

由于流的大小已知,因此没有理由再次重新发明轮子。最简单的解决方案通常是最好的解决方案。您已经显示的第二种方法就是 - 只需使用AtomicInteger作为数组索引,您就可以实现目标 - 单次传递数据,以及可能的parralel流执行(由于AtomicInteger)。

SO

AtomicInteger index=new AtomicInteger()
results.parallelStream().forEach (fab -> {
    int idx=index.getAndIncrement();
    foo_array[idx] = fab.foo_int;
    bar_array[idx] = fab.bar_object;
});

用于parralel执行的线程安全。整个集合的一次迭代

答案 1 :(得分:3)

如果您的先决条件是,迭代列表并通过索引访问列表都是昂贵的操作,则无法从并行流处理中获益。如果您不需要原始列表顺序中的结果值,则可以尝试使用this answer

否则,您无法从并行流处理中受益,因为它要求源能够有效地将其内容分成两半,这意味着随机访问或快速迭代。如果源没有自定义的spliterator,默认实现将尝试通过缓冲元素到阵列中启用并行处理,这已经暗示在并行处理开始之前进行迭代并且具有额外的阵列存储成本,无论如何您的唯一操作是阵列存储操作

当您接受并行处理没有任何好处时,您可以使用顺序解决方案,但通过将其移入Consumer来解决计数器的丑陋问题。由于lambda表达式不支持这个,你可以转向好的旧匿名内部类:

int[]      foo_array = new int[results.size()];
BarClass[] bar_array = new BarClass[results.size()];

results.forEach(new Consumer<AFooAndABarWalkIntoABar>() {
    int index=0;
    public void accept(AFooAndABarWalkIntoABar t) {
        foo_array[index]=t.foo_int;
        bar_array[index]=t.bar_object;
        index++;
    }
});

当然,还有一个经常被忽视的好旧for循环:

int[]      foo_array = new int[results.size()];
BarClass[] bar_array = new BarClass[results.size()];

int index=0;
for(AFooAndABarWalkIntoABar t: results) {
    foo_array[index]=t.foo_int;
    bar_array[index]=t.bar_object;
    index++;
}

我不会感到惊讶,如果这对你的场景表现出色的所有其他选择......

答案 2 :(得分:0)

在流中重用索引的一种方法是将lambda包装在负责增加索引的IntStream中:

IntStream.range(0, results.size()).forEach(i -> {
    foo_array[i] = results.get(i).foo_i;
    bar_array[i] = results.get(i).bar_object;
});

关于Antoniossss的回答,使用IntStream似乎是使用AtomicInteger的稍微偏好的选择:

  • 它也适用于parallel();
  • 两个较少的局部变量;
  • 让Stream API负责并行处理;
  • 两行代码。

编辑:正如Mikhail Prokhorov指出的那样,考虑到O(n)的复杂性,在get等实现上调用LinkedList方法将比其他解决方案慢一些他们的get实现。这可以通过以下方法解决:

AFooAndABarWalkIntoABar temp = results.get(i);
foo_array[i] = temp.foo_i;
bar_array[i] = temp.bar_object;

答案 3 :(得分:0)

Java 12 添加了一个 teeing collector,它提供了一种一次性完成的方法。下面是一些使用 Apache Commons Pair 类的示例代码。

import org.apache.commons.lang3.tuple.Pair;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class Scratch {

    public static void main(String[] args) {
        final Stream<Pair<String, String>> pairs = Stream.of(
                Pair.of("foo1", "bar1"),
                Pair.of("foo2", "bar2"),
                Pair.of("foo3", "bar3")
        );

        final Pair<List<String>, List<String>> zipped = pairs
                .collect(Collectors.teeing(
                        Collectors.mapping(Pair::getLeft, Collectors.toList()),
                        Collectors.mapping(Pair::getRight, Collectors.toList()),
                        (lefts, rights) -> Pair.of(lefts, rights)
                        ));

        // Then get the arrays out
        String[] lefts = zipped.getLeft().toArray(String[]::new);
        String[] rights = zipped.getRight().toArray(String[]::new);

        System.out.println(Arrays.toString(lefts));
        System.out.println(Arrays.toString(rights));
    }
}

输出将是

[foo1, foo2, foo3]
[bar1, bar2, bar3]

它不需要提前知道流的大小。