Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting a boolean flag inside Java 8 Stream

I'm wondering what the best practices are for setting a boolean flag value from a java stream. Here is an example of what I want to do:

    List<Integer> list = Arrays.asList(1,2,3,4,5);
    boolean flag = false;
    List<Integer> newList = list.stream()
                                //many other filters, flatmaps, etc...
                                .filter(i -> {
                                    if(condition(i)){
                                        flag = true;
                                    }
                                    return condition(i);
                                })
                                //many other filters, flatmaps, etc...
                                .collect(Collectors.toList());
    //do some work with the new list and the flag

However, this goes against the language restriction "Variable used in lambda expression should be final or effectively final". There are a few solutions I can think of but I'm not sure which is best. This first solution I have is adding elements that match condition to a list and checking List::isEmpty. One could also wrap flag in an AtomicReference.

Note that my question is similar to this question, but I'm trying to extract a boolean value in the end rather than setting a variable.

like image 423
mitch Avatar asked Jul 18 '17 17:07

mitch


1 Answers

Don’t taint your task of producing the newList with an entirely unrelated task. Just use

boolean flag = list.stream().anyMatch(i -> condition(i));

followed by the other stream code.

There are two typical objections

  1. But this is iterating twice.

    Yes, it is, but who says that iterating an ArrayList twice is a problem? Don’t try to avoid multiple stream operations, unless you know that you really have an expensive-to-traverse stream source, like an external file. If you have such an expensive source, it might still be easier to collect the elements into a collection first, which you can traverse twice.

  2. But it’s evaluating condition(…) more than once.

    Well, actually it’s evaluating it less than you original code

    .filter(i -> {
        if(condition(i)){
            flag = true;
        }
        return condition(i);
    })
    

    as anyMatch stops at the first match, while you original predicate evaluates condition(i) twice per element, unconditionally.


If you have several intermediate steps preceding the condition, you may collect into an intermediate List like

List<Integer> intermediate = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    .collect(Collectors.toList());
boolean flag = !intermediate.isEmpty();
List<Integer> newList = intermediate.stream()
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

but more than often, the intermediate step aren’t as expensive at it might seem at the first glance. The performance characteristics of similar intermediate steps may vary in different stream operations, depending of the actual terminal operation. So it might still work sufficiently doing these steps on-the-fly:

boolean flag = list.stream()
    //many other filters, flatmaps, etc...
    .anyMatch(i -> condition(i));
List<Integer> newList = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

If you worry about the code duplication itself, you can still put the common code into a stream returning utility method.

Only in very rare cases, it pays off to go into the lowlevel API and peek into the Stream like in this answer. And if you do so, you shouldn’t go the route of an Iterator which will loose meta information about the contents, but use a Spliterator:

Spliterator<Integer> sp = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    .spliterator();
Stream.Builder<Integer> first = Stream.builder();
boolean flag = sp.tryAdvance(first);
List<Integer> newList = Stream.concat(first.build(), StreamSupport.stream(sp, false))
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

Note that in all of these cases, you could short-cut if flag is false, as the result can only be an empty list then:

List<Integer> newList = !flag? Collections.emptyList():
/*
   subsequent stream operation
 */;
like image 63
Holger Avatar answered Oct 13 '22 08:10

Holger