foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • 5
  • quiz
Java
  • Master Function, Predicate, Consumer, and Supplier
  • Learn to combine and chain functional interfaces
  • Understand binary variants and operators
  • Use primitive specializations to avoid boxing

Built-in Functional Interfaces

You've been using functional interfaces—Comparator, Runnable, Callable. Java 8 added a comprehensive set in java.util.function. Understanding these saves you from creating custom interfaces.

The Core Four Functional Interfaces

Function<T, R>

Takes one argument of type T, returns result of type R.

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Examples:

// String to Integer
Function<String, Integer> length = s -> s.length();
length.apply("Hello");  // 5

// Integer to String
Function<Integer, String> intToString = n -> "Number: " + n;
intToString.apply(42);  // "Number: 42"

// Object transformation
Function<Person, String> getName = Person::getName;

Chaining Functions:

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;

// Compose: first trim, then uppercase
Function<String, String> process = trim.andThen(upper);
process.apply("  hello  ");  // "HELLO"

// Or reverse order with compose
Function<String, String> process2 = upper.compose(trim);

Predicate

Takes one argument, returns boolean.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Examples:

Predicate<String> isEmpty = String::isEmpty;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Person> isAdult = p -> p.getAge() >= 18;

isEmpty.test("");      // true
isPositive.test(-5);   // false

Combining Predicates:

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;

// AND
Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
isPositiveAndEven.test(4);   // true
isPositiveAndEven.test(-4);  // false

// OR
Predicate<Integer> isPositiveOrEven = isPositive.or(isEven);
isPositiveOrEven.test(-4);  // true (even)

// NOT
Predicate<Integer> isNotPositive = isPositive.negate();
isNotPositive.test(-5);  // true

Consumer

Takes one argument, returns nothing (void).

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Examples:

Consumer<String> printer = System.out::println;
Consumer<List<String>> clearer = List::clear;
Consumer<Person> sendEmail = p -> emailService.send(p.getEmail());

printer.accept("Hello");  // prints "Hello"

Chaining Consumers:

Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> save = s -> database.save(s);

Consumer<String> logAndSave = log.andThen(save);
logAndSave.accept("Data");  // Logs then saves

Supplier

Takes no arguments, returns a value.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Examples:

Supplier<Double> random = Math::random;
Supplier<LocalDateTime> now = LocalDateTime::now;
Supplier<List<String>> listFactory = ArrayList::new;

random.get();       // 0.7234...
now.get();          // 2024-01-15T10:30:00
listFactory.get();  // new empty ArrayList

Use case - Lazy initialization:

public T getOrDefault(T value, Supplier<T> defaultSupplier) {
    return value != null ? value : defaultSupplier.get();
}

// Expensive default only computed if needed
String result = getOrDefault(null, () -> computeExpensiveDefault());

Binary Variants

BiFunction<T, U, R>

Takes two arguments, returns a result.

BiFunction<String, String, Integer> compare = String::compareTo;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = String::repeat;

compare.apply("a", "b");  // -1
add.apply(3, 4);          // 7
repeat.apply("ab", 3);    // "ababab"

BiPredicate<T, U>

Takes two arguments, returns boolean.

BiPredicate<String, String> startsWith = String::startsWith;
BiPredicate<Integer, Integer> greaterThan = (a, b) -> a > b;

startsWith.test("Hello", "He");  // true
greaterThan.test(5, 3);          // true

BiConsumer<T, U>

Takes two arguments, returns nothing.

BiConsumer<String, Integer> printRepeat = (s, n) -> {
    for (int i = 0; i < n; i++) System.out.println(s);
};

BiConsumer<Map<String, Integer>, String> incrementKey = 
    (map, key) -> map.merge(key, 1, Integer::sum);

printRepeat.accept("Hi", 3);  // prints "Hi" 3 times

Operator Variants

UnaryOperator

Special case of Function where input and output are same type.

// UnaryOperator<T> extends Function<T, T>
UnaryOperator<Integer> square = n -> n * n;
UnaryOperator<String> addBang = s -> s + "!";

square.apply(5);     // 25
addBang.apply("Hi"); // "Hi!"

// Used in replaceAll
List<String> names = new ArrayList<>(Arrays.asList("alice", "bob"));
names.replaceAll(String::toUpperCase);  // UnaryOperator<String>

BinaryOperator

Special case of BiFunction where all types are same.

// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> add = (a, b) -> a + b;
BinaryOperator<Integer> max = Math::max;
BinaryOperator<String> concat = String::concat;

add.apply(3, 4);        // 7
max.apply(10, 5);       // 10
concat.apply("a", "b"); // "ab"

// Used in reduce
int sum = numbers.stream().reduce(0, Integer::sum);  // BinaryOperator<Integer>

Primitive Specializations

To avoid boxing overhead, Java provides primitive variants:

For int

IntFunction<String> intToString = n -> "Value: " + n;
IntPredicate isEven = n -> n % 2 == 0;
IntConsumer printInt = System.out::println;
IntSupplier randomInt = () -> (int) (Math.random() * 100);
IntUnaryOperator doubleIt = n -> n * 2;
IntBinaryOperator sum = (a, b) -> a + b;

// Conversions
ToIntFunction<String> length = String::length;
IntToDoubleFunction toDouble = n -> n * 1.0;

For long and double

LongPredicate isPositive = n -> n > 0;
DoubleFunction<String> format = d -> String.format("%.2f", d);
ToLongFunction<String> parseLong = Long::parseLong;
ToDoubleFunction<Integer> toPercent = n -> n / 100.0;

Summary Table

Interface Parameters Returns Method
Function<T,R> T R apply(T)
BiFunction<T,U,R> T, U R apply(T,U)
Predicate<T> T boolean test(T)
BiPredicate<T,U> T, U boolean test(T,U)
Consumer<T> T void accept(T)
BiConsumer<T,U> T, U void accept(T,U)
Supplier<T> none T get()
UnaryOperator<T> T T apply(T)
BinaryOperator<T> T, T T apply(T,T)

Choosing the Right Interface

Need Use
Transform a value Function<T,R>
Test a condition Predicate<T>
Consume a value (side effect) Consumer<T>
Produce a value Supplier<T>
Transform same type UnaryOperator<T>
Combine two same-type values BinaryOperator<T>
Two inputs, one output BiFunction<T,U,R>
Avoid primitive boxing IntXxx, LongXxx, DoubleXxx

Java's built-in functional interfaces cover most cases. Function transforms, Predicate tests conditions, Consumer performs side effects, Supplier creates values lazily. Combine predicates with and(), or(), negate(). Chain functions with andThen(), compose(). Use primitive specializations to avoid boxing. Prefer built-in interfaces over creating your own.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service