W3docs

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");          // true

and, 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'appelle b.test que si a.test a retourné true. a.or(b) n'appelle b.test que si a.test a 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 pas this. 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 string

Predicate.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 nullnull.equals("foo") provoquerait un NPE, alors que Objects.equals(null, "foo") retourne false.

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.removeIf

Tous 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);             // true

La 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Les trois prédicats de base (adult, active, namedWell) sont restés réutilisables. eligible, minor et reachable ont été construits par composition plutôt qu'en écrivant trois lambdas séparés avec une logique qui se chevauche.
  • and a court-circuité exactement comme le fait && : expensive a été appelé moins de fois que cheap car 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'appel removeIf se 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 de not est la petite touche finale.
  • Predicate.isEqual("foo") a compté les deux entrées "foo" en présence d'un null sans lever d'exception. s -> s.equals("foo") aurait provoqué un NPE sur l'élément null.
  • IntPredicate even = n -> n % 2 == 0; s'est branché directement sur IntStream.filter sans 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.

Pratique

Pratique
Vous avez `Predicate<String> notNull = Objects::nonNull;` et `Predicate<String> notBlank = s -> !s.isBlank();` et souhaitez un prédicat qui retourne `true` seulement quand la chaîne est à la fois non-null et non-vide, avec `notNull` vérifié *en premier* pour que `notBlank` ne soit jamais appelé sur un `null`. Quelle expression fait cela correctement ?
Vous avez `Predicate<String> notNull = Objects::nonNull;` et `Predicate<String> notBlank = s -> !s.isBlank();` et souhaitez un prédicat qui retourne `true` seulement quand la chaîne est à la fois non-null et non-vide, avec `notNull` vérifié *en premier* pour que `notBlank` ne soit jamais appelé sur un `null`. Quelle expression fait cela correctement ?
Was this page helpful?