W3docs

Интерфейс 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");          // true

and, 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 string

Predicate.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"), но спасает вас, когда поток может содержать nullnull.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 для демонстрации примитивного варианта.

java— editable, runs on the server

Что следует вынести из выполнения:

  • Три базовых предиката (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, и те же уроки о порядке, переиспользовании и примитивных специализациях применимы здесь.

Практика

Практика
У вас есть `Predicate<String> notNull = Objects::nonNull;` и `Predicate<String> notBlank = s -> !s.isBlank();`, и вы хотите получить один предикат, возвращающий `true` только когда строка одновременно не null и не пустая, причём `notNull` проверяется *первым*, чтобы `notBlank` никогда не вызывался для `null`. Какое выражение делает это правильно?
У вас есть `Predicate<String> notNull = Objects::nonNull;` и `Predicate<String> notBlank = s -> !s.isBlank();`, и вы хотите получить один предикат, возвращающий `true` только когда строка одновременно не null и не пустая, причём `notNull` проверяется *первым*, чтобы `notBlank` никогда не вызывался для `null`. Какое выражение делает это правильно?
Was this page helpful?