0

I try to throw an exception depending on which filter filtered the last element:

// Find the first person who is older than 18 and has a pet
Person firstOlderThan18AndWithPet =  this.people.entrySet().stream()
    .map(e -> e.getValue())
    .filter(e -> e.getAge() >= 18)
    .findAtLeastOne() // <-- this method doesn't exist
    .orElseThrow(AgeException::new) // <-- exception because no person was older than 18
    .filter(e -> e.hasPet())
    .findFirst()
    .orElseThrow(NoPetException::new); // <-- exception because no person older than 18 has a pet

That way I could distinguish why no person was found in the people stream. Was it because of the age or because no person has a pet.

I only see one option and do it with two streams?

List<Person> peopleOlderThan18 = this.people.entrySet().stream()
    .map(e -> e.getValue())
    .filter(e -> e.getAge() >= 18)
    .collect(Collectors.toList());

if (0 == peopleOlderThan18.size()) {
    throw new AgeException();
}

Person firstOlderThan18AndWithPet = peopleOlderThan18.stream()
    .filter(e -> e.hasPet())
    .findFirst()
    .orElseThrow(NoPetException::new); // <-- exception because no person older than 18 has a pet

Or is there anything I could do to do everything in one stream?

TiMESPLiNTER
  • 5,741
  • 2
  • 28
  • 64
  • 1
    You cannot short circuit a `filter`, so one iteration has to be completed to get the intermediate step and your approach is just fine(`isEmpty` could be better) with dealing in two different exceptions. There might be ways to wrap things within another `Optional` etc but that would compromise readability in my opinion. – Naman Jul 04 '21 at 16:53
  • https://stackoverflow.com/questions/26649062/how-to-check-if-a-java-8-stream-is-empty – user_3380739 Jul 04 '21 at 17:09

3 Answers3

2

You have to decide which case you want to optimize for.

Usually, we optimize for the non-error case. Therefore, we may optimistically attempt to find the matching element in a single iteration and without additional storage:

Person firstOlderThan18AndWithPet =  this.people.values().stream()
    .filter(e -> e.getAge() >= 18 && e.hasPet())
    .findFirst()
    .orElseThrow(() ->
        this.people.values().stream().nonMatch(e -> e.getAge() >= 18)?
        new AgeException():
        new NoPetException()
    );

This will only perform a second iteration if no match has been found and this second iteration is short-circuiting. In the erroneous case, as soon as we encounter a person with an age over 18, we know that the issue was that no candidate had pet.

We could try to avoid the second iteration, but it would make the code more complicated while only the exceptional case would benefit, at the costs of the non-error case.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

It is not possible to clone/fork Stream. You can compose a sequence of operators and apply it just once. So one of the approaches is to utilize Tuples to store intermediate results (there are no tuple literals in Java). To save the benefits of streams without losing code expressiveness you can extract common parts and reinitialize the stream to apply another bunch of operators.

    Predicate<Person> olderThan18 = person -> person.age >= 18;

    Person firstOlderThan18 = people.stream()
      .filter(olderThan18)
      .findFirst()
      .orElseThrow(Exception::new);

    Person firstOlderThan18AndWithPet = people.stream()
      .filter(olderThan18)
      .filter(person -> person.hasPet)
      .findFirst()
      .orElseThrow(AssertionError::new);

https://replit.com/join/rxbbvwoudq-redneckz

  • Thanks for your answer. This way I would traverse the the streams twice till I find an element that matches both predicates, right? So I guess an old fashioned for-loop might be better in this case? Especially because it's an finite list of person. – TiMESPLiNTER Jul 05 '21 at 06:12
  • Yep. It could be better to use an imperative style. But remember that streams give you (in most cases) low memory usage and possibility to parallelize for free) – Alexander Alexandrov Jul 05 '21 at 06:16
0

I now figured out a solution in which I can throw the correct exception without leaving the stream in between. The solution I went with looks like this:

AtomicBoolean ageMatchedAtLeastOnce = new AtomicBoolean(false);

// Find the first person who is older than 18 and has a pet
Person firstOlderThan18AndWithPet =  this.people.entrySet().stream()
    .map(e -> e.getValue())
    .filter(e -> {
        var ageMatch = e.getAge() >= 18
     
        if (ageMatch) {
            ageMatchedAtLeastOnce.set(true);
        }

        return ageMatch;
    })
    .filter(e -> e.hasPet())
    .findFirst()
    .orElseThrow(() -> !ageMatchedAtLeastOnce.get() ? new AgeException() : new NoPetException());

I have no clue if this is a good solution or not. Happy to hear your opinion about it in the comments.

TiMESPLiNTER
  • 5,741
  • 2
  • 28
  • 64