Functional Interfaces in Java 8

JUNAID | December 20, 2022

Functional Interfaces in Java 8

This blog serves as a reference for the various functional interfaces included in Java 8, as well as their typical use cases and integration into the JDK library.

Lambdas in Java 8

Lambda expressions, a significant new syntactic enhancement introduced by Java 8, are now available. Lambdas are anonymous functions that we can manage like first-class citizens in our language. For example, we may provide it to a method or return it from one.

In the past, whenever we needed to encapsulate a single piece of functionality, we would typically construct a class. This entailed writing a lot of boilerplate code that was optional to build a simple function representation.

Functional Interfaces

The functional interfaces and recommended practices for working with lambdas are covered in more detail in the article "Lambda Expressions and Functional Interfaces: Tips and Best Practices." The java.util.function package's various functional interfaces are the subject of this manual.

Any interface that contains a SAM (Single Abstract Method) is functional, and the implementation of such an interface can be thought of as lambda expressions.

A functional interface may still have many default methods, and it should be noted that Java 8's default methods


A functional interface with a method that accepts one value and returns another is the most basic and universal example of a lambda. The Function interface, which is parameterized by the types of its arguments

    public interface Function {}

The Map. computer absent method is one of the standard library's uses for the Function type. If a key does not already exist in a map, this method calculates a value and returns it from it. It takes advantage of the supplied Function implementation to calculate a value:

    Map< String, Integer> name = new HashMap<>();

    Integer value = name.computeIfAbsent("John", s -> s.length());

In this instance, a value will be determined by applying a function to a key that has been placed inside a map and has also been returned from a method call. A method reference that matches the types of the supplied and returned values can be used in place of the lambda.

        Integer val = nameMap.computeIfAbsent("John", String::length);

We can combine multiple functions into one and run them successively using the Function interface's default compose method:

    Function< Integer, String> intToString = Object::toString;
    Function< String, String> quote = s -> "'" + s + "'";
    Function< Integer, String> quoteIntToString = quote.compose(intToString);
    assertEquals("'5'", quoteIntToString.apply(5));

Primitive Function Specializations

The most popular primitive types double, int, and long, as well as their combinations in argument and return types, have their own versions of the Function interface because a primitive type cannot be a generic type parameter:

  • IntFunction, LongFunction, and DoubleFunction: Specified type of arguments, parameterized return type
  • ToIntFunction, ToLongFunction, and ToDoubleFunction: The arguments are parameterized and the return type is one of the given types.
  • The titles of the functions DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, and LongToDoubleFunction all specify that the argument and return types must be defined as primitive types.

For instance, a function that accepts a short and returns a byte does not have an existing functional interface; nevertheless, this does not prevent us from creating one ourselves:


    public interface ShortToByteFunction 
            byte applyAsByte(short s);

Using a rule specified by a ShortToByteFunction, we can now create a function that converts an array of shorts into an array of bytes:

    public byte[] transformArray(short[] array, ShortToByteFunction function) {
        int j;
                byte[] arr = new byte[arr.length];
            for ( j = 0; j < arr.length; j++) {
                arr[j] = function.applyAsByte(arr[j]);
            return transformedArray;

Here is how we could apply it to change a collection of shorts into a collection of bytes multiplied by two:

    short[] arr = {(short) 1, (short) 2, (short) 3};
    byte[] trnsArray = transformArray(arr, s -> (byte) (s * 2));
    byte[] expArray = {(byte) 2, (byte) 4, (byte) 6};
    assertArrayEquals(expArray, trnsArray);     

Two-Arity Function Specializations

BiFunction, ToDoubleBiFunction, ToIntBiFunction, and ToLongBiFunction are additional interfaces that must be used in order to define lambdas with two arguments. While ToDoubleBiFunction and other functions let us return a primitive value, BiFunction contains both arguments and a return type that are generated.

In the standard API, the Map.replaceAll method, which allows replacing all of the

Let's construct a BiFunction that takes a key and an old value as inputs, computes a new value for the salary, and then returns it.

    Map< String, Integer> salary = new HashMap<>();
    salary.put("John", 40000);
    salary.put("Freddy", 30000);
    salary.put("Samuel", 50000);
    salary.replaceAll((name, oldValue) -> 
        name.equals("Freddy") ? oldValue : oldValue + 10000);


Another Function specialization that does not accept arguments is the Supplier functional interface. We often employ it for value generation which is lazy. Let's define a function that squares a double value as an example. It won't get anything for itself, but a Supplier will get something for it:

    public double squareLazy(Supplier value)
            return Math.pow(value.get(), 2);

This enables us to use a Supplier implementation to lazily construct the argument for calling this function. If the process of creating the argument requires a lot of time, this may be helpful. Using Guava's sleep Uninterruptible function, we'll mimic that:

    Supplier< Double> val ue= () -> 
            Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
            return 9d;
        Double valueSquared = squareLazy(value);            

Developing the logic for sequence generation is yet another use case for the Supplier. Let's utilize a static Stream to show it. Create a stream of Fibonacci numbers using the following method:

    int[] fib = {0, 1};
    Stream< Integer> fibonacci = Stream.generate(() -> {
        int result = fib[1];
        int fib3 = fib[0] + fib[1];
        fib[0] = fib[1];
        fib[1] = fib3;
        return result;

The Supplier functional interface is implemented by the function that is passed to the Stream. generate method. Keep in mind that the Supplier typically requires some external state in order to be used as a generator. In this instance, its state consists of the final two digits of the Fibonacci sequence.

Since all external variables used inside the lambda must be effectively final, we implement this state using an array rather than a few variables.

BooleanSupplier, DoubleSupplier, LongSupplier, and IntSupplier are further functional interface specializations whose return types are corresponding primitives.


The Consumer consumes a generated argument and gives nothing back, in contrast to the Supplier. It represents side effects as a function.

For instance, let's display a welcome in the console and then greet each person in a list of names. To the List, the lambda passed. Implementing the Consumer functional interface is the forEach method:

    List< String> name = Arrays.asList("John", "Freddy", "Samuel");
        name.forEach(name -> System.out.println("Hello, " + name));            

Additionally, there are specialized Consumers that take arguments in the form of primitive values, like DoubleConsumer, IntConsumer, and LongConsumer. The BiConsumer interface is more intriguing. Its application in iterating through a map's entries is one example:

    Map< String, Integer> age = new HashMap<>();
    age.put("John", 25);
    age.put("Freddy", 24);
    age.put("Samuel", 30);
    age.forEach((name, a) -> System.out.println(name + " is " + age + " years old"));

ObjDoubleConsumer, ObjIntConsumer, and ObjLongConsumer are three additional specialized BiConsumer variants that each take two arguments, one of which is a primitive type and the other is gentrified.


A function that accepts a value and returns a boolean value is referred to as a predicate in mathematical logic.

An extension of a function that takes a generated value and returns a boolean is the Predicate functional interface. The Predicate Lambda is frequently used to filter a set of values:

    List< String> name = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

    List< String> namesWithA =
        .filter(name -> name.startsWith("A"))

In the code above, we use the Stream API to filter a list and only keep the names that begin with the letter "A". The filtering logic is contained within the Predicate implementation.

There are versions of this function that accept primitive values for IntPredicate, DoublePredicate, and LongPredicate, just like in all the preceding examples.


Special cases of a function that accepts and returns the same kind of value are operator interfaces. The interface known as UnaryOperator only accepts one argument. To replace every value in a list with some computed values of the same type is one of its use cases in the Collections API:

    List< String> name = Arrays.asList("bob", "josh", "megan");
        name.replaceAll(e -> name.toUpperCase());    

The Replace List All functions return void because they change the existing values. The lambda that is used to change a list's values must return the same result type that it gets in order to serve its intended purpose. This is why the UnaryOperator is helpful in this situation.

Of course, we can just use a method reference in place of name -> name.toUpperCase():


A reduction operation is one of the more intriguing uses for a BinaryOperator. Consider the case when we want to sum the values of a set of integers. We could accomplish this using a collector in Stream API, but the reduction method would be a more general approach:

    List< Integer> value = Arrays.asList(3, 5, 8, 9, 12);
    int sum =
        .reduce(0, (i1, i2) -> i1 + i2); 

A BinaryOperator function and the initial accumulator value are passed to the reduce method. This function takes two identical-type values as parameters, and it also includes logic to combine those two values into a single identical-type value. The passed function must be associative, which indicates that the order in which values are accumulated is irrelevant. Accordingly, the following requirement must be met:

    op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)    

We may parallelize the reduction process thanks to a BinaryOperator operator function's associative nature.

UnaryOperator and BinaryOperator specializations, such as DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, and LongBinaryOperator, can also be used with primitive values.

Legacy Functional Interfaces

Not every user interface was present in Java 8. We can utilize many interfaces from earlier Java versions as lambdas since they comply with the rules of a FunctionalInterface. The Runnable and Callable interfaces that are utilized in concurrency APIs are two prominent examples. These interfaces are additionally designated as @FunctionalInterface in Java 8. We can substantially simplify concurrency code because of this:

    Thread t = new Thread(() -> System.out.println("Hello From Another Thread"));

This blog looked at various functional interfaces that are available in the Java 8 API that can be used as lambda expressions. I Hope I’ve Shown a clear picture of java 8 functional interfaces, if you need more clarification about the functional interface you can feel free to contact Sanesquare Technologies for more details.

Does your Project Demand Expert Assistance?

Contact us and let our experts guide you and fulfil your aspirations for making the project successful