Java Predicate-Interface
Bedingungen in Java mit dem Predicate-Interface und seinen Kombinatoren and/or/negate testen.
Predicate<T> ist das funktionale Interface für die Frage „Ist dieser Wert in Ordnung?" — eine Eingabe vom Typ T, eine boolean-Antwort. Es steht im Mittelpunkt von Stream.filter, Collection.removeIf, Optional.filter und jeder JDK-Methode, die bedeutet „behalte die passenden Elemente." Das Interface ist klein — eine einzige test(T)-Methode — enthält aber eine kleine Kombinatoralgebra (and, or, negate, isEqual, not), mit der sich komplexe Bedingungen aus einfachen zusammensetzen lassen, ohne je selbst den booleschen Verbindungscode schreiben zu müssen.
Dieses Kapitel hat dieselbe Struktur wie die übrigen Interface-Vertiefungen in Teil 12: das Interface, seine drei oder vier nützlichen Methoden, die Algebra und ein durchgearbeitetes Beispiel.
Das Interface
Die gesamte Deklaration, vereinfacht:
@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 ist die einzige abstrakte Methode, die Lambdas und Methodenreferenzen implementieren. Alles andere baut darauf auf. Sie werden test selten direkt aufrufen — stream().filter(...) und list.removeIf(...) erledigen das für Sie — aber der Methodenname ist wichtig, wenn Sie Code schreiben, der ein Predicate<T> entgegennimmt und es aufrufen muss.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — boolesche Algebra ohne Verbindungscode
Die drei Default-Methoden kombinieren Predicates auf die gleiche Weise, wie die Operatoren &&, ||, ! Booleans kombinieren:
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();Zwei Eigenschaften sind wichtig:
- Kurzschlussauswertung in Deklarationsreihenfolge.
a.and(b)ruftb.testnur auf, wenna.testtruezurückgegeben hat.a.or(b)ruftb.testnur auf, wenna.testfalsezurückgegeben hat. Das entspricht der Auswertungsreihenfolge von&&und||, was bedeutet, dass günstige und häufig schlagende Prüfungen zuerst stehen können und die aufwändigen zuletzt. - Jeder Aufruf gibt ein neues
Predicatezurück. Die Kombinatoren mutierenthisnicht. Verwenden Sie die Originale so oft wie nötig weiter.
negate() kehrt das Ergebnis einfach um. useful.negate() gibt true für Nullwerte, Leerzeilen und Strings kürzer als 3 zurück — jeden Fall, den useful abgelehnt hat.
Predicate.not — die lesbare Negation
Java 11 hat eine statische Kurzform hinzugefügt:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) liefert dieselbe boolean-Antwort wie p.negate(), lässt sich aber an der Aufrufstelle viel natürlicher zusammensetzen. Die Methodenreferenz String::isBlank ist für sich allein ein Predicate<String> — aber (String::isBlank).negate() können Sie nicht schreiben, weil der Compiler einen Zieltyp benötigt, bevor er die Referenz auflösen kann. Predicate.not(String::isBlank) liefert diesen Zieltyp, und das Ganze liest sich wie „nicht leer" in der englischen Reihenfolge.
Ein statischer import von Predicate.not macht Filter-Chains noch klarer:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — null-sichere Gleichheit
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")Die Implementierung lautet buchstäblich t -> Objects.equals(target, t), was bedeutet, dass null auf beiden Seiten sicher verglichen wird. Sie spart selten Zeichen gegenüber s -> s.equals("foo"), schützt aber vor Fehlern, wenn der Stream null enthalten kann — null.equals("foo") würde eine NPE werfen, Objects.equals(null, "foo") gibt false zurück.
Wo Predicate<T> im JDK auftaucht
Dasselbe Predicate<T> fließt durch jede „Filter"-API:
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.removeIfJede dieser Methoden hat dieselbe Form, sodass ein einmal erstelltes Predicate<T> in jede Richtung wiederverwendbar ist — und die Zusammensetzung mit and/or/negate ist genau der Weg, den Geruch von „drei leicht unterschiedlichen Filtern, alle fast-Duplikate" zu vermeiden.
Primitive Spezialisierungen — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> funktioniert mit int-Werten, boxt aber jeden Eingabewert. Für enge numerische Pipelines liefert das Paket:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Dieselbe and/or/negate-Algebra, kein Boxing. Diese sind das, was IntStream.filter akzeptiert — Predicate<Integer> dort zu verwenden würde den Stream zwingen, jedes Element beim Eingang per Autoboxing zu konvertieren.
BiPredicate<T, U> — Tests mit zwei Argumenten
Wenn die Frage zwei Eingaben nimmt (einen Schlüssel und einen Wert, eine Zeile und eine Spalte, einen alten und einen neuen Wert), verwenden Sie BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueDie Kombinatorfläche ist kleiner — and, or, negate sind vorhanden, aber es gibt kein zweiargumentiges isEqual oder not. Map.removeIf((k, v) -> ...) ist genau ein BiPredicate<K, V>.
Ein durchgearbeitetes Beispiel: Predicates, Komposition, die Algebra und wo sie eingebunden werden
Das folgende Programm erstellt drei kleine Predicates über User, kombiniert sie mit and/or/negate, demonstriert die Kurzschlussauswertung durch das Zählen von Aufrufen, verwendet Predicate.not für die Negation an einer removeIf-Aufrufstelle und nutzt ein IntPredicate mit einem IntStream, um die primitive Variante zu zeigen.
Was der Durchlauf zeigt:
- Die drei Bausteine-Predicates (
adult,active,namedWell) blieben wiederverwendbar.eligible,minorundreachablewurden durch Komposition erstellt, anstatt drei separate Lambdas mit überlappender Logik zu schreiben. andhat genau so kurzgeschlossen wie&&:expensivelief seltener alscheap, weil jeder Minderjährige abgelehnt wurde, bevor die aufwändige Prüfung ausgelöst wurde. Das ist der Hebel für die Reihenfolge — günstige, häufig schlagende Prüfungen zuerst.Predicate.not(...)an derremoveIf-Aufrufstelle las sich wie normales Englisch ("remove if not non-blank") und vermied das Bedürfnis nach einem Zieltyp vor der Negation. Der statischeimportvonnotist die kleine abschließende Verbesserung.Predicate.isEqual("foo")zählte die zwei"foo"-Einträge über einemnullhinweg ohne Fehler.s -> s.equals("foo")hätte beimnull-Element eine NPE geworfen.IntPredicate even = n -> n % 2 == 0;wurde direkt inIntStream.filtereingebunden ohne Boxing — und derselbe.and(...)-Kombinator funktioniert auch auf der primitiven Spezialisierung.
Was kommt als Nächstes
Predicate<T> antwortet mit ja oder nein. Das nächste Kapitel, Java Function-Interface, behandelt das Interface für die andere Hälfte der Stream-Arbeit: einen Wert in einen anderen transformieren. Die Form — einzige Methode, Default-Methoden-Komposition (andThen, compose, plus das statische identity()) — ist dieselbe wie bei Predicate, und dieselben Lektionen über Reihenfolge, Wiederverwendung und primitive Spezialisierungen gelten gleichermaßen.