Skip to content
intermediate Phase 7 · Java 8+ Features

Functional Interfaces

Use Predicate, Function, Consumer, Supplier for functional programming.

45m
2 problems
Topic Progress 0%

Predicate

Predicate

Predicate<T> is a functional interface that takes a value of type T and returns a boolean. It is used for testing conditions.

Method: boolean test(T t)

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.*;

public class PredicateDemo {
    public static void main(String[] args) {
        // Basic predicate
        Predicate<String> isEmpty = String::isEmpty;
        Predicate<String> isNotEmpty = isEmpty.negate();

        System.out.println("Empty? " + isEmpty.test("")); // true
        System.out.println("Not empty? " + isNotEmpty.test("hello")); // true

        // Predicate chaining
        Predicate<String> startsWithH = s -> s.startsWith("H");
        Predicate<String> hasLength5 = s -> s.length() == 5;
        Predicate<String> startsWithHAndLength5 = startsWithH.and(hasLength5);
        Predicate<String> startsWithHOrLength5 = startsWithH.or(hasLength5);

        System.out.println("Hello matches both: " + startsWithHAndLength5.test("Hello")); // true
        System.out.println("Hi matches either: " + startsWithHOrLength5.test("Hi")); // true

        // Using predicates with collections
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

        // Filter with predicate
        List<String> longNames = names.stream()
            .filter(s -> s.length() > 4)
            .collect(Collectors.toList());
        System.out.println("Long names: " + longNames); // [Alice, Charlie, David]

        // Compose predicates
        Predicate<String> isShort = s -> s.length() <= 3;
        Predicate<String> startsWithB = s -> s.startsWith("B");
        List<String> shortOrStartsWithB = names.stream()
            .filter(isShort.or(startsWithB))
            .collect(Collectors.toList());
        System.out.println("Short or B: " + shortOrStartsWithB); // [Bob, Eve]

        // Practical: validation
        Predicate<String> validEmail = s -> s != null && s.contains("@") && s.contains(".");
        Predicate<Integer> validAge = age -> age >= 0 && age <= 150;
        Predicate<String> validPassword = s -> s != null && s.length() >= 8
            && s.matches(".*[A-Z].*") && s.matches(".*[0-9].*");

        System.out.println("Valid email: " + validEmail.test("user@example.com")); // true
        System.out.println("Valid age: " + validAge.test(25)); // true
        System.out.println("Valid password: " + validPassword.test("Pass1234")); // true
    }
}

Predicate methods:

  • test(T t) — evaluate the predicate
  • and(Predicate) — logical AND
  • or(Predicate) — logical OR
  • negate() — logical NOT
  • isEqual(Object) — equality predicate

Function

Function<T, R>

Function<T, R> takes a value of type T and returns a value of type R. It is used for transformations.

Method: R apply(T t)

import java.util.*;
import java.util.function.Function;
import java.util.stream.*;

public class FunctionDemo {
    public static void main(String[] args) {
        // Basic function
        Function<String, Integer> toLength = String::length;
        Function<String, String> toUpper = String::toUpperCase;
        Function<String, String> toLower = String::toLowerCase;

        System.out.println("Length of Hello: " + toLength.apply("Hello")); // 5
        System.out.println("Upper: " + toUpper.apply("hello")); // HELLO

        // Function chaining
        Function<String, String> trim = String::trim;
        Function<String, String> upper = String::toUpperCase;
        Function<String, String> process = trim.andThen(upper);
        System.out.println("Processed: " + process.apply("  hello  ")); // HELLO

        // compose vs andThen
        // compose: apply THIS function AFTER the argument function
        // andThen: apply THIS function BEFORE the argument function
        Function<Integer, Integer> times2 = x -> x * 2;
        Function<Integer, Integer> plus3 = x -> x + 3;

        System.out.println("compose: " + times2.compose(plus3).apply(5)); // (5+3)*2 = 16
        System.out.println("andThen: " + times2.andThen(plus3).apply(5)); // (5*2)+3 = 13

        // Using functions with streams
        List<String> names = Arrays.asList("alice", "bob", "charlie");
        List<String> processed = names.stream()
            .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
            .collect(Collectors.toList());
        System.out.println("Processed: " + processed); // [Alice, Bob, Charlie]

        // Practical: parsing
        Function<String, Integer> safeParse = s -> {
            try {
                return Integer.parseInt(s.trim());
            } catch (NumberFormatException e) {
                return 0;
            }
        };
        System.out.println("Parsed: " + safeParse.apply("42")); // 42
        System.out.println("Bad parse: " + safeParse.apply("abc")); // 0

        // identity function
        Function<String, String> identity = Function.identity();
        System.out.println("Identity: " + identity.apply("hello")); // hello
    }
}

Function methods:

  • apply(T t) — apply the function
  • andThen(Function) — apply after this function
  • compose(Function) — apply before this function
  • identity() — returns the input unchanged

Consumer

Consumer

Consumer<T> takes a value of type T and returns nothing. It is used for side effects (printing, logging, modifying state).

Method: void accept(T t)

import java.util.*;
import java.util.function.Consumer;
import java.util.stream.*;

public class ConsumerDemo {
    public static void main(String[] args) {
        // Basic consumer
        Consumer<String> print = System.out::println;
        Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());

        print.accept("Hello"); // Hello
        printUpper.accept("Hello"); // HELLO

        // Consumer chaining
        Consumer<String> log = s -> System.out.println("LOG: " + s);
        Consumer<String> store = s -> System.out.println("STORE: " + s);
        Consumer<String> logAndStore = log.andThen(store);
        logAndStore.accept("data");
        // LOG: data
        // STORE: data

        // Using consumers with forEach
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names.forEach(name -> System.out.println("Hello, " + name + "!"));

        // Practical: building a report
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        Consumer<List<Integer>> printStats = list -> {
            int sum = list.stream().mapToInt(Integer::intValue).sum();
            double avg = (double) sum / list.size();
            System.out.println("Count: " + list.size());
            System.out.println("Sum: " + sum);
            System.out.println("Average: " + avg);
            System.out.println("Min: " + list.stream().min(Integer::compareTo).orElse(0));
            System.out.println("Max: " + list.stream().max(Integer::compareTo).orElse(0));
        };
        printStats.accept(numbers);

        // Practical: modifying objects
        class Person {
            String name;
            int age;
            Person(String name, int age) { this.name = name; this.age = age; }
            @Override
            public String toString() { return name + "(" + age + ")"; }
        }

        Consumer<Person> birthday = p -> p.age++;
        Consumer<Person> printPerson = p -> System.out.println(p);

        Person alice = new Person("Alice", 30);
        birthday.andThen(printPerson).accept(alice); // Alice(31)
    }
}

Consumer methods:

  • accept(T t) — perform the action
  • andThen(Consumer) — chain another consumer after this one

Common patterns:

  • list.forEach(System.out::println) — print each element
  • list.forEach(consumer.andThen(other)) — chain side effects
  • optional.ifPresent(consumer) — action if value present

Supplier

Supplier

Supplier<T> takes no arguments and returns a value of type T. It is used for producing values, lazy initialization, and factory patterns.

Method: T get()

import java.util.*;
import java.util.function.Supplier;

public class SupplierDemo {
    public static void main(String[] args) {
        // Basic supplier
        Supplier<String> greeting = () -> "Hello, World!";
        Supplier<Double> random = Math::random;
        Supplier<List<String>> listFactory = ArrayList::new;

        System.out.println(greeting.get()); // Hello, World!
        System.out.println("Random: " + random.get());
        List<String> newList = listFactory.get();

        // Supplier for lazy evaluation
        Supplier<String> expensiveComputation = () -> {
            System.out.println("Computing...");
            return "result";
        };

        // Only computed when get() is called
        System.out.println("Before get()");
        String result = expensiveComputation.get(); // computes now
        System.out.println("After get(): " + result);

        // Practical: lazy initialization
        class Database {
            private Supplier<Connection> connectionFactory;
            private Connection connection;

            Database(Supplier<Connection> factory) {
                this.connectionFactory = factory;
            }

            Connection getConnection() {
                if (connection == null) {
                    connection = connectionFactory.get(); // lazy init
                }
                return connection;
            }
        }

        // Practical: random data generation
        Supplier<String> randomName = () -> {
            String[] names = {"Alice", "Bob", "Charlie", "David"};
            return names[(int) (Math.random() * names.length)];
        };
        System.out.println("Random name: " + randomName.get());

        // Practical: factory pattern
        Supplier<Map<String, Integer>> hashMapFactory = HashMap::new;
        Supplier<Map<String, Integer>> treeMapFactory = TreeMap::new;

        Map<String, Integer> hashMap = hashMapFactory.get();
        Map<String, Integer> treeMap = treeMapFactory.get();
        System.out.println("HashMap class: " + hashMap.getClass());
        System.out.println("TreeMap class: " + treeMap.getClass());
    }

    // Connection placeholder
    static class Connection {
        @Override
        public String toString() { return "Connection"; }
    }
}

Supplier methods:

  • get() — produce a value

Common patterns:

  • Lazy evaluation: compute only when needed
  • Factory pattern: create new instances
  • Default values: Optional.orElseGet(supplier)

Custom Functional Interfaces

Custom Functional Interfaces

You can create your own functional interfaces using the @FunctionalInterface annotation.

@FunctionalInterface
public interface Transformer<T> {
    T transform(T input);
}

@FunctionalInterface
public interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

@FunctionalInterface
public interface Validator<T> {
    boolean validate(T input);
    default Validator<T> and(Validator<T> other) {
        return input -> this.validate(input) && other.validate(input);
    }
    default Validator<T> or(Validator<T> other) {
        return input -> this.validate(input) || other.validate(input);
    }
}

// Usage
import java.util.*;

public class CustomFunctionalInterfaceDemo {
    public static void main(String[] args) {
        // Transformer
        Transformer<String> shout = s -> s.toUpperCase() + "!";
        Transformer<Integer> doubleIt = n -> n * 2;

        System.out.println(shout.transform("hello")); // HELLO!
        System.out.println(doubleIt.transform(5)); // 10

        // TriFunction
        TriFunction<Integer, Integer, Integer, Integer> maxOfThree =
            (a, b, c) -> Math.max(a, Math.max(b, c));
        System.out.println(maxOfThree.apply(1, 2, 3)); // 3

        // Validator with chaining
        Validator<String> notEmpty = s -> s != null && !s.isEmpty();
        Validator<String> hasAtLeast8Chars = s -> s != null && s.length() >= 8;
        Validator<String> hasUpperCase = s -> s != null && s.matches(".*[A-Z].*");

        Validator<String> passwordValidator = notEmpty
            .and(hasAtLeast8Chars)
            .and(hasUpperCase);

        System.out.println("Valid: " + passwordValidator.validate("Pass1234")); // true
        System.out.println("Invalid: " + passwordValidator.validate("pass")); // false

        // Using custom interface with method reference
        List<String> words = Arrays.asList("hello", "world", "java");
        words.forEach(System.out::println);

        // Practical: callback interface
        @FunctionalInterface
        interface Callback<T> {
            void onComplete(T result);
            default void onError(Throwable t) {
                System.err.println("Error: " + t.getMessage());
            }
        }

        Callback<String> callback = new Callback<String>() {
            @Override
            public void onComplete(String result) {
                System.out.println("Result: " + result);
            }
        };
        callback.onComplete("done");
        callback.onError(new RuntimeException("oops"));
    }
}

Key points:

  • @FunctionalInterface ensures the interface has exactly one abstract method
  • It can have default and static methods
  • Custom interfaces work with lambdas and method references
  • Design for the specific use case (not generic like Predicate/Function)

Practice Problems

0 / 2 solved
Sort an Array
Sorting

Given an array of integers nums, sort the array in ascending order.

Example:

Input: nums = [5, 2, 3, 1]

Output: [1, 2, 3, 5]

Sort in ascending order

Kth Largest Element
Sorting

Find the kth largest element in an unsorted array.

Example:

Input: nums = [3, 2, 1, 5, 6, 4], k = 2

Output: 5

The 2nd largest element is 5

Quiz

1. What is the method signature of Predicate<T>?

Question 1 options

2. What is the difference between Function.compose() and Function.andThen()?

Question 2 options

3. Which functional interface is best for producing a value without arguments?

Question 3 options

4. What does the @FunctionalInterface annotation do?

Question 4 options

Flashcards

Question

What are the 4 core functional interfaces in Java?

Answer

Predicate<T> (boolean test(T)), Function<T,R> (R apply(T)), Consumer<T> (void accept(T)), Supplier<T> (T get()). Each serves a different purpose: conditions, transformations, side effects, and production.

Question

When would you use Consumer vs Function?

Answer

Consumer for side effects that don't return a value (printing, logging, modifying state). Function for transformations that produce a new value (parsing, converting, mapping).

Question

How do you chain Predicates?

Answer

Use .and() for AND, .or() for OR, .negate() for NOT. Example: predicate1.and(predicate2).or(predicate3).negate()

Revision Notes

Key Takeaways

  • 1. Predicate tests conditions (boolean return)
  • 2. Function transforms values (produces new value)
  • 3. Consumer performs side effects (no return)
  • 4. Supplier produces values (no arguments)
  • 5. All support chaining with default methods

Interview Tips

  • Explain each core functional interface and when to use each
  • Demonstrate chaining: Predicate.and/or, Function.andThen/compose
  • Know the difference between compose (applies before) and andThen (applies after)
  • Be ready to create custom functional interfaces for specific use cases

Cheat Sheet

Functional Interfaces Cheat Sheet

Core Interfaces

  • Predicate: boolean test(T t)
  • Function<T,R>: R apply(T t)
  • Consumer: void accept(T t)
  • Supplier: T get()

Chaining

  • Predicate: and(), or(), negate()
  • Function: andThen(), compose()
  • Consumer: andThen()

Common Methods

  • Predicate.isEqual(obj)
  • Function.identity()
  • Consumer.andThen(other)

Custom

  • @FunctionalInterface
  • One abstract method
  • Can have default/static methods