Interfaz Predicate de Java
Evalúa condiciones sobre valores en Java con la interfaz funcional Predicate y sus combinadores and/or/negate.
Predicate<T> es la interfaz funcional para la pregunta "¿este valor es válido?" — una entrada de tipo T, una respuesta boolean. Se encuentra en el centro de Stream.filter, Collection.removeIf, Optional.filter, y cualquier método del JDK que diga "conserva los que coincidan." La interfaz es pequeña — un único método test(T) — pero incluye un pequeño álgebra de combinadores (and, or, negate, isEqual, not) que permite construir condiciones complejas a partir de condiciones simples, sin necesidad de escribir manualmente la lógica booleana.
Este capítulo sigue el mismo esquema que el resto de los análisis detallados de interfaces de la Parte 12: la interfaz, sus tres o cuatro métodos más útiles, el álgebra y luego un ejemplo práctico.
La interfaz
Toda la declaración, parafraseada:
@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 es el único método abstracto que implementan las lambdas y las referencias a métodos. Todo lo demás se construye sobre él. Raramente llamarás test directamente — stream().filter(...) y list.removeIf(...) lo invocan por ti — pero conocer el nombre del método importa cuando escribes código que acepta un Predicate<T> y necesita invocarlo.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — álgebra booleana sin trabajo manual
Los tres métodos default componen predicados de la misma forma en que los operadores &&, ||, ! componen booleanos:
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();Dos propiedades importantes:
- Cortocircuito, en orden de declaración.
a.and(b)solo llama ab.testcuandoa.testdevolviótrue.a.or(b)solo llama ab.testcuandoa.testdevolviófalse. Es el mismo orden de evaluación que&&y||, lo que significa que puedes poner las comprobaciones baratas y de fallo frecuente primero y las costosas al final. - Cada llamada devuelve un nuevo
Predicate. Los combinadores no mutanthis. Reutiliza los originales tanto como quieras.
negate() simplemente invierte el resultado. useful.negate() devuelve true para nulos, cadenas en blanco y cadenas de menos de 3 caracteres — cada caso que useful rechazó.
Predicate.not — la negación legible
Java 11 añadió un atajo estático:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) produce la misma respuesta boolean que p.negate(), pero se compone de forma mucho más natural en el lugar de llamada. La forma con referencia a método String::isBlank es por sí sola un Predicate<String> — pero no puedes escribir (String::isBlank).negate(), porque el compilador necesita un tipo destino antes de poder resolver la referencia. Predicate.not(String::isBlank) le proporciona ese tipo destino, y el conjunto se lee como "not blank" en orden natural.
Un import estático de Predicate.not hace que las cadenas de filtros sean aún más limpias:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — igualdad segura ante null
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")La implementación es literalmente t -> Objects.equals(target, t), lo que significa que un null en cualquiera de los dos lados se compara de forma segura. Rara vez ahorra pulsaciones de tecla frente a s -> s.equals("foo"), pero sí te protege cuando el stream puede contener null — null.equals("foo") lanzaría una NPE, mientras que Objects.equals(null, "foo") devuelve false.
Dónde aparece Predicate<T> en el JDK
El mismo Predicate<T> aparece en todas las API de "filtrado":
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.removeIfTodas tienen la misma forma, por lo que un Predicate<T> construido una vez es reutilizable en todos los contextos — y ensamblarlo con and/or/negate es exactamente la manera de evitar el problema de "tengo tres filtros ligeramente distintos, casi duplicados".
Especializaciones primitivas — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> funciona con ints, pero cada llamada encapsula la entrada. Para canalizaciones numéricas de alto rendimiento, el paquete incluye:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Mismo álgebra and/or/negate, sin encapsulamiento. Estas son las que acepta IntStream.filter — usar Predicate<Integer> allí forzaría al stream a autoencapsular cada elemento al entrar.
BiPredicate<T, U> — pruebas con dos argumentos
Cuando la pregunta recibe dos entradas (una clave y un valor, una fila y una columna, un valor antiguo y uno nuevo), utiliza BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueLa superficie de combinadores es menor — and, or, negate existen, pero no hay isEqual ni not de dos argumentos. Map.removeIf((k, v) -> ...) es exactamente un BiPredicate<K, V>.
Ejemplo práctico: predicados, composición, el álgebra y dónde encajan
El siguiente programa construye tres predicados simples sobre User, los compone con and/or/negate, demuestra el cortocircuito contando llamadas, sustituye Predicate.not por negación en un lugar de llamada removeIf, y usa un IntPredicate contra un IntStream para mostrar la variante primitiva.
Lo que extraer de la ejecución:
- Los tres predicados base (
adult,active,namedWell) permanecieron reutilizables.eligible,minoryreachablese construyeron por composición en lugar de escribir tres lambdas separadas con lógica solapada. andaplicó cortocircuito exactamente igual que&&:expensivese ejecutó menos veces quecheapporque cada menor fue rechazado antes de que se disparara la comprobación costosa. Ese es el mecanismo disponible para el orden — pon primero las comprobaciones baratas y de fallo frecuente.Predicate.not(...)en el lugar de llamada deremoveIfse leía como inglés natural ("remove if not non-blank") y evitó la necesidad de un tipo destino antes de la negación. Importarnotestáticamente es el pequeño toque final.Predicate.isEqual("foo")contó las dos entradas"foo"pasando por encima de unnullsin lanzar excepción.s -> s.equals("foo")habría generado una NPE en el elementonull.IntPredicate even = n -> n % 2 == 0;se conectó directamente aIntStream.filtersin encapsulamiento — y el mismo combinador.and(...)funciona en la especialización primitiva.
Qué sigue
Predicate<T> responde sí o no. El siguiente capítulo, Interfaz Function de Java, cubre la interfaz para la otra mitad del trabajo con streams: transformar un valor en otro. La forma — método único, composición con métodos default (andThen, compose, más el estático identity()) — es la misma que en Predicate, y las mismas lecciones sobre orden, reutilización y especializaciones primitivas se aplican igualmente.