How to extract only one allowed element from a stream?

Viewed 304

I have a list of elements, and want to extract the value of the fields' propery. Problem: all elements should have the same property value.

Can I do better or more elegant than the following?

Set<String> matches = fields.stream().map(f -> f.getField()).collect(Collectors.toSet());
if (matches.size() != 1) throw new IllegalArgumentException("could not match one exact element");
String distrinctVal = matches.iterator().next(); //continue to use the value

Is this possible directly using the stream methods, eg using reduce?

6 Answers

Your current solution is good. You can try this way also to avoid collecting.

Use distinct() then count()

if (fields.stream().map(f -> f.getField()).distinct().count() != 1) 
      throw new IllegalArgumentException("could not match one exact element");

To get the value

String distrinctVal = fields.get(0).getField();

Well you could certainly do this in several ways but as to which is more elegant can vary from person to person.

Anyway if you were to attempt this via streams this is how i would have done it:

With a slight modification to my answer here you could do:

boolean result = fields.stream()
                       .map(f -> f.getField())
                       .distinct()
                       .limit(2) // ENABLE SHORT CIRCUITING
                       .count() != 1;

if (result) throw new IllegalArgumentException("could not match one exact element");

String distinctVal = fields.get(0).getField();

The benefit of this approach is basically utilising limit(2) to enable optimisation where possible.

Conclusion : your current approach is quite good actually so I wouldn't be surprised if you were to stick to that but you also have the choice of this approach where you can short-circuit the pipeline.

That would be the reduce solution.

Optional<String> distinctVal = fields.stream()
    .map(f -> f.getField())
    .reduce((a, b) -> {
        if(a != b) throw new IllegalArgumentException("could not match one exact element");
        return a;
    });

Depending on the frequency of invocation and the size of your set, the iterative code can be significantly faster.

public boolean allEqual(Collection<Fields> fields) {
    if (fields.size() > 1) {
        String last;
        boolean first = true;
        for (Field field : fields) {
            String thisString = field.getField();
            if (first) {
                last = thisString;
            } else {
                if (!StringUtils.equals(last, thisString)) {
                    return false;
                }
            }
        }
    }
    return true;
}

While this is not a streaming solution, it aborts when the first mismatch is found, which - depending on the input - can be significantly faster.

Similar to this:

String distrinctVal = fields.stream()
    .map(f -> f.getField())
    .reduce((a, b) 
        -> { throw new IllegalArgumentException("could not match one exact element");}
    ).get();

As others have said, it’s largely a matter of taste. Here’s mine.

    String distinctVal = fields.iterator().next().getField();
    if (fields.stream().map(Field::getField).anyMatch(e -> ! e.equals(distinctVal)) {
        throw new IllegalArgumentException("could not match one exact element");
    }
    //continue to use the value

(Code is not tested; forgive typos.)

I didn’t particularly code with performance efficiency in mind, but the code will only search until the first non-matching string, so should be efficient.

Related