Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using the same list with streams twice in Java

I have to complete this trivial operation with streams: given a list, get the sum and the sum of the first 20 elements.

This is what I had in mind

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

However I cant do this, since I am told I can't reuse a stream, so.. I tried the following:

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

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

However I am told that I can't use sum on Stream, so I need to mapToInt again for left and right hand side of this trivial operation.

Is there a way to do this operation in a more elegant and concise way using streams?

like image 864
piggyback Avatar asked Mar 12 '15 12:03

piggyback


3 Answers

There's a cool way to do this in a single parallel pass, but not using streams.

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

The parallelPrefix method will compute the sequence of partial sums of the specified function (here, plus) on the array, in place. So the first element is a0; the second is a0+a1, the third is a0+a1+a2, etc.

like image 72
Brian Goetz Avatar answered Oct 19 '22 21:10

Brian Goetz


Instead of collecting the numbers in a List<Integer>, you can turn them into an int[] with toArray(). This way, the code is a bit more compact, you don't have to box and unbox all the time, and you can turn that int[] directly into an IntStream.

int[] nums = obj.stream().mapToInt(d -> d.getInt()).toArray();
IntStream.of(nums).limit(20).sum() / IntStream.of(nums).sum();
like image 28
tobias_k Avatar answered Oct 19 '22 23:10

tobias_k


There's no need to overcomplicate things. Just get the stream twice from the list:

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

Yes it will do two passes in the list (and the second time just for the 20 first elements so not a big deal), so I expect this is what they want you to do. It's simple, efficient and readable.


To do it in one pass you can use a collector. As an example, you can do it like this:

 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);

Basically, you stream the indices first. Then for each indice; you'll split them in the map in two lists, one for the 20 first indexes, and the others in the other list; resulting in a Map<Boolean, List<Integer>>.

Then each index is mapped to its value in the original List (with the mapping collector).

Finally you transform each List<Integer> by summing their values into a single Integer. Then you just get the total sum.

Another workaround with a custom collector:

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);
}

and an example of usage:

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]


As you can see it's not very readable and, as mentioned in comments, maybe a for-loop would be appropriate in this case (although you were asked to use Streams).
like image 32
Alexis C. Avatar answered Oct 19 '22 23:10

Alexis C.