Search code examples
javafunctional-programmingpredicatefunctional-interface

What is the preferred way to chain predicates in Java?


I have a bunch of predicates, and I want to chain them together with logical "and" so that the final result is true only if all of the individual predicates evaluate to true.

As I see it, there are two ways I can write this. I can chain them together like this:

Predicate composedPredicate = 
  predicate1
  .and(predicate2)
  .and(predicate3)
  .and(predicate4)

Or I can use a more nested approach like this:

Predicate composedPredicate = 
  predicate1
  .and(predicate2
       .and(predicate3
            .and(predicate4)))

Obviously, option 1 is more readable, but it seems like it might be slightly less efficient. I imagine option 1 is roughly equivalent to: (((p1 && p2) && p3) && p4)

While option 2 would be: (p1 && (p2 && (p3 && p4)))

In the second case, the first argument p1 would be evaluated and if it is false, the whole thing immediately short-circuits and you are done. In the first option, the first argument is actually the whole expression ((p1 && p2) && p3), which itself has the first argument of (p1 && p2), which in turn has p1 as its first argument. Basically, you're going 2 more "steps" up the stack before p1 actually can be evaluated. I don't actually know how Java implements the default predicate methods so correct me if I'm wrong here.

Is there any way to get the best of both worlds? If not, should I favor the more readable approach over what is probably a very marginal performance increase or vise versa?


Solution

  • Generally, favor readability any perceived marginal performance considerations. For most predicates, we would expect them to execute very quickly and it would be hard to notice such composed predicates that short circuit early and often versus those that short circuit late or not at all.

    So start with your first option, the more readable one, that isn't nesting the predicates to mess with the order of operations.

    If you do find that there is a performance concern, due to some kind of profiler stats or benchmark performance run, then you can reorder the operations for a performance benefit.

    1. You can choose to place predicates that are most likely to short-circuit first, to avoid the unnecessary overhead of executing a predicate that won't have an effect on the result. Because you're using and between them, these would be the ones that are more likely to return false.
    2. If you are calling these chained predicates a large number of times, see if you can find any predicates that are invariant, e.g. don't depend on a loop iteration index, and save those results so they don't need to be repeated over and over.
    3. If you find that one of the predicates is computationally intensive and it isn't invariant over a loop, so it needs to be executed over and over, then place it last in the chain, to see if previous predicates may short-circuit so that it doesn't need to be executed at all.

    But this also shouldn't affect readability. You shouldn't need to nest the predicates as you have done; at most you would need to reorder them, e.g.

    Predicate composedPredicate = 
      predicate4
      .and(predicate2)
      .and(predicate1)
      .and(predicate3)
    

    if you notice that predicate4 rarely returns true and predicate3 is computationally intensive.