Skip to content
intermediatePhase 15 · Java 8+ Features

Functional Interfaces

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

45m
2 problems
Topic Progress0%

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/2solved
Filter and Transform Pipeline

Create a method that takes a list of strings, filters them using a Predicate, and transforms them using a Function. Return the resulting list.

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

public class Pipeline {
    public static <T, R> List<R> pipeline(List<T> input, Predicate<T> filter, Function<T, R> transform) {
        return input.stream()
            .filter(filter)
            .map(transform)
            .collect(Collectors.toList());
    }
}
Composable Validators

Create a Validator functional interface with validate(), and() default methods. Then create validators for checking if a string is not empty, has minimum length, and contains only alphanumeric characters.

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

public class ValidatorDemo {
    public static void main(String[] args) {
        Validator<String> notEmpty = s -> s != null && !s.isEmpty();
        Validator<String> minLength = s -> s != null && s.length() >= 6;
        Validator<String> alphanumeric = s -> s != null && s.matches("[a-zA-Z0-9]+");

        Validator<String> combined = notEmpty.and(minLength).and(alphanumeric);
        System.out.println("Valid: " + combined.validate("hello123")); // true
        System.out.println("Invalid: " + combined.validate("hi")); // false
    }
}

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()

Question

What is Java Functional Interfaces?

Answer

Java Functional Interfaces is a key concept in Java programming.

Question

When to use Java Functional Interfaces?

Answer

Use Java Functional Interfaces when building production systems that require reliability, scalability, and maintainability.

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