Interface Java Predicate
Testez des conditions sur des valeurs en Java avec l'interface fonctionnelle Predicate et ses combinateurs and/or/negate.
Predicate<T> est l'interface fonctionnelle pour la question « cette valeur est-elle valide ? » — une entrée de type T, une réponse boolean. Elle est au cœur de Stream.filter, Collection.removeIf, Optional.filter et de toutes les méthodes JDK qui disent « garder celles qui correspondent ». L'interface est minuscule — une seule méthode test(T) — mais elle est fournie avec une petite algèbre de combinateurs (and, or, negate, isEqual, not) qui permet de construire des conditions complexes à partir de conditions simples sans jamais écrire la logique booléenne de liaison à la main.
Ce chapitre suit la même structure que les autres zoom sur les interfaces de la Partie 12 : l'interface, ses trois ou quatre méthodes utiles, l'algèbre, puis un exemple concret.
L'interface
La déclaration complète, résumée :
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // the only abstract method
default Predicate<T> and(Predicate<? super T> other);
default Predicate<T> or(Predicate<? super T> other);
default Predicate<T> negate();
static <T> Predicate<T> isEqual(Object target);
static <T> Predicate<T> not(Predicate<? super T> target); // Java 11+
}test est la seule méthode abstraite que les lambdas et les références de méthodes implémentent. Tout le reste est construit par-dessus. Vous appelez rarement test vous-même — c'est stream().filter(...) et list.removeIf(...) qui l'appellent pour vous — mais connaître le nom de la méthode est important lorsque vous écrivez du code qui accepte un Predicate<T> et doit l'invoquer.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — algèbre booléenne sans le code de liaison
Les trois méthodes par défaut composent les prédicats de la même manière que les opérateurs &&, ||, ! composent les booléens :
Predicate<String> notNull = Objects::nonNull;
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> longEnough = s -> s.length() >= 3;
Predicate<String> useful = notNull.and(notBlank).and(longEnough);
Predicate<String> usableOrShort = useful.or(s -> s.length() == 1);
Predicate<String> bad = useful.negate();Deux propriétés importantes :
- Court-circuit, dans l'ordre de déclaration.
a.and(b)n'appelleb.testque sia.testa retournétrue.a.or(b)n'appelleb.testque sia.testa retournéfalse. C'est le même ordre d'évaluation que&&et||, ce qui signifie que vous pouvez placer les vérifications bon marché et échouant souvent en premier, et les plus coûteuses en dernier. - Chaque appel retourne un nouveau
Predicate. Les combinateurs ne mutent pasthis. Réutilisez les originaux autant que vous le souhaitez.
negate() inverse simplement le résultat. useful.negate() retourne true pour les nulls, les chaînes vides et les chaînes de moins de 3 caractères — chaque cas que useful avait rejeté.
Predicate.not — la négation lisible
Java 11 a ajouté un raccourci statique :
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) donne la même réponse boolean que p.negate(), mais se compose beaucoup plus naturellement au site d'appel. La forme de référence de méthode String::isBlank est à elle seule un Predicate<String> — mais vous ne pouvez pas écrire (String::isBlank).negate(), car le compilateur a besoin d'un type cible avant de pouvoir résoudre la référence. Predicate.not(String::isBlank) lui fournit ce type cible, et l'ensemble se lit comme « not blank » dans l'ordre naturel.
Un import statique de Predicate.not rend les chaînes de filtres encore plus propres :
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — égalité null-safe
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")L'implémentation est littéralement t -> Objects.equals(target, t), ce qui signifie qu'un null de part ou d'autre se compare sans danger. Cela économise rarement des frappes par rapport à s -> s.equals("foo"), mais cela vous protège quand le stream peut contenir des null — null.equals("foo") provoquerait un NPE, alors que Objects.equals(null, "foo") retourne false.
Où Predicate<T> apparaît dans le JDK
Le même Predicate<T> traverse toutes les API de « filtrage » :
Stream<String> kept = stream.filter(notBlank); // Stream.filter
boolean removed = list.removeIf(String::isBlank); // Collection.removeIf
Optional<String> ok = opt.filter(notBlank); // Optional.filter
boolean any = stream.anyMatch(notBlank); // anyMatch / allMatch / noneMatch
map.values().removeIf(String::isBlank); // Map view + Collection.removeIfTous ont la même forme, donc un Predicate<T> construit une fois est réutilisable dans toutes les directions — et l'assembler avec and/or/negate est exactement la façon d'éviter l'odeur « j'ai trois filtres légèrement différents, tous quasi-dupliqués ».
Spécialisations primitives — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> fonctionne sur les int, mais chaque appel encapsule l'entrée. Pour les pipelines numériques intensifs, le package fournit :
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Même algèbre and/or/negate, sans encapsulation. Ce sont ces types qu'IntStream.filter accepte — utiliser Predicate<Integer> forcerait le stream à auto-encapsuler chaque élément à l'entrée.
BiPredicate<T, U> — tests à deux arguments
Quand la question prend deux entrées (une clé et une valeur, une ligne et une colonne, une ancienne et une nouvelle), utilisez BiPredicate :
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueLa surface des combinateurs est plus petite — and, or, negate existent, mais il n'y a pas d'isEqual ou de not à deux arguments. Map.removeIf((k, v) -> ...) est exactement un BiPredicate<K, V>.
Un exemple concret : prédicats, composition, algèbre et où ils s'intègrent
Le programme ci-dessous construit trois petits prédicats sur User, les compose avec and/or/negate, démontre le court-circuit en comptant les appels, remplace la négation par Predicate.not au site d'appel removeIf, et utilise un IntPredicate contre un IntStream pour montrer la variante primitive.
Ce qu'il faut retenir de l'exécution :
- Les trois prédicats de base (
adult,active,namedWell) sont restés réutilisables.eligible,minoretreachableont été construits par composition plutôt qu'en écrivant trois lambdas séparés avec une logique qui se chevauche. anda court-circuité exactement comme le fait&&:expensivea été appelé moins de fois quecheapcar chaque mineur était rejeté avant que la vérification coûteuse ne soit déclenchée. C'est le levier dont vous disposez pour l'ordonnancement — placez les vérifications bon marché qui échouent souvent en premier.Predicate.not(...)au site d'appelremoveIfse lisait comme de l'anglais courant (« remove if not non-blank ») et évitait d'avoir besoin d'un type cible avant la négation. L'import statique denotest la petite touche finale.Predicate.isEqual("foo")a compté les deux entrées"foo"en présence d'unnullsans lever d'exception.s -> s.equals("foo")aurait provoqué un NPE sur l'élémentnull.IntPredicate even = n -> n % 2 == 0;s'est branché directement surIntStream.filtersans encapsulation — et le même combinateur.and(...)fonctionne sur la spécialisation primitive.
Prochaine étape
Predicate<T> répond par oui ou non. Le prochain chapitre, Interface Java Function, couvre l'interface pour l'autre moitié du travail sur les streams : transformer une valeur en une autre. La forme — méthode unique, composition via méthodes par défaut (andThen, compose, plus le statique identity()) — est la même que Predicate, et les mêmes leçons sur l'ordonnancement, la réutilisation et les spécialisations primitives s'appliquent.