An explanation of lambda expressions in Java. With examples and tasks. Part 1

Published in the Java Developer group
Who is this article for?
  • It's for people who think they already know Java Core well, but have no clue about lambda expressions in Java. Or maybe they've heard something about lambda expressions, but the details are lacking
  • It's for people who have a certain understanding of lambda expressions, but are still daunted by them and unaccustomed to using them.
An explanation of lambda expressions in Java. With examples and tasks. Part 1 - 1If you don't fit one of these categories, you might find this article boring, flawed, or generally not your cup of tea. In this case, feel free to move on to other things or, if you're well versed in the subject, please make suggestions in the comments on how I could improve or supplement the article. The material does not claim to have any academic value, let alone novelty. Quite the contrary: I will try to describe things that are complex (for some people) as simply as possible. A request to explain the Stream API inspired me to write this. I thought about it and decided that some of my stream examples would be incomprehensible without an understanding of lambda expressions. So we'll start with lambda expressions. What do you need to know to understand this article?
  1. You should understand object-oriented programming (OOP), namely:

    • classes, objects, and the difference between them;
    • interfaces, how they differ from classes, and relationship between interfaces and classes;
    • methods, how to call them, abstract methods (i.e. methods without an implementation), method parameters, method arguments and how to pass them;
    • access modifiers, static methods/variables, final methods/variables;
    • inheritance of classes and interfaces, multiple inheritance of interfaces.
  2. Knowledge of Java Core: generic types (generics), collections (lists), threads.
Well, let's get to it.

A little history

Lambda expressions came to Java from functional programming, and to there from mathematics. In the United States in the middle of the 20th century, Alonzo Church, who was very fond of mathematics and all kinds of abstractions, worked at Princeton University. It was Alonzo Church who invented the lambda calculus, which was initially a set of abstract ideas entirely unrelated to programming. Mathematicians such as Alan Turing and John von Neumann worked at Princeton University at the same time. Everything came together: Church came up with the lambda calculus. Turing developed his abstract computing machine, now known as the "Turing machine". And von Neumann proposed a computer architecture that has formed the basis of modern computers (now called a "von Neumann architecture"). At that time, Alonzo Church's ideas didn't become so well known as the works of his colleagues (with the exception of the field of pure mathematics). However, a little later John McCarthy (also a Princeton University graduate and, at the time of our story, an employee of the Massachusetts Institute of Technology) became interested in Church's ideas. In 1958, he created the first functional programming language, LISP, based on those ideas. And 58 years later, the ideas of functional programming leaked into Java 8. Not even 70 years have passed... Honestly, this isn't the longest it's taken for a mathematical idea to be applied in practice.

The heart of the matter

A lambda expression is a kind of function. You can consider it to be an ordinary Java method but with the distinctive ability to be passed to other methods as an argument. That's right. It has become possible to pass not only numbers, strings, and cats to methods, but also other methods! When might we need this? It would be helpful, for example, if we want to pass some callback method. That is, if we need the method we call to have the ability to call some other method that we pass to it. In other words, so we have the ability to pass one callback under certain circumstances and a different callback in others. And so that our method that receives our callbacks calls them. Sorting is a simple example. Suppose we're writing some clever sorting algorithm that looks like this:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
In the if statement, we call the compare() method, passing in two objects to be compared, and we want to know which of these objects is "greater". We assume the "greater" one comes before the "lesser" one. I put "greater" in quotes, because we are writing a universal method that will know how to sort not only in ascending order, but also in descending order (in this case, the "greater" object will actually be the "lesser" object, and vice versa). To set the specific algorithm for our sort, we need some mechanism to pass it to our mySuperSort() method. That way we'll be able to "control" our method when it is called. Of course, we could write two separate methods — mySuperSortAscend() and mySuperSortDescend() — for sorting in ascending and descending order. Or we could pass some argument to the method (for example, a boolean variable; if true, then sort in ascending order, and if false, then in descending order). But what if we want to sort something complicated such as a list of string arrays? How will our mySuperSort() method know how to sort these string arrays? By size? By the cumulative length of all the words? Perhaps alphabetically based on the first string in the array? And what if we need to sort the list of arrays by array size in some cases, and by the cumulative length of all the words in each array in other cases? I expect you've already heard about comparators and that in this case we would simply pass to our sorting method a comparator object that describes the desired sorting algorithm. Because the standard sort() method is implemented based on the same principle as mySuperSort(), I will use sort() in my examples.

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 
arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

Comparator<;String[]> sortByLength = new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}; 

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() { 

    @Override 
    public int compare(String[] o1, String[] o2) { 
        int length1 = 0; 
        int length2 = 0; 
        for (String s : o1) { 
            length1 += s.length(); 
        } 

        for (String s : o2) { 
            length2 += s.length(); 
        } 

        return length1 - length2; 
    } 
};

arrays.sort(sortByLength);
Result:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Here the arrays are sorted by the number of words in each array. An array with fewer words is considered "lesser". That's why it comes first. An array with more words is considered "greater" and gets placed at the end. If we pass a different comparator to the sort() method, such as sortByCumulativeWordLength, then we'll get a different result:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Now the are arrays are sorted by the total number of letters in the words of the array. In the first array, there are 10 letters, in the second — 12, and in the third — 15. If we have only a single comparator, then we don't have to declare a separate variable for it. Instead, we can simply create an anonymous class right at the time of the call to the sort() method. Something like this:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 

arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}); 
We'll get the same result as in the first case.Task 1. Rewrite this example so that it sorts arrays not in ascending order of the number of words in each array, but in descending order. We already know all this. We know how to pass objects to methods. Depending on what we need at the moment, we can pass different objects to a method, which will then invoke the method that we implemented. This begs the question: why in the world do we need a lambda expression here?  Because a lambda expression is an object that has exactly one method. Like a "method object". A method packaged in an object. It just has a slightly unfamiliar syntax (but more on that later). Let's take another look at this code:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Here we take our arrays list and call its sort() method, to which we pass a comparator object with a single compare() method (it's name doesn't matter to us — after all, it's this object's only method, so we can't go wrong). This method has two parameters that we'll work with. If you're working in IntelliJ IDEA, you probably saw it offer to significantly condense the code as follows:

arrays.sort((o1, o2) -> o1.length - o2.length);
This reduces six lines to a single short one. 6 lines are rewritten as one short one. Something disappeared, but I guarantee it wasn't anything important. This code will work exactly the same way as it would with an anonymous class. Task 2. Take a guess at rewriting the solution to Task 1 using a lambda expression (at the very least, ask IntelliJ IDEA to convert your anonymous class to a lambda expression).

Let's talk about interfaces

In principle, an interface is simply a list of abstract methods. When we create a class that implements some interface, our class must implement the methods included in the interface (or we have to make the class abstract). There are interfaces with lots of different methods (for example, List), and there are interfaces with only one method (for example, Comparator or Runnable). There are interfaces that don't have a single method (so-called marker interfaces such as Serializable). Interfaces that have only one method are also called functional interfaces. In Java 8, they are even marked with a special annotation: @FunctionalInterface. It is these single-method interfaces that are suitable as target types for lambda expressions. As I said above, a lambda expression is a method wrapped in an object. And when we pass such an object, we are essentially passing this single method. It turns out that we don't care what the method is called. The only things that matters to us are the method parameters and, of course, the body of the method. In essence, a lambda expression is the implementation of a functional interface. Wherever we see an interface with a single method, an anonymous class can be rewritten as a lambda. If the interface has more or less than one method, then a lambda expression won't work and we'll instead use an anonymous class or even an instance of an ordinary class. Now it's time to dig into lambdas a bit. :)

Syntax

The general syntax is something like this:

(parameters) -> {method body}
That is, parentheses surrounding the method parameters, an "arrow" (formed by a hyphen and greater-than sign), and then the method body in braces, as always. The parameters correspond to those specified in the interface method. If variable types can be unambiguously determined by the compiler (in our case, it knows that we're working with string arrays, because our List object is typed using String[]), then you don't have to indicate their types.
If they are ambiguous, then indicate the type. IDEA will color it gray if it is not needed.
You can read more in this Oracle tutorial and elsewhere. This is called "target typing". You can name the variables whatever you want — you don't have to use the same names specified in the interface. If there are no parameters, then just indicate empty parentheses. If there is only one parameter, simply indicate the variable name without any parentheses. Now that we understand the parameters, it's time to discuss the body of the lambda expression. Inside the curly braces, you write code just as you would for an ordinary method. If your code consists of a single line, then you can omit the curly braces entirely (similar to if-statements and for-loops). If your single-line lambda returns something, you don't have to include a return statement. But if you use curly braces, then you must explicitly include a return statement, just as you would in an ordinary method.

Examples

Example 1.

() -> {}
The simplest example. And the most pointless :), since it doesn't do anything. Example 2.

() -> ""
Another interesting example. It takes nothing and returns an empty string (return is omitted, because it is unnecessary). Here's the same thing, but with return:

() -> { 
    return ""; 
}
Example 3. "Hello, World!" using lambdas

() -> System.out.println("Hello, World!")
It takes nothing and returns nothing (we can't put return before the call to System.out.println(), because the println() method's return type is void). It simply displays the greeting. This is ideal for an implementation of the Runnable interface. The following example is more complete:

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Or like this:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Or we can even save the lambda expression as a Runnable object and then pass it to the Thread constructor:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Let's take a closer look at the moment when a lambda expression is saved to a variable. The Runnable interface tells us that its objects must have a public void run() method. According to the interface, the run method takes no parameters. And it returns nothing, i.e. its return type is void. Accordingly, this code will create an object with a method that doesn't take or return anything. This perfectly matches the Runnable interface's run() method. That's why we were able to put this lambda expression in a Runnable variable.  Example 4.

() -> 42
Again, it takes nothing, but it returns the number 42. Such a lambda expression can be put in a Callable variable, because this interface has only one method that looks something like this:

V call(),
where V is the return type (in our case, int). Accordingly, we can save a lambda expression as follows:

Callable<Integer> c = () -> 42;
Example 5. A lambda expression involving several lines

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Again, this is a lambda expression with no parameters and a void return type (because there is no return statement).  Example 6

x -> x
Here we take an x variable and return it. Please note that if there's only one parameter, then you can omit the parentheses around it. Here's the same thing, but with parentheses:

(x) -> x
And here's an example with an explicit return statement:

x -> { 
    return x;
}
Or like this with parentheses and a return statement:

(x) -> { 
    return x;
}
Or with an explicit indication of the type (and thus with parentheses):

(int x) -> x
Example 7

x -> ++x
We take x and return it, but only after adding 1. You can rewrite that lambda like this:

x -> x + 1
In both cases, we omit the parentheses around the parameter and method body, along with the return statement, since they are optional. Versions with parentheses and a return statement are given in Example 6. Example 8

(x, y) -> x % y
We take x and y and return the remainder of division of x by y. The parentheses around the parameters are required here. They are optional only when there is only one parameter. Here it is with an explicit indication of the types:

(double x, int y) -> x % y
Example 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
We take a Cat object, a String name, and an int age. In the method itself, we use the passed name and age to set variables on the cat. Because our cat object is a reference type, it will be changed outside the lambda expression (it will get the passed name and age). Here's a slightly more complicated version that uses a similar lambda:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    private String name; 
    private int age; 

    @Override 
    public void setName(String name) { 
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age; 
    } 

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' + 
                ", age=" + age + 
                '}';
    }
}
Result:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
As you can see, the Cat object had one state, and then the state changed after we used the lambda expression. Lambda expressions combine perfectly with generics. And if we need to create a Dog class that also implements HasNameAndAge, then we can perform the same operations on Dog in the main() method without changing the lambda expression. Task 3. Write a functional interface with a method that takes a number and returns a boolean value. Write an implementation of such an interface as a lambda expression that returns true if the passed number is divisible by 13. Task 4. Write a functional interface with a method that takes two strings and also returns a string. Write an implementation of such an interface as a lambda expression that returns the longer string. Task 5. Write a functional interface with a method that takes three floating-point numbers: a, b, and c and also returns a floating-point number. Write an implementation of such an interface as a lambda expression that returns the discriminant. In case you forgot, that's D = b^2 — 4ac. Task 6. Using the functional interface from Task 5, write a lambda expression that returns the result of a * b^c. An explanation of lambda expressions in Java. With examples and tasks. Part 2
Comments (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Seb Level 41, Crefeld, Germany
23 March 2020
Excellent. Thank you.