Интерфейс Java Predicate
Проверяйте условия на значениях в Java с помощью функционального интерфейса Predicate и его комбинаторов and/or/negate.
Predicate<T> — это функциональный интерфейс для вопроса «подходит ли это значение?»: один входной параметр типа T и один ответ типа boolean. Он лежит в основе Stream.filter, Collection.removeIf, Optional.filter и каждого метода JDK, который говорит «оставить те, что совпадают». Интерфейс крошечный — один метод test(T) — но поставляется с небольшой алгеброй комбинаторов (and, or, negate, isEqual, not), которая позволяет строить сложные условия из простых без написания булевой «склейки» вручную.
Эта глава построена так же, как остальные детальные разборы интерфейсов в части 12: сам интерфейс, три-четыре полезных метода, алгебра, затем практический пример.
Интерфейс
Полное объявление в упрощённом виде:
@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 — единственный абстрактный метод, который реализуют лямбды и ссылки на методы. Всё остальное построено поверх него. Вы редко будете вызывать test напрямую — это делают за вас stream().filter(...) и list.removeIf(...) — но знать имя метода важно, когда вы пишете код, который принимает Predicate<T> и должен его вызвать.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — булева алгебра без склейки
Три метода по умолчанию компонуют предикаты так же, как операторы &&, ||, ! компонуют булевы значения:
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();Два важных свойства:
- Сокращённое вычисление в порядке объявления.
a.and(b)вызываетb.testтолько еслиa.testвернулtrue.a.or(b)вызываетb.testтолько еслиa.testвернулfalse. Это тот же порядок вычисления, что у&&и||, а значит дешёвые, часто «отсеивающие» проверки можно ставить первыми, а дорогие — последними. - Каждый вызов возвращает новый
Predicate. Комбинаторы не изменяютthis. Исходные предикаты можно переиспользовать сколько угодно.
negate() просто инвертирует результат. useful.negate() вернёт true для null, пустых строк и строк короче 3 символов — то есть для всех случаев, которые useful отклонил.
Predicate.not — читаемое отрицание
В Java 11 добавлено статическое сокращение:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) даёт тот же boolean-ответ, что и p.negate(), но значительно естественнее читается на месте вызова. Ссылка на метод String::isBlank сама по себе является Predicate<String> — но написать (String::isBlank).negate() нельзя, потому что компилятору нужен целевой тип прежде, чем он сможет разрешить ссылку. Predicate.not(String::isBlank) предоставляет этот целевой тип, и всё вместе читается как «не пустой» в привычном порядке.
Статический import Predicate.not делает цепочки фильтров ещё чище:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — null-безопасное равенство
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")Реализация буквально такая: t -> Objects.equals(target, t), что означает: null с любой стороны сравнивается безопасно. Это редко экономит нажатия клавиш по сравнению с s -> s.equals("foo"), но спасает вас, когда поток может содержать null — null.equals("foo") выбросит NPE, а Objects.equals(null, "foo") вернёт false.
Где встречается Predicate<T> в JDK
Один и тот же Predicate<T> используется во всех 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.removeIfВсе они принимают одинаковую форму, поэтому Predicate<T>, построенный один раз, переиспользуется в любом направлении — а составление его через and/or/negate — именно то, что помогает избежать запаха «у меня три почти одинаковых фильтра с дублирующейся логикой».
Примитивные специализации — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> работает с int, но каждый вызов упаковывает входное значение. Для интенсивных числовых конвейеров пакет предоставляет:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Та же алгебра and/or/negate, без упаковки. Именно их принимает IntStream.filter — использование Predicate<Integer> там заставило бы поток выполнять автоупаковку каждого элемента на входе.
BiPredicate<T, U> — проверки с двумя аргументами
Когда вопрос требует двух входных значений (ключа и значения, строки и столбца, старого и нового), используйте BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueНабор комбинаторов меньше — and, or, negate есть, но двухаргументного isEqual или not нет. Map.removeIf((k, v) -> ...) — это именно BiPredicate<K, V>.
Практический пример: предикаты, композиция, алгебра и их применение
Программа ниже строит три простых предиката над User, компонует их через and/or/negate, демонстрирует сокращённое вычисление посчётом вызовов, заменяет отрицание на Predicate.not в месте вызова removeIf и использует IntPredicate с IntStream для демонстрации примитивного варианта.
Что следует вынести из выполнения:
- Три базовых предиката (
adult,active,namedWell) остались переиспользуемыми.eligible,minorиreachableбыли построены через композицию, а не написанием трёх отдельных лямбд с перекрывающейся логикой. andвыполнил сокращённое вычисление ровно так же, как&&:expensiveвызывался реже, чемcheap, потому что каждый несовершеннолетний отсеивался ещё до срабатывания дорогой проверки. Именно в этом и состоит рычаг управления порядком — ставьте дешёвые, часто отсеивающие проверки первыми.Predicate.not(...)на месте вызоваremoveIfчитался как обычный английский текст («remove if not non-blank») и позволял избежать необходимости указывать целевой тип перед отрицанием. Статический импортnot— небольшой завершающий штрих.Predicate.isEqual("foo")подсчитал два вхождения"foo"в наличииnullбез выброса исключения.s -> s.equals("foo")выбросил бы NPE на элементеnull.IntPredicate even = n -> n % 2 == 0;напрямую подключился кIntStream.filterбез упаковки — и тот же комбинатор.and(...)работает для примитивной специализации.
Что дальше
Predicate<T> отвечает «да» или «нет». Следующая глава, Интерфейс Java Function, рассматривает интерфейс для другой половины потоковой обработки: преобразования одного значения в другое. Форма — один абстрактный метод, композиция через методы по умолчанию (andThen, compose, плюс статический identity()) — та же, что у Predicate, и те же уроки о порядке, переиспользовании и примитивных специализациях применимы здесь.