在 Java 中两次使用相同的列表和流

Using the same list with streams twice in Java

我必须用流完成这个微不足道的操作:给定一个列表,获取前 20 个元素的总和。

这就是我的想法

IntStream stream = obj.stream().mapToInt(d->d.getInt());
stream.limit(20).sum() / stream.sum();

但是我不能这样做,因为我被告知我不能重复使用流,所以..我尝试了以下方法:

List<Integer> counts = obj.stream()
    .mapToInt(d -> d.getInt())
    .boxed().collect(Collectors.toList());

counts.stream().limit(20).sum() / counts.stream().sum();

但是我被告知我不能在 Stream 上使用 sum,所以我需要再次为这个简单操作的左侧和右侧使用 mapToInt。

有没有办法使用流以更优雅和简洁的方式执行此操作?

您可以用 toArray() 将它们变成 int[],而不是收集 List<Integer> 中的数字。这样代码就更紧凑了,不用一直装箱拆箱,直接把那个int[]变成一个IntStream.

int[] nums = obj.stream().mapToInt(d -> d.getInt()).toArray();
IntStream.of(nums).limit(20).sum() / IntStream.of(nums).sum();

没有必要把事情复杂化。只需从列表中获取流两次:

int totalSum = obj.stream().mapToInt(i -> i).sum();
int first20 = obj.stream().limit(20).mapToInt(i -> i).sum();

是的,它会在列表中执行两次(第二次仅针对前 20 个元素,所以没什么大不了的),所以我希望这就是他们希望您执行的操作。它简单、高效且可读。


要一次性完成,您可以使用收集器。例如,您可以这样做:

 Map<Boolean, Integer> map = IntStream.range(0, obj.size())
                                      .boxed()
                                      .collect(partitioningBy(i -> i < 20, 
                                               mapping(i -> obj.get(i), 
                                                       summingInt(i -> i))));

int first20 = map.get(true);
int totalSum = map.get(true) + map.get(false);

基本上,您首先流式传输索引。然后对于每个指数;您将在地图中将它们分成两个列表,一个用于前 20 个索引,另一个在另一个列表中;结果是 Map<Boolean, List<Integer>>

然后每个索引都映射到它在原始 List 中的值(使用 mapping 收集器)。

最后,您将每个 List<Integer> 的值相加为一个 Integer。然后你就得到总和了。

自定义收集器的另一种解决方法:

public static Collector<Integer, Integer[], Integer[]> limitSum(int limit, List<Integer> list) {
    return Collector.of(() -> new Integer[]{0, 0},
        (a, t) -> { a[0] += list.get(t); if(t < limit) a[1] += list.get(t); },
        (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
        a -> a);
}

和用法示例:

List<Integer> obj = Stream.iterate(0, x -> x + 1).limit(5).collect(toList()); //[0, 1, 2, 3, 4]
Integer[] result = IntStream.range(0, obj.size())
                            .boxed()
                            .collect(limitSum(4, obj));
System.out.println(Arrays.toString(result)); //[10, 6]


如您所见,它的可读性不是很好,并且如评论中所述,for 循环在这种情况下可能是合适的(尽管您被要求使用 Streams)。

有一种很酷的方法可以在单次并行传递中执行此操作,但不使用流。

int[] nums = obj.stream().mapToInt(Integer::intValue).toArray();
Arrays.parallelPrefix(nums, Integer::sum);
int sumOfFirst20 = nums[19];
int sumOfAll = nums[nums.length-1];

parallelPrefix 方法将就地计算数组上指定函数(此处为加号)的部分和序列。所以第一个元素是a0;第二个是a0+a1,第三个是a0+a1+a2,依此类推