Combining Predicate Types
The Java Predicate
class provides default methods for combining predicates with
logical and
and logical or
, but they don’t always work as expected.
Unexpected Behavior When Combining Predicates
The java.util.function.Predicate
functional interface provides two methods to combine one predicate with another:
Predicate<T> and(Predicate<? super T> other)
for logical andPredicate<T> or(Predicate<? super T> other)
for logical or
where <T>
is the type of the predicate parameter.
Assuming that we want to write a predicate which checks whether a String
is null
or
empty based on the following two predicates
Predicate<Object> isNull = o -> o != null;
Predicate<String> isEmpty = s -> s.isEmpty(); // assumes s != null
we’d like to define our new predicate
Predicate<String> isNullOrEmpty = isNull.or(isEmpty); // won't compile
As the comment indicates this simple solution will not compile. The reason is that the
generic parameter of isEmpty
is not within the required bounds. Allowed are only
parameter types which are a super class of Object
(or Object
itself), and
String
isn’t.
I have my own predicate class from times before Java 8, and I didn’t like this. You can easily write
String s = ...;
if (s == null || s.isEmpty()) ...
so this should work with predicates, too.
Obviously in this case I could just make isNull
a Predicate<String>
and everything
would work nicely, but in general this is not always possible.
Solutions
Dedicated Methods
My first solution was to write new methods and_()
and or_()
(note the underscore)
to allow for predicate arguments which were extending type predicate’s type <T>
.
As both arguments are Predicate
s it’s not possible to overload.
Here’s what the normal and()
and the underscore and_()
look like in my Predicate1<P>
class:
@NotNull
default Predicate1<P> and(@NotNull Predicate<? super P> other)
{
return p -> test(p) && other.test(p);
}
@NotNull
default <T extends P> Predicate1<T> and_(@NotNull Predicate<T> other)
{
return p -> test(p) && other.test(p);
}
You see they look pretty the same, the code is exactly the same, only the parameter and the return type slightly differ.
It works, but it is awkward that the user is forced to use the correct method depending on the parameter types.
The real problem with this solution arises when we come to BiPredicate
(a predicate
with 2 parameters) where there are 4 possible parameter combinations. In my lib the
BiPredicate
substitute is Predicate2
, and having predicates with up to 9 parameters
would require to choose the correct one of 128 possible slightly differently named and()
method depending on the parameter types for my Predicate9
.
Static Methods
The main problem is the asymmetry of the situation when using methods. The resulting
parameter type either depends on the basic object (this
) or the other
parameter.
Basically the compiler should be able to determine the correct outcome, but this would
only be possible with a static method where both objects are handled on the same level.
But how to declare such method. My first try was to define the dependencies in the generic parameters of the method, but both obvious versions wouldn’t compile:
// WON'T COMPILE
@NotNull
static <T, T1 super T, T2 super T> Predicate<T> and(@NotNull Predicate<T1> pred1,
@NotNull Predicate<T2> pred2)
{
p -> pred1.test(p) && pred2.test(p)
}
// WON'T COMPILE
@NotNull
static <T1, T2, T extends T1 & T2> Predicate<T> and(@NotNull Predicate<T1> pred1,
@NotNull Predicate<T2> pred2)
{
p -> pred1.test(p) && pred2.test(p)
}
The first one does not compile because super
isn’t allowed in the generic parameter
list of a method, although I’m not sure why. Basically the compiler should be able
to figure out the correct classes in this case, so maybe this is a Java 8 problem.
But I’m stuck with Java 8 because of various customers using it, so I haven’t checked
if newer implementations are more generous.
The second one has the problem that multiple extensions (i.e. T extends T1 & T2
)
requite the second parameter T2
to be an interface. As there is no guarantee that
this will be the case (indeed in our example it isn’t) the compiler will reject it.
So the solution I came up with was a variant of the first one:
@NotNull
static <T> Predicate1<T> and(@NotNull Predicate<? super T> pred1,
@NotNull Predicate<? super T> pred2)
{
return p -> pred1.test(p) && pred2.test(p);
}
As far as I can tell this gives the compiler the same information as the first try,
but in this case it accepts it. And it is easily adapted to accept more
generic parameters for Predicate2
(basically the same as BiPredicate
)
and its friends.
Usage is not as nice as with the method (example uses isNull
and isEmpty
defined above):
Predicate<String> isNullOrEmpty = Predicate1.or(isNull, isEmpty);
For me this is harder to read, but it’s still much nicer to handle and remember than having to use differently named methods, especially in cases with more generic parameters.
The compiler will not accept the method if the two types are not depending on
each other so that either the first extends the second or vice versa. Eg you cannot
or
a Predicate<String>
and a Predicate<java.awt.Point>
.
Last Words
The
de.caff.generics.function.Predicate1
,
de.caff.generics.function.Predicate2
, …,
de.caff.generics.function.Predicate9
,
classes are part of the generics
module of the
de·caff Commons which can
be downloaded for free from this website.