Learn Java

Lambda Expressions in Java

As you already know, Java is primarily an object-oriented programming language. It supports classes, methods, fields, and other concepts from this paradigm. Methods are the main way to represent the behavior of objects, classes, and whole programs. You can write absolutely any code inside their bodies and then invoke this code from other parts of your program using the names of the methods. This approach allows developers to create very structured and well-readable programs, but sometimes it's not enough and we should use other ways to represent behavior rather than methods.

Functional programming in Java

This topic begins to explain another programming paradigm called functional programming (FP) that uses functions as the primary concept providing an alternative way to solve many programming challenges. Like methods, functions are used to decompose code into small pieces. However, unlike methods, functions can also behave like regular Java objects (e.g., be passed to/returned from a method).

Of course, it is impossible to explain the whole paradigm at once, so there will be a lot of engaging topics. The first concept we will learn is lambda expressions which is the closest one to the standard Java methods. Let's take a look at what they are and why we use them.

Lambda expressions

By a lambda expression (or just "a lambda"), we mean a function that isn't bound to its name (an anonymous function) but can be assigned to a variable.

The most general form of a lambda expression looks like this: (parameters) -> { body };. Here, the part before -> is the list of parameters (like in methods), and the part after that is the body that can return a value. The brackets { } are required only for multi-line lambda expressions.

Sometimes, lambdas don't have parameters or return values or even both. Even if a lambda doesn't have a value to return, it has a body that does some useful actions (e.g. prints or saves something). You will encounter practical examples with such lambdas in the following topics.

Another important thing — like a regular Java object, a lambda expression always has a special type. There are a lot of types presented in the Java Standard Library. In this topic, we will only mention two of them: Function and BiFunction. Both of the classes are located in the java.util.function package among others.

You don't need to find and remember all possible types of lambda expressions at once. You will gradually do this as you learn.

Let's consider a single-line lambda expression that just checks whether the first number is divisible by the second one.

BiFunction<Integer, Integer, Boolean> isDivisible = (x, y) -> x % y == 0;

The expression has the type BiFunction<Integer, Integer, Boolean> which means, that it takes two Integer values and returns a Boolean value.

There are a lot of ways to write lambda expressions. Let's consider more examples.

// if it has only one argument "()" are optional
Function<Integer, Integer> adder1 = x -> x + 1;

// with type inference
Function<Integer, Integer> mult2 = (Integer x) -> x * 2;

// with multiple statements
Function<Integer, Integer> adder5 = (x) -> {
    x += 2;
    x += 3;
    return x;
};

Although Java provides a lot of ways to write lambda expressions, you always need to choose the shortest and most readable way to do this.

Invoking lambda expressions

Once a lambda expression is created, it can be used in other places of your program like a regular Java object. You can invoke the body of an expression using special methods like apply as many times as you need. The name of the method depends on the type of lambda expression.

boolean result4Div2 = isDivisible.apply(4, 2); // true
boolean result3Div5 = isDivisible.apply(3, 5); // false

So, we can invoke a lambda expression like a regular method passing arguments and obtaining results!

Passing lambda expressions to methods

One of the most popular use case is to pass a lambda expression to a method and then call it there.

Look at the method below. It takes an object of the standard generic Function type.

private static void printResultOfLambda(Function<String, Integer> function) {
    System.out.println(function.apply("HAPPY NEW YEAR 3000!"));
}

This function can take a String argument and return an Integer result. To test the method, let's create an object and pass it into the method:

// it returns the length of a string
Function<String, Integer> f = s -> s.length();
printResultOfLambda(f); // it prints 20

You can also pass a lambda expression to the method directly without an intermediate reference:

// passing without a reference
printResultOfLambda(s -> s.length()); // the result is the same: 20

As you can see, we can present our function as an object and pass it to a method as its argument, if the method takes an object of that type. Then, inside the method, the given function will be invoked.

But why do we need it? First, let's look at another example, which uses a more complex lambda expression and calculates the number of digits in the string.

// It prints the number of digits: 4
printResultOfLambda(s -> {
    int count = 0;
    for (char c : s.toCharArray()) {
        if (Character.isDigit(c)) {
            count++;
        }
    }
    return count;
});

What is important here? We do not pass data to the printResultOfLambda, but rather some piece of code as data. So, we can parameterize the same method with a different behavior at runtime. This is what typical uses of lambda expressions look like. Many standard methods can accept lambda expressions. This will be discussed in more detail in the following topics.

Let's introduce an important term according to the examples. In functional programming theory, a function that accepts or returns another function is called a higher-order function. In terms of Java, we're talking about methods or functions which take/return Function<T, R>BiFunction<T, U, R>, or other types we will consider soon.

Closures

Another important trick with lambda expressions is the possibility to capture values from a context where the lambda is defined and use the values within the body. This technique is called closure.

Capturing is possible only if a context variable has the final keyword or it's effectively final, i.e. the variable isn't changed after initialization. Otherwise, an error occurs.

Let's look at an example.

final String hello = "Hello, ";
Function<String, String> helloFunction = (name) -> hello + name;

System.out.println(helloFunction.apply("John"));
System.out.println(helloFunction.apply("Anastasia"));

The lambda expression captured the final variable hello.

The result of this code is:

Hello, John
Hello, Anastasia

Let's consider an example with an effective final variable.

int constant = 100;
Function<Integer, Integer> adder100 = x -> x + constant;

System.out.println(adder100.apply(200)); // 300
System.out.println(adder100.apply(300)); // 400

The constant variable is effectively final and is captured by the lambda expression.

Functional interfaces

Functional interfaces make it possible to use lambda expressions, method references, and other functional stuff. Since you are already familiar with interfaces, lambda expressions, and anonymous classes, we are going to show you how they are all connected with each other.

Functions and interfaces

If an interface contains only a single abstract method (such an interface is sometimes called a SAM type), it can be considered as a function. In addition to standard ways of implementing interfaces through inheritance or anonymous classes, these interfaces can have various implementations, including those using a lambda expression or a method reference

Here is a clone of the standard Function<T, R> interface that demonstrates the basic idea:

@FunctionalInterface 
interface Func<T, R> { 
    R apply(T val); // single abstract method
}

The @FunctionalInterface annotation is used to mark functional interfaces and to raise a compile-time error if an interface doesn't satisfy the requirements of a functional interface. The use of this annotation is not mandatory but recommended.

The Func<T, R> interface meets the requirements to be a functional interface because it has a single apply method, which takes a value of type T and returns a result of type R.

Here is an example of a lambda expression that implements this custom interface:

Func<Integer, Integer> multiplier10 = n -> n * 10;
System.out.println(multiplier10.apply(5)); // 50

In a similar way, all standard functions can be represented as functional interfaces, including BiFunction<T, U, R>. The concept of functional interfaces is another way to model functions instead of using regular methods.

It is worth noticing, that static and default methods are allowed in functional interfaces because these methods are not abstract:

@FunctionalInterface 
interface Func<T, R> { 
    R apply(T val);

    static void doNothingStatic() { }

    default void doNothingByDefault() { } 
}

This interface is a valid functional interface as well.

Implementing functional interfaces

There are several ways to implement a functional interface. As you know from the previous OOP theory, it is impossible to directly create an instance of an interface. Then what should we do?

First of all, we should implement the interface to create a concrete class. Then, create an instance of this concrete class. The main requirement is to implement the apply method to get a concrete behavior.

Let's consider three ways to do it:

1) Anonymous classes

Of course, like any other interface, a functional interface can be implemented by using an anonymous class or regular inheritance.

To implement a functional interface let's create an anonymous class and override the apply method. The overridden method calculates the square of a given value:

Func<Long, Long> square = new Func<Long, Long>() {
    @Override
    public Long apply(Long val) {
        return val * val;
    }
};

long val = square.apply(10L); // the result is 100L

In this example, we model a math function that squares a given value. This code works perfectly but it is a bit unclear since it contains a lot of extra characters to perform a single line of useful code.

We won't give you an example of a regular class because it has the same (and even more) disadvantages.

2) Lambda expressions

A functional interface can also be implemented and instantiated by using a lambda expression.

Here is a lambda expression that has the same behavior as the anonymous class above:

Func<Long, Long> square = val -> val * val; // the lambda expression

long val = square.apply(10L); // the result is 100L

The type of the functional interface (left) and the type of the lambda (right) are the same from a semantic perspective. Parameters and the result of a lambda expression correspond to the parameters and the result of a single abstract method of the functional interface.

The code that creates a lambda expression may look as if the object of an interface type is created. As you know, it is impossible. Actually, the Java compiler automatically creates a special intermediate class that implements the functional interface and then creates an object of this class rather than an object of an interface type. The name of such a class may look like Functions$$Lambda$14/0x0000000100066840 or something similar.

3) Method references

Another way to implement a functional interface is by using method references. In this case, the number and type of arguments and the return type of a method should match the number and types of arguments and the return type of the single abstract method of a functional interface.

Suppose, there is a square method that takes and returns a long value:

class Functions {

    public static long square(long val) {
        return val * val;
    }
}

The argument and the return type of this method fits the Func<Long, Long> functional interface. This means we can create a method reference and assign it to an object of the Func<Long, Long> type:

Func<Long, Long> square = Functions::square;

Keep in mind, that the compiler creates an intermediate hidden class that implements the Func<Long, Long> interface in a similar way to the case of lambda expressions.

Usually, method references are more readable than the corresponding lambda expressions. We recommend this way of implementing and instantiating functional interfaces when possible.

Create a free account to access the full topic

“It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.”
Andrei Maftei
Hyperskill Graduate