What is the difference between using a Predicate or a function as a Java stream filter?

No! Predicate is not really limited in comparison to a method reference! In fact, these things are all the same!

Just look at the filter() function signature: filter(Predicate<? super T> predicate)

And let's consider your examples:

x.stream().filter(e -> e % 2 == 0)

Predicate<Integer> isEven = e -> e % 2 == 0;
...
x.stream().filter(isEven)

The first one is just an inlined version of the latter.

private static boolean isEven(Integer integer) {
return integer % 2 == 0;
}
...
x.stream().filter(MyClass::isEven)

and here you can see Method References in action. MR are just a syntactic sugar allowing you to define Lambda Expression based on already existing functions.

At the end of the day all those expressions become the same implementation of Predicate functional interface.

Also, you can also perform side effects in your Lambda Expressions by using block syntax on the right side but it's generally not advised:

e -> {
    //side effects here
    return e % 2 == 0;
}

When looking at it from the point of view of building a library of reusable Predicates, a library of functions that return a boolean is far more versatile collection of predicates than a library of static final Predicate instances. Why?

Consider a library that contains the following:

public static boolean isEven(int i) { return i -> i % 2 == 0; }

vs.

public static final Predicate<Integer> IS_EVEN = i -> i % 2 == 0; 
  • if library functions are named well, they read better. When revisiting code after a long absence it's easier to scan filter(MyLib::isEven) than filter(i -> i % 2 == 0) and filter(MyLib::isEven) tells you exactly what is being invoked, while filter(MyLib.IS_EVEN) does not
  • If you just want to call your library function instead of use it as a predicate, it's more readable. MyLib.isEven(i) scans better than MyLib.IS_EVEN.test(i)
  • If you need to use an IntPredicate instead of Predicate<Integer> a Guava Predicate<Integer>, Apache Collections4 Predicate<Integer>, etc., with a library function you just continue to do MyLib::isEven. With a static final Predicate<Integer> instance you would have to convert it by doing MyLib.IS_EVEN::test (and you end up using a method reference anyway)

The same reasoning applies to all functional types. Write functions. They can be applied to any functional type that matches the signature with a simple method reference.