23

I have a list of numbers with some 0s inside. Since 0 means invalid measure in my situation, I need change the 0 valued element with the first non 0 element that I can find in the previous positions.

For example the list

45 55 0 0 46 0 39 0 0 0

must become

45 55 55 55 46 46 39 39 39 39

This is the implementation using the classic for each

      int lastNonZeroVal = 0;
        for (MntrRoastDVO vo : res) {
            if (vo.getValColor() > 0) {
                lastNonZeroVal = vo.getValColor();
            } else {
                vo.setValColor(lastNonZeroVal);
            }
        }

Is there a way to implement this with the Java Streams and Lambda Functions?

Since I know that I must not change the source of the stream in the foreach lambda, actually the list is a list of object and I do not change the element of the list but I just assign new values.

This was my first solution

int lastNonZeroVal = 1;
resutl.stream().forEach(vo -> {
        if(vo.getValColor()>0){
            lastNonZeroVal=vo.getValColor();
        }else{
            vo.setValColor(lastNonZeroVal);
        }
});

But I also read here

It's best if the lambdas passed to stream operations are entirely side effect free. that is, that they don't mutate any heapbased state or perform any I/O during their execution.

This is what is worryng me

the data is partitioned, there's no guarantee that when a given element is processed, all elements preceding that element were already processed.

Can this solution produce invalid results, maybe when the number of elements in the list are high? ? Event if I do not use parallelStream() ?

4
  • could give incorrect results if the stream contains negative numbers....rest cases it looks fine ... Commented Sep 2, 2016 at 9:43
  • 5
    Why do you want to use a stateless structure (stream) for a stateful operation ? Commented Sep 2, 2016 at 12:42
  • I guess this is exactly my error, stream are not the right solution in this case Commented Sep 5, 2016 at 8:44
  • @Panciz please either accept the answer or comment what is missing Commented Sep 5, 2016 at 10:40

4 Answers 4

10

It's best if the lambdas passed to stream operations are entirely side effect free. that is, that they don't mutate any heapbased state or perform any I/O during their execution.

Your solution does infact have a side effect, it changes your source list to a resource list. To avoid that, you need the map operator and transform your stream to a Collection. Because you can not access the previous element the state must be stored outside in a final field. For reasons of brevity I used Integer instead of your object:

List<Integer> sourceList = Arrays.asList(45, 55, 0, 0, 46, 0, 39, 0, 0, 0);

final Integer[] lastNonZero = new Integer[1]; // stream has no state, so we need a final field to store it
List<Integer> resultList = sourceList.stream()
             .peek(integer -> {
                 if (integer != 0) {
                     lastNonZero[0] = integer;
                 }
             })
             .map(integer -> lastNonZero[0])
             .collect(Collectors.toList());

System.out.println(sourceList); // still the same
System.out.println(resultList); // prints [45, 55, 55, 55, 46, 46, 39, 39, 39, 39]

Using a stream for your problem is not the best solution, unless you need some additional operations like filter, other map operations or sort.

Sign up to request clarification or add additional context in comments.

3 Comments

It looks like there are some typos in this solution: "item" should be changed to "integer" and change the map to: map(integer -> { integer == 0 ? lastNonZero[0] : integer })
@mancocapac thx for the integer typo, the rest should be correct
I suggest using AtomicReference<Integer> instead Integer[]
5

There is a way to do this with just stream functions, although it's not particularly clean in my opinion. You could create your own collector to default to the last entry in the list if the current entry is zero. Something like this:

void AddOrLast(List<Integer> list, Integer value) {
    Integer toAdd = 0;
    if (value != 0) {
        toAdd = value;
    } else {
        if (!list.isEmpty()) {
            toAdd = list.get(list.size() - 1);
        }
    }
    list.add(toAdd);
}

@Test
public void name() {
    List<Integer> nums = Arrays.asList(45, 55, 0, 0, 46, 0, 39, 0, 0, 0);

    Collector<Integer, ArrayList<Integer>, List<Integer>> nonZeroRepeatCollector =
            Collector.of(
                    ArrayList::new,
                    this::AddOrLast,
                    (list1, list2) -> { list1.addAll(list2); return list1; },
                    (x) -> x);

    List<Integer> collect = nums.stream().collect(nonZeroRepeatCollector);
    System.out.println(collect);
    // OUT: [45, 55, 55, 55, 46, 46, 39, 39, 39, 39]
}

The AddOrLast method will add the current value if non-zero, otherwise the last entry from the array we're building.

The nonZeroRepeatCollector uses the supplier, accumulator, combiner, finisher pattern.

  • The supplier initialized the returned object. (Our ArrayList)
  • The accumulator updated the returned object with the new value. (list.add)
  • The combiner is used in the case where the stream was split and needs to be rejoined, in parallel stream for example. (This won't be called in our case)
  • The finished is a final action to complete the collection. In our case, just return the ArrayList.

Comments

1

You can modify state of objects inside stream. But you can not modify data source state.

The problem is the same as in classic iterating.

Use forEachOrdered() to perform

an action for each element of this stream, in the encounter order of the stream if the stream has a defined encounter order

If you call

result.stream.forEachOrdered(...)

all elements will be processed sequentially in order.

For sequential streams forEach seems to respect the order.

2 Comments

I specified better what is worryng me.
Ok this solved the problem of the execution order but I think I still may have problem with the visibility of the shared variable " Performing the action for one element happens-before performing the action for subsequent elements, but for any given element, the action may be performed in whatever thread the library chooses."
1

First off, you shouldn't be mutating state within a lambda. That said, you could use a custom list extending ArrayList and override the iterator() and spliterator() methods.

Note that this is using a Pair class which I've omitted here for brevity.

public class MemoList extends ArrayList<Pair<Integer,MntrRoastDVO>> {
    private static final long serialVersionUID = -2816896625889263498L;

    private final List<MntrRoastDVO> list;

    private MemoList(List<MntrRoastDVO> list) {
        this.list = list;
    }

    public static MemoList of(List<MntrRoastDVO> list) {
        return new MemoList(Objects.requireNonNull(list));
    }

    @Override
    public Iterator<Pair<Integer,MntrRoastDVO>> iterator() {
        Iterator<MntrRoastDVO> it = list.iterator();

        return new Iterator<Pair<Integer,MntrRoastDVO>>() {
            private Integer previous = null;

            @Override
            public boolean hasNext() {
                return it.hasNext();
            }

            @Override
            public Pair<Integer,MntrRoastDVO> next() {
                MntrRoastDVO next = it.next();
                Pair<Integer,MntrRoastDVO> pair = new Pair<>(previous, next);

                if (next.getValColor() > 0) {
                    previous = next.getValColor();
                }

                return pair;
            }

        };
    }

    @Override
    public Spliterator<Pair<Integer,MntrRoastDVO>> spliterator() {
        return Spliterators.spliterator(iterator(), list.size(), Spliterator.SIZED);
    }
}

I would then use it like this.

public void doWork(List<MntrRoastDVO> res)
{
    MemoList.of(res).stream().forEach(this::setData);
}

private void setData(Pair<Integer,MntrRoastDVO> pair)
{
    MntrRoastDVO data = pair.two();

    if (data.getValColor() <= 0)
    {
        data.setValColor(pair.one());
    }
}

Note that this is not tested using a parallel stream. In fact, I'm almost certain it wouldn't work in parallel.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.