16

My aim is to filter for a best match. In my example I have a list of persons, which I want to filter by surname and firstname.

The matching prescendence would be:

  1. both surname and firstname match, return first match
  2. only surname matches, return first match
  3. none match, throw some exception

My code so far:

final List<Person> persons = Arrays.asList(
  new Person("Doe", "John"),
  new Person("Doe", "Jane"),
  new Person("Munster", "Herman");

Person person = persons.stream().filter(p -> p.getSurname().equals("Doe")).???
user871611
  • 3,307
  • 7
  • 51
  • 73
  • 4
    You may consider `Stream.max()` with a comparator that rates match on both names higher than match on surname only, in turn higher than no match at all. You may want to filter out the last category altogether first. – Ole V.V. Jan 02 '18 at 13:46
  • 1
    It's interesting that writing this out with a simple `for` loop, or even a `for` loop on persons filtered by surname, produces simpler *and* maximally efficient code. My initial though was, like you, to first filter by surname and then return the first to match by first-name... or just the first if there is first-name match; however there doesn't appear to be a way to short-circuit the "reduce" operation (without throwing, which would be non-idiomatic) and trying to think about it the interface I come up with would be clunky anyway. – Matthieu M. Jan 02 '18 at 14:59

4 Answers4

16

Assuming Person implements equals and hashCode:

Person personToFind = new Person("Doe", "Jane");

Person person = persons.stream()
    .filter(p -> p.equals(personToFind))
    .findFirst()
    .orElseGet(() -> 
        persons.stream()
            .filter(p -> p.getSurname().equals(personToFind.getSurname()))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("Could not find person ..."))
    );
Cyril
  • 2,376
  • 16
  • 21
  • 1
    Assuming it doesn't, just swap `p -> p.equals(personToFind)` with `p -> p.getSurname().equals(personToFind.getSurname()) && p.getFirstName().equals(personToFind.getFirstName())` + check for nulls if neccesary. – qwerty1423 Jan 02 '18 at 12:32
  • 1
    @qwerty1423 And it will not match/satisfy the second criteria where only surnames are equal. Read the question again. – whatamidoingwithmylife Jan 02 '18 at 12:37
  • @GCP It will work just as it would with equals (assuming equals checks the exact condition I've written above). The second criteria is checked in the inner stream. No changes there. – qwerty1423 Jan 02 '18 at 12:40
  • You didn't need the braces and a return statement. You can use an expression lambda. I've edited it for you :) – Michael Jan 02 '18 at 12:41
  • @Cyril I thought about this one too, but a much nicer way would be to code a cancelable reducing here, but that would be much more complicated – Eugene Jan 02 '18 at 12:56
  • @Eugene yeah, I think a for each loop would be simpler if you wanted to do this more efficiently. – Cyril Jan 02 '18 at 14:06
  • @Cyril agreed. still since you put up the effort here, 1+ – Eugene Jan 02 '18 at 14:06
  • 3
    @Eugene: if there is an exact match, this solution does already the minimum. If not, processing all elements at least once, is unavoidable. Depending on the likelihood of a match, you may focus on avoiding iterating the elements twice. – Holger Jan 02 '18 at 23:58
7

You can use

Person person = persons.stream()
        .filter(p -> p.getSurName().equals("Doe"))
        .max(Comparator.comparing(p -> p.getFirstName().equals("Jane")))
        .orElse(null);

It will only consider elements having the right surname and return the best element of them, which is the one with a matching first name. Otherwise, the first matching element is returned.

As already mentioned in a comment, a for loop could be more efficient if there is a best element, as it can short circuit. If there is no best element with matching surname and first name, all element have to be checked in all implementations.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 3
    This is an interesting approach. What about stability? If there are two or more people with the `Doe` surname and none of them is named `Jane`, does this always return the same `Person`? – fps Jan 03 '18 at 00:52
  • 3
    @FedericoPeraltaSchaffner: it will return either, the first full match if there is at least one or the first person with a matching surname if there is no full match. Granted, this behavior is a bit underspecified, see also [this answer](https://stackoverflow.com/a/36716063/2711488)… – Holger Jan 03 '18 at 08:16
  • 1
    Hmmmm... That's good for multiple max elements, according to the provided comparator (if there are multiple Jane Does and the stream is ordered, this will return the first one). But I'm afraid it will return the last Doe if there are no Janes. – fps Jan 03 '18 at 08:41
  • 3
    @FedericoPeraltaSchaffner: note that this comparator only compares `Boolean` values; all persons with matching first names are considered equal to each other and all persons with no matching first names are considered equal to each other. So if there are no first name matches, the first of the “equal” surname matches will be returned. – Holger Jan 03 '18 at 08:59
2

I would propose this:

Optional<Person> bestMatch = persons.stream()
            .filter(p -> "Doe".equals(p.getSurname()))
            .reduce((person, person2) -> {
                if ("John".equals(person.getFirstName())) {
                    return person;
                } else if ("John".equals(person2.getFirstName())) {
                    return person2;
                }
                return person;
            });
Person result = bestMatch.orElseThrow(IllegalArgumentException::new);
Ondra K.
  • 2,767
  • 4
  • 23
  • 39
  • You don't need `if ("John".equals(person.getFirstName())) {` right? As it will be covered by the final `return person;` anyway – Michael Jan 02 '18 at 12:53
  • @OndraK. I thought of something similar too, problem is that this is not short-circuiting and you need to traverse the entire source of the stream, even if your result might be known from the very first element – Eugene Jan 02 '18 at 12:56
  • Yes, thank you, both of you are correct. The solution proposed by Cyril is more efficient as it is in fact short circuiting, on the other hand, a nested stream is used, which is somehow less readable. – Ondra K. Jan 02 '18 at 13:26
0

The right tool is .findFirst(). You can also use .limit(1).

ncmathsadist
  • 4,684
  • 3
  • 29
  • 45