Java programming language

Functional programming

Using lambda expressions

  

Reference version: These notes use Java 25 LTS. Lambda expressions and the standard functional interfaces were introduced in Java 8.

Since Java 8, lambda expressions have provided a concise way to implement the single abstract method of a functional interface. In this document we are going to explain what they are, how to define them and how to use them.

1. Functional interfaces and lambda expressions

Functional interfaces are interfaces with exactly one abstract method. They may also contain default methods, static methods and methods corresponding to public methods of Object. The @FunctionalInterface annotation documents the intention and lets the compiler verify the definition, but an interface can be functional even without the annotation.

There are many useful examples of functional interfaces in Java API, such as:

Lambda expressions are an easy way to implement these interfaces, without having to define a new class, or even an anonymous class.

2. Defining lambda expressions

Lambda expressions define behaviour without declaring a named class. They can be written directly where they are needed or assigned to a variable and reused. Let’s see how to define these expressions with some examples.

2.1. A first example: java.io.FileFilter

java.io.FileFilter is a functional interface available in Java core API. It defines a method called accept so that we can define an acceptance criteria for files. In other words, when we implement this method, we must specify which files will be accepted from a list of files, according to one or many factors: file size, extension…

public interface FileFilter 
{
    boolean accept(File file);
}

We are going to implement this interface to accept only Java files (i.e. files with .java extension). We will see how to do this in different Java versions, so that you can compare the evolution of these programming patterns.

Implementation using a named class

One option is to create a named class for the implementation. This is useful when the implementation is complex or will be reused in several places:

public class JavaFileFilter implements FileFilter 
{
    @Override
    public boolean accept(File file)
    {
        return file.getName().endsWith(".java");
    }
}

And then use it like this:

File dir = new File(".");
File[] javaFiles = dir.listFiles(new JavaFileFilter());

Implementation using an anonymous class

An anonymous class lets us declare and instantiate the implementation at the point where it is used. Anonymous classes existed before Java 7 and are not exclusive to that version.

File dir = new File(".");
File[] javaFiles = dir.listFiles(new FileFilter() 
{
    @Override
    public boolean accept(File file) 
    {
        return file.getName().endsWith(".java");
    }
});

Implementation using a lambda expression (Java 8+)

Besides normal and anonymous classes, since Java 8, when we are implementing a functional interface, we can do it with less code using a lambda expression:

File dir = new File(".");
File[] javaFiles=dir.listFiles((File file) -> file.getName().endsWith(".java"));

We don’t have to specify that it’s a FileFilter interface what we are implementing because the compiler knows that the listFiles() method needs a FileFilter object as an argument. We don’t need to use the return word either, because the compiler will assume it. We can even omit the parameter type because the compiler can look at it in the interface definition. So the lambda expression can be even more simple, like this:

File[] javaFiles = dir.listFiles(file -> file.getName().endsWith(".java"));

2.2. Another example: java.util.Comparator

Comparator interface from java.util package is another functional interface. It has only one method called compare that takes two objects as parameters, and compares them returning an integer that tells us which object comes first. Let’s see how to implement this comparator to compare two String objects according to their length.

Implementation using a named class

A named class can be used when the comparison logic is going to be reused or needs a descriptive type. For instance:

public class MyStringComparator implements Comparator<String> 
{
    @Override
    public int compare(String s1, String s2) 
    {
        return Integer.compare(s1.length(), s2.length());
    }
}

// MAIN
List<String> list = Arrays.asList("Hello", "Hi", "Goodbye", "Farewell", "Bye");
MyStringComparator msc = new MyStringComparator();
...
Collections.sort(list, msc);

Implementation using an anonymous class

The same comparison can be implemented with an anonymous class:

// MAIN
List<String> list = Arrays.asList("Hello", "Hi", "Goodbye", "Farewell", "Bye");
Comparator<String> comp = new Comparator<String>() 
{
    @Override
    public int compare(String s1, String s2) 
    {
        return Integer.compare(s1.length(), s2.length());
    }
};
Collections.sort(list, comp);

Implementation using a lambda expression (Java 8+)

With Java 8 or later, we can use a lambda expression. In this case, the method to be implemented has two parameters, so we define both in the parentheses of the lambda expression:

Comparator<String> lComp = (s1,s2) -> Integer.compare(s1.length(), s2.length());
List<String> list = Arrays.asList("Hello", "Hi", "Goodbye", "Farewell", "Bye");
Collections.sort(list, lComp);

We can even shorten this code placing the lambda expression as the second parameter of Collections.sort method:

List<String> list = Arrays.asList("Hello", "Hi", "Goodbye", "Farewell", "Bye");
Collections.sort(list, (s1,s2) -> Integer.compare(s1.length(), s2.length()));

Modern Java also provides comparator factory methods:

list.sort(Comparator.comparingInt(String::length));

2.3. One more example: java.lang.Runnable

You will use this interface when talking about threads and multithreaded programming. It has only one method to implement, with no parameters nor return type. It is used in Thread objects to define the method these threads will execute.

Implementation using a named class

As usual, we can define a named class and use an object of that class:

public class MyRunnable implements Runnable 
{
    @Override
    public void run() 
    {
        for(int i = 0; i < 3; i++) 
        {
            System.out.println("This is thread: " + 
                Thread.currentThread().getName());
        }
    }
}

// MAIN
Runnable run = new MyRunnable();
Thread t = new Thread(run);
t.start();

Implementation using an anonymous class

We can also define an anonymous class at the point where the implementation is needed:

// MAIN
Runnable run = new Runnable() 
{
    @Override
    public void run() 
    {
        for(int i = 0; i < 3; i++) 
        {
            System.out.println("This is thread: " + 
                Thread.currentThread().getName());
        }
    }
};
Thread t = new Thread(run);
t.start();

Implementation using a lambda expression (Java 8+)

If we use Java 8 or later, we can also use a lambda expression. In this case, as the method has no parameters, we leave the parentheses of the lambda expression empty (we need to type the parentheses anyway):

Runnable lambdaRun = () -> {
    for(int i = 0; i < 3; i++) 
    {
        System.out.println("This is thread: " + 
            Thread.currentThread().getName());
    }
};
Thread t = new Thread(lambdaRun);
t.start();

Note that, in this example, our code needs more than one sentence, so we need to use the curly brackets { … } after the arrow of the lambda expression. Again, we can also define the lambda expression in the parameter of Thread constructor:

Thread t = new Thread(() -> {
    for(int i = 0; i < 3; i++) 
    {
        System.out.println("This is thread: " + 
            Thread.currentThread().getName());
    }
});
t.start();

2.4. Conclusions

When we want to use a lambda expression, we only have to focus on the implemented method, and check:

Then, in the place of our application where we want to use the lambda expression, we define an object of the given interface, and define the lambda expression as follows:

Remember the way we have created the lambda expression for Comparator (two parameters and an int result):

Comparator<String> lComp = (s1,s2) -> Integer.compare(s1.length(), s2.length());

and the way we have created the lambda expression for Runnable (no parameters nor return type):

Runnable lambdaRun = () -> {
    for(int i = 0; i < 3; i++) 
    {
        System.out.println("This is thread: " + 
            Thread.currentThread().getName());
    }
};

Some more concepts we can take into account

There is a way to make a lambda expression even shorter. When it contains a method call that takes the same parameters as the lambda expression and in the same order, we can just write a reference to that method, omitting even the parameters, using this especial syntax:

// Normal version
Comparator<Integer> comp = (i1, i2) -> Integer.compare(i1, i2);
// Shorter version
Comparator<Integer> comp2 = Integer::compare;

Although lambda expressions and anonymous classes can sometimes solve the same problem, they have different language semantics. A lambda does not declare an anonymous class in the source code. The Java compiler and JVM are free to choose an efficient runtime implementation, so we should choose lambdas primarily for clarity and suitability for the target functional interface.

Exercise 1:

Create a project called BookComparators, with a Book class to store some information about books (title and price). Then, in the main program, create a list of books and use lambda expressions to sort and print the list by title (in alphabetical, ascending order) and then by price (in descending order).

Exercise 2:

Create a project called ListFilter that includes the following:

List<Student> filterStudents(List<Student> srcList, Predicate<Student> predicate)

The filterStudents method receives a student list and returns another list with only the items which meet the condition defined in the Predicate. A Predicate is a functional interface that needs to implement a method (boolean test(T t)). Implement it using lambda expressions.

In the main method you’ll have to create a list of at least 8 students, and then, using filterStudents method, generate 3 other lists that only hold students who: