Interface Predicate do Java
Teste condições sobre valores em Java com a interface funcional Predicate e seus combinadores and/or/negate.
Predicate<T> é a interface funcional para a pergunta "este valor é bom?" — uma entrada do tipo T, uma resposta boolean. Ela está no coração de Stream.filter, Collection.removeIf, Optional.filter e de todo método do JDK que diz "mantenha os que correspondem." A interface é pequena — um único método test(T) — mas vem com uma pequena álgebra de combinadores (and, or, negate, isEqual, not) que permite construir condições complexas a partir de condições simples, sem jamais escrever a lógica booleana manualmente.
Este capítulo tem a mesma estrutura dos demais capítulos detalhados de interfaces da Parte 12: a interface, seus três ou quatro métodos úteis, a álgebra e depois um exemplo prático.
A interface
A declaração completa, em paráfrase:
@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 é o único método abstrato que lambdas e referências de métodos implementam. Todo o resto é construído sobre ele. Você raramente chamará test diretamente — stream().filter(...) e list.removeIf(...) o chamam por você — mas conhecer o nome do método importa quando você escreve código que aceita um Predicate<T> e precisa invocá-lo.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — álgebra booleana sem a cola
Os três métodos default compõem predicados da mesma forma que os operadores &&, ||, ! compõem 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();Duas propriedades são importantes:
- Curto-circuito, na ordem de declaração.
a.and(b)só chamab.testquandoa.testretornoutrue.a.or(b)só chamab.testquandoa.testretornoufalse. Esta é a mesma ordem de avaliação de&&e||, o que significa que você pode colocar verificações baratas e que falham frequentemente primeiro, e as custosas por último. - Cada chamada retorna um novo
Predicate. Os combinadores não alteram othis. Reutilize os originais à vontade.
negate() simplesmente inverte o resultado. useful.negate() retorna true para nulos, strings em branco e strings menores que 3 — todo caso que useful rejeitou.
Predicate.not — a negação legível
O Java 11 adicionou um atalho estático:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) produz a mesma resposta boolean que p.negate(), mas compõe muito mais naturalmente no ponto de chamada. A forma de referência de método String::isBlank é um Predicate<String> por si só — mas você não pode escrever (String::isBlank).negate(), porque o compilador precisa de um tipo alvo antes de resolver a referência. Predicate.not(String::isBlank) fornece esse tipo alvo, e o resultado todo se lê como "not blank" em ordem natural.
Um import estático de Predicate.not torna as cadeias de filtros ainda mais limpas:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — igualdade segura para null
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")A implementação é literalmente t -> Objects.equals(target, t), o que significa que null em qualquer lado é comparado com segurança. Raramente economiza teclas em relação a s -> s.equals("foo"), mas de fato salva quando o stream pode conter null — null.equals("foo") lançaria NPE, enquanto Objects.equals(null, "foo") retorna false.
Onde Predicate<T> aparece no JDK
O mesmo Predicate<T> flui por todas as APIs de "filtro":
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.removeIfTodos eles têm a mesma forma, então um Predicate<T> construído uma vez é reutilizável em todas as direções — e montá-lo com and/or/negate é exatamente como se evita o cheiro de "tenho três filtros ligeiramente diferentes, todos quase duplicados".
Especializações primitivas — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> funciona com ints, mas cada chamada encapsula a entrada. Para pipelines numéricas intensas, o pacote fornece:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;A mesma álgebra and/or/negate, sem boxing. Estas são as que IntStream.filter aceita — usar Predicate<Integer> ali forçaria o stream a fazer autoboxing de cada elemento na entrada.
BiPredicate<T, U> — testes com dois argumentos
Quando a pergunta requer duas entradas (uma chave e um valor, uma linha e uma coluna, um antigo e um novo), use BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueA superfície de combinadores é menor — and, or, negate existem, mas não há isEqual ou not para dois argumentos. Map.removeIf((k, v) -> ...) é exatamente um BiPredicate<K, V>.
Um exemplo prático: predicados, composição, a álgebra e onde se encaixam
O programa abaixo constrói três predicados simples sobre User, compõe-os com and/or/negate, demonstra o curto-circuito contando chamadas, substitui Predicate.not por negação em um ponto de chamada removeIf e usa um IntPredicate com um IntStream para mostrar a variante primitiva.
O que extrair da execução:
- Os três predicados base (
adult,active,namedWell) permaneceram reutilizáveis.eligible,minorereachableforam construídos por composição, em vez de escrever três lambdas separadas com lógica sobreposta. andfez curto-circuito exatamente como&&:expensiverodou menos vezes quecheapporque cada menor foi rejeitado antes da verificação custosa ser acionada. Essa é a alavanca que você tem para ordenar — coloque verificações baratas e que falham frequentemente primeiro.Predicate.not(...)no ponto de chamadaremoveIffoi lido como inglês simples ("remove if not non-blank") e evitou precisar de um tipo alvo antes da negação. Importar estaticamentenoté o toque final.Predicate.isEqual("foo")contou as duas entradas"foo"passando por umnullsem lançar exceção.s -> s.equals("foo")teria lançado NPE no elementonull.IntPredicate even = n -> n % 2 == 0;encaixou diretamente emIntStream.filtersem boxing — e o mesmo combinador.and(...)funciona na especialização primitiva.
O que vem a seguir
Predicate<T> responde sim ou não. O próximo capítulo, Interface Function do Java, aborda a interface para a outra metade do trabalho com streams: transformar um valor em outro. A forma — método único, composição com métodos default (andThen, compose, mais o estático identity()) — é a mesma de Predicate, e as mesmas lições sobre ordenação, reutilização e especializações primitivas se aplicam.