Back to Blog
Java

Java Predicate vs Function: Key Differences

java predicate vs function: Compare Java's Predicate<T> and Function<T,R> interfaces: their signatures, return types, composition methods, and when to use each in real...

Java functional interfacesjava.util.functionLambda expressionsStream APIPredicate interfaceFunction interface
A visual comparison of Java's Predicate and Function interfaces showing boolean output versus value transformation.

Choosing between Predicate<T> and Function<T, R> is a common decision when working with Java's java.util.function package. The java predicate vs function question comes down to one fundamental difference: a Predicate<T> always returns a boolean, while a Function<T, R> returns a value of type R. That single difference drives how each interface is used in production code.

The Core Difference: boolean vs. Result Value

Predicate<T> has a single abstract method test(T t) that returns boolean. It answers a yes-or-no question about an input: is this value valid, does it match a condition, should it be included in a result set?

Function<T, R> has a single abstract method apply(T t) that returns R. It transforms an input into an output of a potentially different type. The transformation can be anything: extracting a field, converting a format, or computing a derived value.

Predicate<String> isLongEnough = s -> s.length() >= 8; boolean result = isLongEnough.test("password123"); // true Function<String, Integer> stringLength = s -> s.length(); Integer length = stringLength.apply("password123"); // 11

The return type is the defining characteristic. Predicate is for classification and filtering. Function is for mapping and transformation. When a method accepts a Predicate, it uses the result to make a decision. When a method accepts a Function, it uses the result as a new value.

Method Signatures and Default Methods

Both interfaces belong to java.util.function and were introduced in Java 8. Their abstract methods differ only in return type, but that difference shapes the available composition methods.

InterfaceAbstract methodReturn typePurpose
Predicate<T>boolean test(T t)booleanTest a condition
Function<T, R>R apply(T t)RTransform a value

Predicate<T> provides default methods for logical composition: and, or, and negate. These combine predicates into larger boolean expressions, mirroring the &&, ||, and ! operators.

Function<T, R> provides andThen and compose for chaining transformations. andThen applies another function to the result of the first, while compose applies another function to the input before the original runs.

Predicate<String> hasLetter = s -> s.chars().anyMatch(Character::isLetter); Predicate<String> hasDigit = s -> s.chars().anyMatch(Character::isDigit); Predicate<String> isStrong = hasLetter.and(hasDigit); Function<String, String> trim = String::trim; Function<String, Integer> parse = Integer::parseInt; Function<String, Integer> parseTrimmed = trim.andThen(parse);

The composition methods reflect the underlying semantics. Predicates combine boolean conditions with logical operators. Functions chain data transformations in a pipeline.

Predicate in Stream Filtering and Validation

The most common use of Predicate is with the Stream API's filter method. Each element is tested against the predicate, and only elements that return true are kept.

List<Product> products = getProducts(); List<Product> available = products.stream() .filter(p -> p.getStock() > 0) .filter(p -> p.getPrice() < 100.0) .toList();

Predicates also appear in validation logic. A collection of predicates can represent a set of business rules, and each rule can be evaluated independently or combined.

Predicate<Order> hasValidCustomer = o -> o.getCustomerId() != null; Predicate<Order> hasPositiveTotal = o -> o.getTotal() > 0; Predicate<Order> isShippable = hasValidCustomer.and(hasPositiveTotal);

This approach keeps validation rules declarative and reusable. The same predicate can be applied across different code paths without duplicating the condition logic.

Function for Mapping and Transformation

Function is the standard choice when a value needs to be converted from one representation to another. The Stream API's map method is the canonical example.

List<User> users = getUsers(); List<String> emails = users.stream() .map(User::getEmail) .toList();

Function also appears in Comparator.comparing, where a key extractor determines the sort order.

List<User> sorted = users.stream() .sorted(Comparator.comparing(User::getLastName)) .toList();

Here the function extracts the last name from each user, and the comparator uses the extracted value for ordering. The function does not decide whether a user is included; it only produces the value used for comparison.

Composition Order and Readability

The default methods on Function can be a source of confusion because compose and andThen apply functions in different orders.

Function<Integer, Integer> multiplyByTwo = x -> x * 2; Function<Integer, Integer> subtractOne = x -> x - 1; // andThen: multiplyByTwo first, then subtractOne Function<Integer, Integer> a = multiplyByTwo.andThen(subtractOne); // a.apply(5) = (5 * 2) - 1 = 9 // compose: subtractOne first, then multiplyByTwo Function<Integer, Integer> b = multiplyByTwo.compose(subtractOne); // b.apply(5) = (5 - 1) * 2 = 8

andThen reads left to right: the first function runs, then the second. compose reads right to left: the argument function runs first, then the original. For most code, andThen is easier to follow because execution order matches reading order.

Predicates do not have this ambiguity. and, or, and negate behave exactly like their boolean operator counterparts. There is no ordering question because the result is always a boolean.

Runtime Cost and Allocation Considerations

Both Predicate and Function are functional interfaces, so they can be implemented with lambda expressions or method references. When a lambda captures no external variables, the JVM can represent it as a singleton instance. When a lambda captures local variables or fields, a new instance is allocated each time the lambda expression is evaluated.

This matters in hot paths. A predicate that captures a threshold value from an enclosing scope will allocate a new instance on each evaluation. In a tight loop over a large collection, that allocation can add up.

// Captures threshold, so a new instance is created per evaluation double threshold = 50.0; products.stream() .filter(p -> p.getPrice() < threshold) .toList();

The practical impact is usually small because modern JVMs handle short-lived allocations efficiently. But if a predicate or function is evaluated millions of times in a latency-sensitive path, hoisting the lambda into a static field or a local variable created once can reduce allocation pressure.

private static final Predicate<Product> UNDER_50 = p -> p.getPrice() < 50.0;

This is a micro-optimization. Apply it only when profiling shows allocation is a real problem. The default behavior is acceptable for most application code.

Choosing Between Predicate and Function

The decision is straightforward once the return type is clear. If the code needs a yes-or-no answer about an input, use Predicate<T>. If the code needs to produce a new value from an input, use Function<T, R>.

Consider a method that accepts a condition versus one that accepts a transformation:

// A condition: boolean result public List<Product> filterProducts(Predicate<Product> condition) { return products.stream().filter(condition).toList(); } // A transformation: new value result public List<String> extractNames(Function<Product, String> extractor) { return products.stream().map(extractor).toList(); }

The method signature communicates intent. A Predicate parameter tells callers the method will decide something. A Function parameter tells callers the method will convert something. Mixing them up produces code that is confusing to read and often requires awkward workarounds.

A common mistake is using Function<T, Boolean> instead of Predicate<T>. While both can represent a boolean-producing operation, Predicate provides and, or, and negate, which are not available on Function. Using Function<T, Boolean> forces manual boolean composition and loses the semantic clarity of a predicate.

Where the Interfaces Appear in the JDK

The JDK uses these interfaces in distinct places. Stream.filter, Stream.anyMatch, Stream.allMatch, and Stream.noneMatch all accept Predicate. Stream.map, Stream.flatMap, Comparator.comparing, and Map.computeIfAbsent accept Function or its specialized variants.

Map.computeIfAbsent is a useful example because it shows how a function produces a value only when one is missing:

Map<String, List<Integer>> cache = new HashMap<>(); List<Integer> values = cache.computeIfAbsent("key", k -> new ArrayList<>());

The function receives the key and returns a new list. If the key is already present, the function is never called. This is a transformation use case: the function produces the default value.

The specialized variants IntPredicate, LongPredicate, DoublePredicate, IntFunction, LongFunction, and DoubleFunction avoid boxing when working with primitive types. For primitive-heavy code, these variants reduce allocation overhead compared to the generic Predicate<T> and Function<T, R>.

java predicate vs function: Practical Usage and Code Example | RYUSLOG DEV