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

Published in the Java Developer group
Who is this article for?
  • It's for people who read the first part of this article;
  • 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.
If 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. An explanation of lambda expressions in Java. With examples and tasks. Part 2 - 1The 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.

Access to external variables

Does this code compile with an anonymous class?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
No. The counter variable must be final. Or if not final, then at least it cannot change its value. The same principle applies in lambda expressions. They can access all the variables that they can "see" from the place they are declared. But a lambda must not change them (assign a new value to them). However, there is a way to bypass this restriction in anonymous classes. Simply create a reference variable and change the object's internal state. In doing so, the variable itself does not change (points to the same object) and can be safely marked as final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Here our counter variable is a reference to an AtomicInteger object. And the incrementAndGet() method is used to change the state of this object. The value of the variable itself does not change while the program is running. It always points to the same object, which lets us declare the variable with the final keyword. Here are the same examples, but with lambda expressions:

int counter = 0;
Runnable r = () -> counter++;
This won't compile for the same reason as the version with an anonymous class: counter must not change while the program is running. But everything is fine if we do it like this:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
This also applies to calling methods. Within lambda expressions, you can not only access all "visible" variables, but also call any accessible methods.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Although staticMethod() is private, it is accessible inside the main() method, so it can also be called from inside a lambda created in the main method.

When is a lambda expression executed?

You may find the following question too simple, but you should ask it just the same: when will the code inside the lambda expression be executed? When it is created? Or when it is called (which is not yet known)? This is fairly easy to check.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Screen output:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
You can see that the lambda expression was executed at the very end, after the thread was created and only when the program's execution reaches the run() method. Certainly not when it is declared. By declaring a lambda expression, we've only created a Runnable object and described how its run() method behaves. The method itself is executed much later.

Method references?

Method references aren't directly related to lambdas, but I think it makes sense to say a few words about them in this article. Suppose we have a lambda expression that doesn't do anything special, but simply calls a method.

x -> System.out.println(x)
It receives some x and just calls System.out.println(), passing in x. In this case, we can replace it with a reference to the desired method. Like this:

System.out::println
That's right — no parentheses at the end! Here's a more complete example:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
In the last line, we use the forEach() method, which takes an object that implements the Consumer interface. Again, this is a functional interface, having just one void accept(T t) method. Accordingly, we write a lambda expression that has one parameter (because it is typed in the interface itself, we don't specify the parameter type; we only indicate that we will call it x). In the body of the lambda expression, we write the code that will be executed when the accept() method is called. Here we simply display what ended up in the x variable. This same forEach() method iterates through all the elements in the collection and calls the accept() method on the implementation of the Consumer interface (our lambda), passing in each item in the collection. As I said, we can replace such a lambda expression (one that simply class a different method) with a reference to the desired method. Then our code will look like this:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
The main thing is that the parameters of the println() and accept() methods match. Because the println() method can accept anything (it is overloaded for all primitives types and all objects), instead of lambda expressions, we can simply pass a reference to the println() method to forEach(). Then forEach() will take each element in the collection and pass it directly to the println() method. For anyone encountering this for the first time, please note that we are not calling System.out.println() (with dots between words and with parentheses at the end). Instead, we are passing a reference to this method. If we write this

strings.forEach(System.out.println());
we will have a compilation error. Before the call to forEach(), Java sees that System.out.println() is being called, so it understands that the return value is void and will try to pass void to forEach(), which is instead expecting a Consumer object.

Syntax for method references

It's quite simple:
  1. We pass a reference to a static method like this: ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. We pass a reference to a non-static method using an existing object, like this: objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. We pass a reference to a non-static method using the class that implements it as follows: ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. We pass a reference to a constructor like this: ClassName::new

    Method references are very convenient when you already have a method that would work perfectly as a callback. In this case, instead of writing a lambda expression containing the method's code, or writing a lambda expression that simply calls the method, we simply pass a reference to it. And that's it.

An interesting distinction between anonymous classes and lambda expressions

In an anonymous class, the this keyword points to an object of the anonymous class. But if we use this inside a lambda, we gain access to the object of the containing class. The one where we actually wrote the lambda expression. This happens because lambda expressions are compiled into a private method of the class they are written in. I would not recommend using this "feature", since it has a side effect and that contradicts the principles of functional programming. That said, this approach is entirely consistent with OOP. ;)

Where did I get my information and what else should you read?

And, of course, I found a ton of stuff on Google :)
Comments (2)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Esperanza Cutillas Rastoll Level 2, Calp, Spain
6 February 2021
This is great! Thanks
Seb Level 41, Crefeld, Germany
23 March 2020
Fantastic explanation. :-)