Definition
- Predicate
: boolean test(T t)takes input, returns boolean. Used for filtering and condition checks. Composition:and(),or(),negate(),Predicate.not(). - Function
: R apply(T t)takes input, returns output of different type. Used for transformation and mapping. Composition:andThen(),compose(),Function.identity(). - Consumer
: void accept(T t)takes input, returns nothing. Used for side effects like logging and notifications. Composition:andThen(). - Supplier
: T get()takes nothing, returns output. Used for lazy creation and factory patterns. No composition methods. - BiFunction
: two inputs to one output. UnaryOperator : same-type transformation (extends Function). BinaryOperator : same-type reduction (extends BiFunction).
Think of four desks at National Bank of India. The Predicate desk is the verification clerk: "Is this account active? YES or NO." Takes an account, returns boolean. The Function desk is the exchange counter: "Convert this dollar cheque to rupees." Takes one type, returns another. The Consumer desk is the stamp counter: "Stamp this cheque as processed." Takes a cheque, returns nothing. The Supplier desk is the token machine: "Press button, get a queue number." Takes nothing, produces a value.
Understanding the Concepts
What are the differences between Predicate, Function, Supplier, and Consumer?
How does each interface work?
| Interface | Signature | Input | Output | Purpose | Stream Usage |
|-----------|-----------|-------|--------|---------|-------------|
| Predicate
| Function
| Consumer
| Supplier
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.*;
public class FourPillarsDemo {
record Account(String id, String holder, BigDecimal balance, boolean active) {}
public static void main(String[] args) {
List<Account> accounts = List.of(
new Account("SAV2001", "Rajesh", new BigDecimal("125000"), true),
new Account("SAV2002", "Priya", new BigDecimal("8000"), false),
new Account("CUR3001", "Amit", new BigDecimal("250000"), true)
);
// Predicate: T -> boolean
Predicate<Account> isActive = Account::active;
Predicate<Account> isLarge = a -> a.balance().compareTo(new BigDecimal("100000")) > 0;
// Function: T -> R
Function<Account, String> toSummary = a -> a.id() + ": Rs." + a.balance();
// Consumer: T -> void
Consumer<Account> logAccount = a -> System.out.println("Processed: " + a.id());
// Supplier: () -> T
Supplier<String> txnId = () -> "TXN-" + System.currentTimeMillis();
// All four in one pipeline:
System.out.println("Report ID: " + txnId.get());
accounts.stream()
.filter(isActive.and(isLarge)) // Predicate
.map(toSummary) // Function
.forEach(System.out::println); // Consumer
}
}
Report ID: TXN-1705312800000
SAV2001: Rs.125000
CUR3001: Rs.250000
How does composition work for each?
package com.bank.service;
import java.math.BigDecimal;
import java.util.function.*;
public class CompositionDemo {
public static void main(String[] args) {
// Predicate composition: and, or, negate
Predicate<BigDecimal> positive = amt -> amt.signum() > 0;
Predicate<BigDecimal> large = amt -> amt.compareTo(new BigDecimal("100000")) > 0;
Predicate<BigDecimal> positiveLarge = positive.and(large); // AND
Predicate<BigDecimal> smallOrNeg = positiveLarge.negate(); // NOT
Predicate<BigDecimal> positiveOrLarge = positive.or(large); // OR
// Function composition: andThen, compose
Function<String, BigDecimal> parse = BigDecimal::new;
Function<BigDecimal, String> format = amt -> "Rs." + amt.toPlainString();
Function<String, String> parseAndFormat = parse.andThen(format); // parse first, then format
System.out.println(parseAndFormat.apply("50000")); // Rs.50000
// Consumer composition: andThen
Consumer<String> log = msg -> System.out.println("LOG: " + msg);
Consumer<String> audit = msg -> System.out.println("AUDIT: " + msg);
Consumer<String> both = log.andThen(audit); // log first, then audit
both.accept("Transfer completed");
}
}
Rs.50000
LOG: Transfer completed
AUDIT: Transfer completed
What is the extended family?
package com.bank.service;
import java.math.BigDecimal;
import java.util.function.*;
public class ExtendedFamilyDemo {
record Account(String id, BigDecimal balance) {}
public static void main(String[] args) {
// BiFunction: two inputs -> one output
BiFunction<Account, BigDecimal, Account> applyInterest =
(acc, rate) -> new Account(acc.id(), acc.balance().multiply(rate.add(BigDecimal.ONE)));
// UnaryOperator: same type in and out (extends Function<T,T>)
UnaryOperator<BigDecimal> applyGST = amt -> amt.multiply(new BigDecimal("1.18"));
// BinaryOperator: same type reduction (extends BiFunction<T,T,T>)
BinaryOperator<BigDecimal> sum = BigDecimal::add;
BigDecimal total = java.util.List.of(
new BigDecimal("1000"), new BigDecimal("2000"), new BigDecimal("3000")
).stream().reduce(BigDecimal.ZERO, sum);
System.out.println("Total: Rs." + total);
System.out.println("With GST: Rs." + applyGST.apply(total));
}
}
Total: Rs.6000
With GST: Rs.7080.00
Where is each used in the Stream API?
| Interface | Stream Method | Example |
|-----------|--------------|--------|
| Predicate | filter() | .filter(Account::active) |
| Function | map() | .map(Account::balance) |
| Consumer | forEach() | .forEach(System.out::println) |
| Supplier | generate() | Stream.generate(UUID::randomUUID) |
| BinaryOperator | reduce() | .reduce(ZERO, BigDecimal::add) |
| Comparator | sorted() | .sorted(Comparator.comparing(...)) |
What are common mistakes?
Using Consumer where Function is needed (returning void when a value is expected). Confusing Function and UnaryOperator (use UnaryOperator when input and output types are the same). Using Supplier with expensive operations in orElse instead of orElseGet.
What should a senior engineer know?
The seven core interfaces plus their Bi variants cover all common lambda shapes. Creating custom functional interfaces is appropriate only when the domain name adds clarity. Named predicates stored as static factory methods are reusable and testable. Composition replaces class hierarchies for combining simple behaviours into complex ones.
Visual Overview
Four Core + Extended Family: ──────────────────────────── Predicate<T> T ──→ boolean filter() Function<T,R> T ──→ R map() Consumer<T> T ──→ void forEach() Supplier<T> () ──→ T generate() ──────────────────────────── BiFunction<T,U,R> (T,U) ──→ R Map.compute() UnaryOperator<T> T ──→ T replaceAll() BinaryOperator<T> (T,T) ──→ T reduce() BiPredicate<T,U> (T,U) ──→ boolean BiConsumer<T,U> (T,U) ──→ void Map.forEach()
Key Takeaway
The four core functional interfaces cover all common lambda shapes. Predicate takes T and returns boolean for filtering. Function takes T and returns R for transformation. Consumer takes T and returns void for side effects. Supplier takes nothing and returns T for lazy creation. Each supports composition methods that build complex behaviour from simple building blocks.
The extended family covers multi-argument and same-type cases. BiFunction takes two inputs and returns one output. UnaryOperator extends Function where input and output are the same type, used in replaceAll. BinaryOperator extends BiFunction where all three types are the same, used in reduce. Together with the core four, these eight interfaces cover every function signature in Java programming.
In Rajesh's portfolio analysis service at National Bank of India, all four work together in one pipeline: Supplier generates a unique report ID, Predicate filters active accounts, Function transforms Account entities into summary strings, and Consumer logs each processed account. BinaryOperator (BigDecimal::add) reduces all balances into a total.
Remember one sentence: Predicate filters, Function transforms, Consumer acts, Supplier creates, and together they cover every lambda shape.
---