Skip to main content

Command Palette

Search for a command to run...

Mastering Java Functional Programming

Lambdas, Functional Interfaces & java.util.function

Updated
6 min readView as Markdown
Mastering Java Functional Programming

Introduction

Java's functional programming features were primarily introduced in Java 8 centered on functional interfaces and lambda expressions - let you express behavior as concise, inline functions instead of creating separate implementing classes. This article explains what functional interfaces are, how lambda expressions and method references target those interfaces, and highlights commonly used types from the java.util.function package to make your code more expressive and reusable.

Functional Interface

Interface with only one method signature is called as functional interface. "@FunctionalInterface" annotation can also be used to declare an interface as functional. With annotation compiler throws exceptions if more than 1 methods are declared in such interface.

@FunctionalInterface
public interface Operation {
    public int operate(int a, int b);
}

In general we create a class to implement this interface as below.

public class Operational implements Operation {
    @Override
    public int operate(int a, int b) {
        return a + b;
    }
}

Operation class implements the operation functional interface and operate method is fixed to addition. Other operations require creating new classes.

public class Main{
    public static void main(String[] args) {
        Operational op = new Operational();
        System.out.println(op.operate(5, 3)); // 8
    }
}

But wait...This looks normal like any object creation of a class implementing interfaces.

No. Functional interfaces are different and can be implemented without any explicit class definition as below.

We can use "Operation" interface as type declaration and define the single method with arrow notation. That is called lambda expression.

Operation add = (a, b) -> a + b;
System.out.println(add.operate(5, 3)); // 8

Operation multiply = (a, b) -> a * b;
System.out.println(multiply.operate(5, 3)); // 15

Same interface can be used to define multiple operations and execute the operate method for any specific operation.

Package java.util.function

Functional interfaces provide target types for lambda expressions and method references. Each functional interface has a single abstract method, called the functional method for that functional interface, to which the lambda expression's parameter and return types are matched or adapted. Functional interfaces can provide a target type in multiple contexts, such as assignment context, method invocation, or cast context.

The interfaces in this package are general purpose functional interfaces used by the JDK, and are available to be used by user code as well. The interfaces in this package are annotated with FunctionalInterface.

Lets see some of the important interfaces in this package.

Predicate:

Predicate - Functional interface that takes an input and returns a Boolean. It holds a condition that can be tested against input values. 'test' is the method name that is used to invoke predicate condition against any argument.

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(23)); //false

Predicate<String> startsWithA = s -> s.toLowerCase().startsWith("a");
Predicate<String> endsWithE = s -> s.toLowerCase().endsWith("e");
System.out.println(startsWithA.test("Apple")); //true
System.out.println(endsWithE.test("Apple")); //true

Function:

Function<T,R> - Functional interface that takes an input and produces an output. 'apply' is the method name that is used to invoke Function

Function<String, Integer> stringLength = s -> s.length();
System.out.println(stringLength.apply("Hello")); // 5

Function<List<Integer>, Integer> sumation =
                list -> {
                    int sum = 0;
                    for (int num : list) {
                        sum += num;
                    }
                    return sum;
                };
System.out.println(sumation.apply(Arrays.asList(1, 2, 3, 4, 5))); //15

Function has a static method called identity that can return the same element passed as input.

Function<Integer,Integer> identity = Function.identity();
System.out.println(identity.apply(69)); //69

Consumer:

Consumer - Functional interface that takes an input and performs an action without returning a result. 'accept' is the method name used to involve consumer to consume parameters.

Consumer<String> print = s -> System.out.println(s);
print.accept("This is a consumer example."); //This is a consumer example.

Consumer<List<Integer>> printList = list -> {
            for (int num : list) {
                System.out.print(num + " ");
            }
            System.out.println();
        };
printList.accept(Arrays.asList(1, 2, 3, 4, 5)); // 1 2 3 4 5

Supplier:

Supplier - Functional interface that takes no input and produces a result. T is the type of results supplied by the supplier. T is the type of output produced by the supplier

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); //0.8347876

Similarly we have BiFunction, BiConsumer, BiPredicate for two inputs and more variations for primitive types (IntFunction, LongConsumer, etc.)

Unary Operator:

Unary Operator - Functional interface that takes a single input and produces a result of the same type

UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(5)); // 25

Binary Operator:

Binary Operator - Functional interface that takes two inputs and produces a result of the same type

BinaryOperator<Integer> addBinary = (a, b) -> a + b; System.out.println(addBinary.apply(5, 3)); // 8

Method Reference and Constructor Reference

Method reference - A shorthand notation of a lambda expression to call a method

Function<String, Integer> stringLengthMethodRef = String::length;
System.out.println(stringLengthMethodRef.apply("Hello")); // 5

Constructor reference - A shorthand notation of a lambda expression to call a constructor


public class Test {

    public int value;

    public Test(int value) {
        this.value = value;
    }

    public Test() {
    }

    public int getValue() {
        return value;
    }
}

Supplier<Test> testSupplier = Test::new;
Test t2 = testSupplier.get();
System.out.println("Test value from constructor ref: " + t2.getValue());

In constructor ref can we pass parameters? No, but we can use lambda for that.

Conclusion

Functional interfaces—interfaces with a single abstract method (optionally annotated with @FunctionalInterface)—is the pillar for Java’s lambda support. Lambdas and method references let you implement those interfaces inline, removing boilerplate class definitions and enabling multiple behaviors to be expressed successfully. The java.util.function package provides standard target types (e.g., Function, Consumer, Supplier, Predicate, UnaryOperator, BinaryOperator) that make lambdas widely reusable across assignment, method invocation, and cast contexts, and they pair naturally with the Streams API for functional-style programming. Use lambdas to improve clarity and modularity, but keep readability and side-effect management in mind when refactoring anonymous classes into lambda expressions.

Official Oracle Documentation:

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

🙏Thanks for reading my blog✍️.

Happy coding journey🙂