Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Java 8 Stream Reduce to return List after performing operation on each element using previous elements values

I'm new to Streams and Reduce so I'm trying it out and have hit a problem:

I have a list of counters which have a start counter and end counter. The startcounter of an item is always the endcounter of the previous. I have a list of these counters listItems which I want to loop through efficiently, filter out inactive records and then reduce the list into a new List where all the StartCounters are set. I have the following code:

List<CounterChain> active = listItems.stream()
                .filter(e -> e.getCounterStatus() == CounterStatus.ACTIVE)
                .reduce(new ArrayList<CounterChain>(), (a,b) -> { b.setStartCounter(a.getEndCounter()); return b; });

But it doesn't really work and I'm kind of stuck, can anyone give me a few suggestions to help me get this working? Or is there an equally efficient better way to do this? Thanks!

like image 830
user1501171 Avatar asked Sep 21 '17 17:09

user1501171


1 Answers

A Reduction reduces all elements to a single value. Using a reduction function of the (a,b) -> b form will reduce all elements to the last one, so it’s not appropriate when you want to get a List containing all (matching) elements.

Besides that, you are performing a modification of the input value, which is violating the contract of that operation. Note further, that the function is required to be associative, i.e. it shouldn’t matter whether the stream will perform f(f(e₁,e₂),e₃)) or f(e₁,f(e₂,e₃)) when processing three subsequent stream elements with your reduction function.

Or, to put it in one line, you are not using the right tool for the job.

The cleanest solution is not to mix these unrelated operations:

List<CounterChain> active = listItems.stream()
    .filter(e -> e.getCounterStatus() == CounterStatus.ACTIVE)
    .collect(Collectors.toList());
for(int ix=1, num=active.size(); ix<num; ix++)
    active.get(ix).setStartCounter(active.get(ix-1).getEndCounter());

The second loop could also be implemented using forEach, but it would require an inner class due to its stateful nature:

active.forEach(new Consumer<CounterChain>() {
    CounterChain last;
    public void accept(CounterChain next) {
        if(last!=null) next.setStartCounter(last.getEndCounter());
        last = next;
    }
});

Or, using an index based stream:

IntStream.range(1, active.size())
    .forEach(ix -> active.get(ix).setStartCounter(active.get(ix-1).getEndCounter()));

But neither has much advantage over a plain for loop.

like image 153
Holger Avatar answered Oct 30 '22 21:10

Holger