Java Functional Interfaces: Predicate, BiPredicate, Consumer, BiConsumer, Function, BiFunction, and Supplier

In Java, these are functional interfaces provided in the java.util.function package, primarily used for lambda expressions and method references. Each of these interfaces serves a specific purpose in functional programming. Let’s go through each pair and the single interface you mentioned, with their differences and use cases:

1. Predicate vs. BiPredicate

  • Predicate: Represents a function that takes one argument and returns a boolean value. It is often used for filtering or matching.
  • Use cases:
    • Filtering collections using Stream.filter(): Check if elements in a stream meet a certain criteria.
    • Validating data: Ensure input adheres to specific rules.
    • Custom logic in conditional statements: Define complex conditions for if statements.
  • Use case example:
    • Filtering a list of strings to include only those that start with a certain letter.
List<String> strings = Arrays.asList("apple", "banana", "cherry");
Predicate<String> startsWithA = s -> s.startsWith("a");
strings.stream().filter(startsWithA).forEach(System.out::println); // Outputs "apple"

// Example 2:
Predicate<String> isLongString = name -> name.length() > 10;
List<String> longNames = names.stream().filter(isLongString).collect(Collectors.toList());
  • BiPredicate: Similar to Predicate but takes two arguments. It’s useful for conditions that need two inputs. Similar to Predicate, but takes two arguments of types T and U and returns a boolean value. It represents a condition that involves two elements.
  • Use cases:
    • Comparing elements in a stream: Check if two elements in a stream satisfy a certain relationship.
    • Combining conditions: Combine multiple conditions into a single check.
  • Use case example: Filtering a map entries based on a condition involving key and value.
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
BiPredicate<String, Integer> keyAndValueCheck = (key, value) -> key.startsWith("a") && value > 0;
map.entrySet().stream().filter(entry -> keyAndValueCheck.test(entry.getKey(), entry.getValue())).forEach(System.out::println); // Outputs "a=1"

// example 2:
BiPredicate<Integer, Integer> isEvenSum = (x, y) -> (x + y) % 2 == 0;
List<Integer[]> evenSumPairs = pairs.stream().filter(isEvenSum).collect(Collectors.toList());

2. Consumer vs. BiConsumer

  • Consumer: Represents an operation that takes a single input and returns no result. It is typically used for operations that affect an object or perform an action.
  • A functional interface that takes one argument of any type T and performs an action (doesn’t return a value). It represents an operation on a single element.
  • Use cases:
    • Processing elements in a stream using Stream.forEach(): Iterate through elements and perform side effects (printing, logging, etc.).
    • Updating elements in a collection: Modify elements based on some logic.
  • Use case example: Printing each element of a list.
Consumer<String> printName = name -> System.out.println(name);
names.forEach(printName);  

Consumer<String> printer = System.out::println;
strings.forEach(printer); // Prints "apple", "banana", "cherry"
  • BiConsumer: Like Consumer but takes two arguments. It’s used for operations that require two inputs.
  •  Similar to Consumer, but takes two arguments of types T and U and performs an action. It represents an operation on two elements.
  • Use cases:
    • Combining elements in a stream: Merge or combine two elements into a single result.
    • Processing data pairs: Perform an operation on two pieces of data.
  • Use case example: Applying an operation on the key and value of a map entry.
BiConsumer<String, String> concatenateNames = (firstName, lastName) -> System.out.println(firstName + " " + lastName);
pairs.forEach(concatenateNames);


BiConsumer<String, Integer> mapPrinter = (key, value) -> System.out.println(key + ":" + value);
map.forEach(mapPrinter); // Prints "a:1", "b:2", "c:3"

3. Function vs. BiFunction

  • Function: Represents a function that takes one argument and produces a result. It is used for transformations or computations.
  • A functional interface that takes one argument of type T and returns a value of type R. It represents a transformation or mapping operation on a single element.
  • Use cases:
    • Transforming elements in a stream using Stream.map(): Convert elements to a different type or apply a function to them.
    • Extracting data: Extract specific information from elements.
  • Use case example: Converting a list of strings to their lengths.
  Function<String, Integer> stringLength = String::length;
  List<Integer> lengths = strings.stream().map(stringLength).collect(Collectors.toList()); // [5, 6, 6]

Function<String, Integer> stringLength = name -> name.length();
List<Integer> lengths = names.stream().map(stringLength).collect(Collectors.toList());
  • BiFunction: Similar to Function but takes two arguments. It’s used for operations or computations involving two inputs.
  •  Similar to Function, but takes two arguments of types T and U and returns a value of type R. It represents a transformation or mapping operation on two elements.
  • Use cases:
    • Combining elements: Create a new result from two elements.
    • Performing calculations: Apply a function to two values and get an output.
  • Use case example: Adding two integers.
  BiFunction<Integer, Integer, Integer> adder = Integer::sum;
  int result = adder.apply(1, 2); // 3

BiFunction<Integer, Integer, Double> calculateAverage = (x, y) -> (double) (x + y) / 2;
List<Double> averages = pairs.stream().map(calculateAverage).collect(Collectors.toList());

4. Supplier

  • Supplier: Represents an operation that takes no argument and returns a result. It’s often used for lazy generation of values.
  • A functional interface that doesn’t take any arguments and returns a value of type T. It represents a way to generate or supply a value without any input.
  • Use cases:
    • Lazy initialization: Create an object only when it’s needed.
    • Generating random values: Generate random data to use in your program.
  • Use case example: Generating a new date instance when needed.
  Supplier<LocalDate> dateSupplier = LocalDate::now;
  LocalDate today = dateSupplier.get(); // Gets the current date

Supplier<String> randomName = () -> UUID.randomUUID().toString();
String generated = randomName.get();  

Each of these functional interfaces plays a critical role in making Java’s lambda expressions and method references more powerful and flexible, enabling cleaner and more concise code, especially in the context of stream operations and functional programming.

Leave a Reply

Your email address will not be published. Required fields are marked *