Last Updated on July 17, 2024

In Java, Functional Interfaces are a core concept introduced in Java 8 that enable a more functional programming style.

A functional interface is an interface that has exactly one abstract method. It can also have any number of default and static methods.

Functional interfaces serve as a target type for lambda expressions and method references, promoting a concise and readable way to define functionality.

They allow developers to write code that focuses on “what” needs to be done rather than “how” it’s done, often leading to cleaner and more maintainable code.

A functional interface is also called Single Abstract Method (SAM) interface as it only contains one abstract method and can contain any number of default or static methods.

A Java functional interface can be implemented by a Java Lambda Expression.

Lambda expressions, introduced in Java 8, offer a concise and anonymous way to define functionality.

Example:

@FunctionalInterface 
public interface MathOperation { int operation(int a, int b); } 

// Lambda expression assigned to a variable 
MathOperation add = (a, b) -> a + b; 

// Lambda expression passed as an argument 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); 
numbers.forEach(number -> System.out.println(number * 2));

Java mandates that lambda expressions can only be assigned to variables or passed as arguments if the target type is a functional interface. A functional interface, by definition, has a single abstract method.

These functional interfaces are defined in the java.util.function package.

Java Built-in Functional Interfaces

Java provides a collection of pre-defined functional interfaces for common tasks, saving you the time and effort of creating your own for every situation. 

We’ll explore some of these handy interfaces in the following sections

Function

The java.util.function.Function interface is a fundamental building block in Java’s functional programming features.

It represents a function that takes one argument of type T and returns a value of type R.

The abstract method in Function is named apply(T t), where T is the type of the input argument and the return type is R.

public interface Function<T,R> {

    public <R> apply(T parameter);
    
    //Non-abstract methods
    ………
}

The Function interface offers a few additional methods beyond apply(T t). These methods come with pre-written implementations, so you don’t need to create your own versions unless you have a specific reason to modify them.

// Lambda expression example 
Function<String, Integer> stringLength = s -> s.length(); 

int stringLengthResult = stringLength.apply("Hello"); // stringLengthResult will be 5

It is commonly used for mapping or transforming input to output.

Predicate

The Predicate interface in Java, introduced in Java 8, is another fundamental building block for functional programming.

It represents a function that takes one argument of any type and returns a boolean value (true or false).

The abstract method in Predicate is named test(T t), where T is the type of the input argument and the return type is boolean.

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

  //Non-abstract methods
  ………
}

The Predicate interface is frequently used in conjunction with lambda expressions and method references to filter elements within collections or streams.

It allows you to define conditions or criteria for selecting elements based on specific logic.

// Lambda expression example
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
    .filter(number -> number % 2 == 0) // Uses Predicate with lambda expression
    .forEach(System.out::println);  // Prints even numbers

The Predicate interface is a valuable tool for filtering elements based on boolean conditions.

Supplier

It represents a supplier of a value.

get(): This abstract method has no arguments and returns a value of type T. The specific type T depends on the context in which you use the Supplier interface.

@FunctionalInterface
public interface Supplier<T> {
  T get();
  //Non-abstract methods
  ………
}

It can be used to generate random values, read data from a source like a file or network, or provide default values.

// Generating a random number
Supplier<Double> randomDoubleSupplier = () -> Math.random();
double randomNumber = randomDoubleSupplier.get(); 

Consumer

It represents an operation that consumes a single argument but doesn’t return any value.

accept(T t): This abstract method takes one argument of type T and performs some operation on it. The specific type T depends on the context in which you use the Consumer interface.

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

  //Non-abstract methods
  ………
}

You can use Consumer to perform actions on individual elements within collections or streams. This includes tasks like printing, logging, or modifying elements.

// Printing elements
List<String> names = Arrays.asList("John", "Mary", "Peter");
names.forEach(name -> System.out.println(name.toUpperCase()));  // Uses Consumer with lambda expression

Functional programming generally avoids side effects, Consumer is designed for scenarios where you need to perform actions with potential side effects, like modifying data or interacting with external resources.

UnaryOperator

The UnaryOperator interface in Java, is a specialized functional interface designed to work with a single argument and return a value of the same type.

T apply(T t): This abstract method takes one argument of type T and returns a value of type T as well. The specific type T depends on the context in which you use the UnaryOperator interface.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
  T apply(T t);
   
  //Non-abstract methods
  ………
}

You can use UnaryOperator to perform operations like incrementing, converting to uppercase, or applying any logic that modifies a value and returns a new value of the same type.

// Incrementing numbers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
    .map(UnaryOperator.identity()) // Keeps elements unchanged (useful for chaining operations)
    .map(number -> number + 1)  // Uses UnaryOperator with lambda expression
    .forEach(System.out::println);  // Prints incremented numbers

BinaryOperator

The BinaryOperator interface in Java, is a functional interface designed for performing operations on two operands and returning a result of the same type.

T apply(T t1, T t2): This abstract method takes two arguments of type T and returns a value of type T. The specific type T depends on the context in which you use the BinaryOperator interface.

The BinaryOperator interface extends the BiFunction interface. However, it enforces the constraint that all three types (input arguments and return type) must be the same.

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
  T apply(T t1, T t2);
}

BinaryOperator is a preferred choice for stream reduction operations where you want to combine elements while maintaining the same type.

// Adding numbers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, Integer::sum); // Uses BinaryOperator with method reference (reduction)

Conclusion

Functional interfaces have revolutionized the way developers approach tasks in Java. By leveraging these interfaces alongside lambda expressions, code becomes more concise, readable, and expressive.

Understanding and using functional interfaces effectively unlocks a whole new level of development in Java. They provide a powerful toolkit for writing clean, concise, and functional code, making your applications more maintainable and expressive.

Scroll to Top