8

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?

membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • Your solution is correct and faster than most others, if your elements are supposed to be compared by `equals`. As usually with streams, the iterative code can reach a quicker end, as it can abort faster, when a mismatch is found. – TreffnonX May 25 '20 at 12:22
  • It shall even throw an exception if "fields" is empty? – Sven Döring May 25 '20 at 12:56
  • 1
    @TreffnonX one could also add a little bit of salt ([`limit(2)`](https://stackoverflow.com/questions/62002153/how-to-extract-only-one-allowed-element-from-a-stream/62002878#62002878)) into the stream pipeline to enable short-circuiting where possible. – Ousmane D. May 25 '20 at 13:28
  • Does your code work? `if (matches.size() != 1)` and *all elements should have the same property value.* doesn't really look sufficient for execution though. Out of some guesswork, it also looks like `groupBy` might just be more practical for the use case. – Naman May 25 '20 at 16:14

6 Answers6

5

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();
Eklavya
  • 17,618
  • 4
  • 28
  • 57
  • 1
    Great, but I also need to extract the distinct value. – membersound May 25 '20 at 12:30
  • Hey, apologies if my answer looks almost identical to yours :) it wasn't my intention. your post was deleted hence me posting my own answer with **_some improvement_** (i.e. enabling short circuiting mechanism into the pipeline) to yours which I would have suggested for you to do instead of posting my own answer if your post wasn't deleted initially. – Ousmane D. May 25 '20 at 13:15
4

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.

Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
3

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;
    });
Sven Döring
  • 3,927
  • 2
  • 14
  • 17
0

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.

TreffnonX
  • 2,924
  • 15
  • 23
0

Similar to this:

String distrinctVal = fields.stream()
    .map(f -> f.getField())
    .reduce((a, b) 
        -> { throw new IllegalArgumentException("could not match one exact element");}
    ).get();
lczapski
  • 4,026
  • 3
  • 16
  • 32
0

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.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161