Interfaccia Predicate di Java
Testa condizioni sui valori in Java con l'interfaccia funzionale Predicate e i suoi combinatori and/or/negate.
Predicate<T> è l'interfaccia funzionale per la domanda "questo valore va bene?" — un input di tipo T, una risposta boolean. Si trova al cuore di Stream.filter, Collection.removeIf, Optional.filter e di ogni metodo del JDK che dice "mantieni quelli che corrispondono." L'interfaccia è minima — un unico metodo test(T) — ma viene fornita con una piccola algebra di combinatori (and, or, negate, isEqual, not) che consente di costruire condizioni complesse a partire da quelle semplici, senza mai scrivere manualmente la logica booleana di collegamento.
Questo capitolo segue la stessa struttura degli altri approfondimenti sulle interfacce della Parte 12: l'interfaccia, i suoi tre o quattro metodi utili, l'algebra, e poi un esempio pratico.
L'interfaccia
L'intera dichiarazione, in forma semplificata:
@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 è l'unico metodo astratto che lambda e riferimenti a metodi implementano. Tutto il resto è costruito sopra di esso. Raramente chiamerai test direttamente — stream().filter(...) e list.removeIf(...) lo chiamano per te — ma conoscere il nome del metodo è importante quando scrivi codice che accetta un Predicate<T> e deve invocarlo.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — algebra booleana senza il codice di collegamento
I tre metodi default compongono i predicati allo stesso modo in cui gli operatori &&, ||, ! compongono i booleani:
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();Due proprietà sono importanti:
- Cortocircuito, nell'ordine di dichiarazione.
a.and(b)chiamab.testsolo quandoa.testha restituitotrue.a.or(b)chiamab.testsolo quandoa.testha restituitofalse. È lo stesso ordine di valutazione di&&e||, il che significa che puoi mettere prima i controlli economici e a frequente fallimento, e quelli costosi alla fine. - Ogni chiamata restituisce un nuovo
Predicate. I combinatori non mutanothis. Riutilizza gli originali quanto vuoi.
negate() inverte semplicemente il risultato. useful.negate() restituisce true per null, valori vuoti e stringhe più corte di 3 — ogni caso che useful aveva rifiutato.
Predicate.not — la negazione leggibile
Java 11 ha aggiunto una scorciatoia statica:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) fornisce la stessa risposta boolean di p.negate(), ma si compone in modo molto più naturale nel punto di chiamata. La forma con riferimento a metodo String::isBlank è già un Predicate<String> — ma non puoi scrivere (String::isBlank).negate(), perché il compilatore ha bisogno di un tipo target prima di poter risolvere il riferimento. Predicate.not(String::isBlank) gli fornisce quel tipo target, e il tutto si legge come "non blank" nell'ordine naturale.
Un import statico di Predicate.not rende le catene di filtri ancora più pulite:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — uguaglianza null-safe
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")L'implementazione è letteralmente t -> Objects.equals(target, t), il che significa che null su entrambi i lati viene confrontato in modo sicuro. Raramente risparmia battute rispetto a s -> s.equals("foo"), ma ti salva quando lo stream potrebbe contenere null — null.equals("foo") lancerebbe una NPE, mentre Objects.equals(null, "foo") restituisce false.
Dove appare Predicate<T> nel JDK
Lo stesso Predicate<T> scorre attraverso ogni API di "filtraggio":
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.removeIfOgnuno di questi ha la stessa forma, quindi un Predicate<T> costruito una volta è riutilizzabile in ogni direzione — e assemblarlo con and/or/negate è esattamente il modo per evitare il problema dei "tre filtri leggermente diversi, tutti quasi duplicati."
Specializzazioni primitive — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> funziona con gli int, ma ogni chiamata esegue il boxing dell'input. Per pipeline numeriche intensive il pacchetto include:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Stessa algebra and/or/negate, nessun boxing. Queste sono ciò che IntStream.filter accetta — usare Predicate<Integer> lì costringerebbe lo stream a fare l'autoboxing di ogni elemento in ingresso.
BiPredicate<T, U> — test a due argomenti
Quando la domanda richiede due input (una chiave e un valore, una riga e una colonna, un vecchio e un nuovo), usa BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueLa superficie dei combinatori è più ridotta — and, or, negate esistono, ma non c'è un isEqual o un not a due argomenti. Map.removeIf((k, v) -> ...) è esattamente un BiPredicate<K, V>.
Un esempio pratico: predicati, composizione, l'algebra e dove si collegano
Il programma seguente costruisce tre semplici predicati su User, li compone con and/or/negate, dimostra il cortocircuito contando le chiamate, sostituisce Predicate.not per la negazione in un punto di chiamata removeIf, e usa un IntPredicate con un IntStream per mostrare la variante primitiva.
Cosa ricavare dall'esecuzione:
- I tre predicati di base (
adult,active,namedWell) sono rimasti riutilizzabili.eligible,minorereachablesono stati costruiti per composizione anziché scrivendo tre lambda separate con logica sovrapposta. andha cortocircuitato esattamente come fa&&:expensiveè stato eseguito meno volte dicheapperché ogni minore veniva rifiutato prima che scattasse il controllo costoso. Questo è il meccanismo di ottimizzazione disponibile attraverso l'ordine — metti prima i controlli economici e a frequente fallimento.Predicate.not(...)nel punto di chiamataremoveIfsi leggeva come inglese semplice ("remove if not non-blank") ed evitava la necessità di un tipo target prima della negazione. L'import statico dinotè il piccolo tocco finale.Predicate.isEqual("foo")ha contato le due voci"foo"passando oltre unnullsenza lanciare eccezioni.s -> s.equals("foo")avrebbe generato una NPE sull'elementonull.IntPredicate even = n -> n % 2 == 0;si è collegato direttamente aIntStream.filtersenza boxing — e lo stesso combinatore.and(...)funziona sulla specializzazione primitiva.
Cosa viene dopo
Predicate<T> risponde sì o no. Il capitolo successivo, Interfaccia Function di Java, tratta l'interfaccia per l'altra metà del lavoro con gli stream: trasformare un valore in un altro. La forma — metodo singolo, composizione con metodi default (andThen, compose, più lo statico identity()) — è la stessa di Predicate, e le stesse lezioni sull'ordine, il riutilizzo e le specializzazioni primitive si applicano anche qui.