Blog Java Java 8 Features

Supercharge Your Java 8 Skills: Unleash the Power of Functional Interfaces – Predicate, Consumer, Supplier, and More

Functional Interface

Photo by Tracy Adams on Unsplash

Introduction

Java 8 introduced lambda expressions and functional interfaces. It introduced a powerful set of functional interfaces in the java.util.function package, enhancing the language’s capabilities for functional programming.

Among these, Predicate, Consumer, Supplier, and Function stand out for their versatility and widespread application. This blog post dives deep into each of these interfaces, providing insights and examples to demonstrate their utility.

What are Functional Interfaces in Java 8?

In Java 8, a Functional Interface is an interface that has only one abstract method. It can have any number of default and static methods, but it must have only one abstract method.

The purpose of introducing Functional Interfaces in Java 8 is to support Lambda expressions and method references.

Here are some key points about Functional Interfaces in Java 8:

Lambda Expressions: Functional Interfaces are designed to be used with Lambda expressions, which allow you to represent an instance of the interface in a concise way. Lambda expressions provide an easy way to implement the single abstract method defined by the Functional Interface.

@FunctionalInterface Annotation: Java provides the @FunctionalInterface annotation. The primary reason to use a functional interface is to enforce having a single abstract method.

If someone by mistake adds a second abstract method compiler will raise the interface violates the rules of a Functional Interface (e.g., having more than one abstract method), and the compiler will generate an error.

Java

@FunctionalInterface
interface InvalidInterface {

int operation(int a, int b);

// Attempting to add another abstract method:

    String formatResult(int result);
}

//Compiler Error
/*
java: Unexpected @FunctionalInterface annotation
lambda_expression.consumer.InvalidInterface is not a functional interface
multiple non-overriding abstract methods found in interface lambda_expression.consumer.InvalidInterface
*/


Built-in Functional Interfaces: Java 8 introduced several built-in Functional Interfaces in the java.util.function package, such as ConsumerPredicateFunctionSupplier, and more. These interfaces are commonly used with Lambda expressions and stream operations.

Custom Functional Interfaces: You can also define your own custom Functional Interfaces to suit your specific requirements.

Method References: In addition to Lambda expressions, Functional Interfaces also support method references, which provide a more concise way to refer to an existing method implementation.

Here’s an example of a custom Functional Interface and how it can be used with a Lambda expression:

Java
@FunctionalInterface
interface Operation {
    int perform(int a, int b);
}

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        Operation addition = (a, b) -> a + b;
        int result = addition.perform(5, 3); // result = 8
        System.out.println("Result: " + result);
    }
}

In this example, Operation is a custom Functional Interface with a single abstract method perform. We create a Lambda expression (a, b) -> a + b and assign it to an instance of the Operation interface. This Lambda expression implements the perform method and can be used to perform addition.

Functional Interfaces and Lambda expressions are essential features introduced in Java 8 that enable functional programming style and promote more concise and expressive code.

Benefits of Using Functional Interfaces and Lambda Expressions:

  • Conciseness: Lambda expressions significantly reduce code size compared to traditional anonymous inner classes.
  • Readability: Their clear and compact syntax improves code comprehension, especially for short, well-defined functions.
  • Improved Functional Programming: They enable a more functional programming style, promoting code clarity and maintainability.
  • Stream API Integration: Seamlessly integrates with the Java Stream API, allowing for powerful data manipulation and processing. 

Predicate

A Predicate<T> represents a single argument function that returns a boolean value. It’s primarily used for filtering or matching.

Java
@FunctionalInterface
public interface Predicate<T> {

    /**
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * AND of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code false}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ANDed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * AND of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    /**
     * Returns a predicate that represents the logical negation of this
     * predicate.
     *
     * @return a predicate that represents the logical negation of this
     * predicate
     */
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * OR of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code true}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ORed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * OR of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    /**
     * Returns a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}.
     *
     * @param <T> the type of arguments to the predicate
     * @param targetRef the object reference with which to compare for equality,
     *               which may be {@code null}
     * @return a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}
     */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    /**
     * Returns a predicate that is the negation of the supplied predicate.
     * This is accomplished by returning result of the calling
     * {@code target.negate()}.
     *
     * @param <T>     the type of arguments to the specified predicate
     * @param target  predicate to negate
     *
     * @return a predicate that negates the results of the supplied
     *         predicate
     *
     * @throws NullPointerException if target is null
     *
     * @since 11
     */
    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}

Examples: Filtering a List

Java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
Predicate<String> startsWithA = name -> name.startsWith("A");
List<String> namesStartingWithA = names.stream()
                                        .filter(startsWithA)
                                        .collect(Collectors.toList());
System.out.println(namesStartingWithA); // [Alice]

If you check the above code, Predicate is passsing in the filter method in Java 8’s Stream. API has the following signature:

Java
Stream<T> filter(Predicate<? super T> predicate)

Combining Predicates

Java
Predicate<String> longerThanThree = name -> name.length() > 3;
Predicate<String> startsWithC = name -> name.startsWith("C");
List<String> filteredNames = names.stream()
                                   .filter(longerThanThree.and(startsWithC))
                                   .collect(Collectors.toList());
System.out.println(filteredNames); // [Charlie]

Consumer

The Consumer<T> functional interface in Java 8 can only take a single input argument, and it doesn’t return any result.

Java
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

The accept method takes a single argument of type T and doesn’t return any value (it has a void return type).

Examples Printing List Elements

Java
Consumer<String> printConsumer = System.out::println;
names.forEach(printConsumer); // Prints each name in the list

Consumer<String>: This declares a functional interface Consumer that takes a String as input and performs an operation (doesn’t return anything).

System.out::println: This is a method reference. It’s a shorthand way of creating a lambda expression that’s equivalent to: s -> System.out.println(s).

names.forEach(printConsumer): The forEach method on the list iterates over each name in the names list and applies the printConsumer (which uses the System. out:println method reference) to print each name.

Modifying List Elements

Java
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
Consumer<List<Integer>> multiplyByTwo = list -> {
    for (int i = 0; i < list.size(); i++) {
        list.set(i, list.get(i) * 2);
    }
};
multiplyByTwo.accept(numbers);
System.out.println(numbers); // [2, 4, 6]
  • Consumer<List<Integer>>: A Consumer that takes a List<Integer> as input.
  • list -> { ... }: A lambda expression that defines the action: iterate over the list and multiply each element by 2.
  • multiplyByTwo.accept(numbers): Executes the lambda expression, modifying the original numbers list.

Supplier: What is the Supplier Interface?

The Supplier<T> interface represents a supplier of results. It has a single method, T get(), which returns an instance of T. A Supplier<T> takes no arguments but returns a result. It’s often used for lazy generation of values.

Here’s the basic structure of the Supplier<T> interface:

Java
package java.util.function;

/**
 *
 * @param <T> the type of outcomes return by supplier
 */
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

How to Use the Supplier Interface?

The Supplier interface can be used whenever you need a functional interface that supplies a value of some sorts. Here’s an example:

Examples:

1. Generating Random Numbers

Java
Supplier<Double> randomSupplier = Math::random;
System.out.println(randomSupplier.get()); // Random double between 0.0 and 1.0

2. Instantiating Objects

Java
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> newList = listSupplier.get();
newList.add("Generated");
System.out.println(newList); // [Generated]

3. Return String

Java
Supplier<String> stringSupplier = () -> "Hello, World!";
System.out.println(stringSupplier.get());  // prints: Hello, World!

Function

A Function<T,R> takes one argument of type T and returns a result of the type R. It’s useful for transforming data.

Examples

Converting Strings to Lengths

Java
Function<String, Integer> stringLengthFunction = String::length;
List<Integer> lengths = names.stream()
                             .map(stringLengthFunction)
                             .collect(Collectors.toList());
System.out.println(lengths); // [5, 3, 7, 5]

Concatenating Strings

Java
Function<String, String> addExclamation = s -> s + "!";
List<String> excitedNames = names.stream()
                                  .map(addExclamation)
                                  .collect(Collectors.toList());
System.out.println(excitedNames); // [Alice!, Bob!, Charlie!, Diana!]

Here are examples of using BiPredicate, BiConsumer, and BiFunction in Java 8:

1. BiPredicate

A BiPredicate represents a predicate (boolean-valued function) that takes two arguments. It can be used for filtering or testing elements based on two inputs.

Java
import java.util.function.BiPredicate;

public class BiPredicateExample {
    public static void main(String[] args) {
        BiPredicate<Integer, Integer> isGreaterThan = (a, b) -> a > b;

        boolean result1 = isGreaterThan.test(5, 3); // true
        boolean result2 = isGreaterThan.test(2, 4); // false

        System.out.println(result1); // true
        System.out.println(result2); // false
    }
}

In this example, isGreaterThan is a BiPredicate that takes two Integer values and returns true if the first value is greater than the second value.

2. BiConsumer

A BiConsumer represents an operation that takes two arguments and performs some action or side-effect on those arguments.

Java
import java.util.function.BiConsumer;

public class BiConsumerExample {
    public static void main(String[] args) {
        BiConsumer<String, Integer> printStringAndNumber = (str, num) -> {
            System.out.println("String: " + str);
            System.out.println("Number: " + num);
        };

        printStringAndNumber.accept("Hello", 42);
    }
}

This example defines a BiConsumer called printStringAndNumber that takes a String and an Integer as arguments and prints them to the console.

3. BiFunction

A BiFunction represents a function that takes two arguments and produces a result.

Java
import java.util.function.BiFunction;

public class BiFunctionExample {
    public static void main(String[] args) {
        BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;

        int result = sum.apply(5, 3); // 8

        System.out.println(result);
    }
}

In this example, sum is a BiFunction that takes two Integer values and returns their sum. The apply method is used to invoke the function with the provided arguments.

These examples demonstrate how BiPredicate, BiConsumer, and BiFunction can be used to work with operations that involve two arguments. They provide a more concise and functional way of representing and combining operations in Java 8.

Conclusion

Java 8’s functional interfaces bring a functional programming flair to the language, simplifying code and making it more expressive. Whether you’re filtering collections with Predicate, performing operations with Consumer, generating values with Supplier, or transforming data with Function, these interfaces offer a robust toolkit for tackling a wide array of programming challenges. Embrace these examples as a starting point, and explore the vast possibilities they unlock in your Java applications.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over