Core Java

Lambda Expressions Java Tutorial

In this post, we feature a comprehensive Lambda Expressions Java Tutorial.

1. Lambda Expressions Java Tutorial – Introduction

Lambda Expressions are considered as one of the best features which were introduced in Java 8. Lambda Expressions are considered as Java’s first step into the Functional Programming world. It can be seen as a function which can be created without a class. It can also be passed like a parameter and can be executed when and as needed. Java Lambda Expressions are a concise representation of anonymous functions which can be passed around. Anonymous classes with a single function presented an unwieldy presentation with extra syntax. These expressions aim to clear that confusion.

You can also check this tutorial in the following video:

Java Lambda Expressions Tutorial – video

As we already stated above, Java Lambda Expressions are nameless functions which can be passed as constant values. This means that they can be present anywhere where any other constant value might have been present, but are typically written as a parameter to some other function. To consider a canonical example, we can pass a comparison function to a generic sort function, and instead of going to the trouble of defining a whole procedure (and incurring the lexical discontinuity and namespace pollution) to describe this comparison, we can just pass a lambda expression describing the comparison. Let us look at some of the properties of a Lambda Expression:

  • Anonymous: It can still be called as anonymous because it doesn’t have an explicit name.
  • Concise: As mentioned the case with Anonymous classes, we write a lot less code with Lambdas as compared to the Anonymous classes.
  • Function: A Lambda is more like a function than a method. This is because a method belongs to a class whereas a Lambda doesn’t. But just like a method, a Lambda accepts a list of parameters, has a body and can throw exceptions as well.
  • Can be passed: A Lambda can be passed around to other functions just like a normal parameter.

To clear any misconceptions which might arise due to the points we mentioned above, lambda doesn’t add any more functionality we had prior to its introduction. It just improves how we write our code and reduces a lot of boilerplate code. This boilerplate code is even related to the system-level programming we used to do to make out code exploit the multi-core nature of the underlying Operating System. Let’s see how this simple syntactical sugar makes our work easier in terms of parallelism, code cleanliness and compactness.

2. Writing Lambda Expressions

In this section, we will see how Java Lambda Expressions can reduce the lines of code which needs to be written to perform some simple operations. For instance, we will compare the number of lines of code to make a Comparator function. To establish a comparison, we will make a simple POJO class here, a Student class which contains a Student’s ID as a Long and name as a String parameter:

Student.java

1
2
3
4
5
6
7
public class Student {
   
   private Long id;
   private String name;
 
   // standard setters and getters
}

It is very general programming practice to compare even the POJO objects we define in our applications. If we want to compare two Student class objects, we can make a Comparator like:

Comparator with Anonymous class

1
2
3
4
5
6
Comparator<Student> byId = new Comparator<Student>() {
   @Override
   public int compare(Student s1, Student s2) {
       return s1.getId().compareTo(s2.getId());
   }
};

This was a simple Comparator implementation as an Anonymous class but we will find that the same implementation, when done with Lambda is so much precise and clean. Let us see the same task done with a Lambda expression here:

pom.xml

1
Comparator<Student> byId = (s1, s2) -> s1.getId().compareTo(s2.getId());

Above Lambda Expression can also be called as a Block Lambda Expression as it is made up of a single block of code on the right-hand side of the > symbol. Sounds amazing that it can get even more concise and small, see this code snippet:

Concise implementation of Lambda

1
Comparator<Student> byId = Comparator.comparing(Student::getId);

That is a great way of establishing a comparator and simple as well. For the Block Lambda Expression we made above, let us break it in parts to understand better:

Lambda Expressions Java - Lambda Expression
Lambda Expression
  • Lambda Expression starts with a list of parameters which are passed to the function, Comparator in the above case
  • The Arrow symbol separates the Lambda Expression parameters from the Lambda body
  • The body clearly compares the two Student Objects with their id and this expression defines the Lambda return value

This is to be noted that the compiled code i.e. the bytecode of the anonymous class version and the Lambda expression version will exactly be the same as Lambda expressions are only syntactical sugar to make the code clean. Although, with Lambda expression the code might become less-readable sometimes.

3. Lambda Expressions vs Anonymous class

The code we write using Lambda Expressions can be written using Anonymous classes as well which achieves exactly the same as Lambda Expressions. The difference will be the concise nature of the code with Lambda.

For a comparison example, let us construct a class and a method which takes a Runnable as an input:

Runnable class

1
2
3
4
5
public class RunnableInstance {
    public static void doSomething(Runnable runnable){
        runnable.run();
    }
}

When we make a Runnable using an Anonymous class, here is what it can look like:

Runnable with Anonymous class

1
2
3
4
5
6
7
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.print("Anonymous class implementation.");
    }
};
doSomething(runnable);

Let us try to convert above code into a Lambda expressions and see how clean things can get:

Runnable with Lambda

1
2
Runnable runnable = () -> System.out.print("Lambda Expression.");
doSomething(runnable);

In cases where we do not want to use the runnable implementation more than once, we can even avoid making the reference:

Concise Lambda Runnable

1
doSomething(() -> System.out.print("Lambda Expression."));

4. Parallel programming with Lambda Expressions

Whenever we talk about threads, most of us take a step back and think if we really need to implement threading in our application to support parallelism due to it being trivial in nature and difficult to manage. When we have a collection of items and we implement a lambda like:

Parallel Programming

1
collection.map { // my lambda }

Here, the collection itself is capable of implementing parallelism with the supplied Lambda without having to implement threading ourself. This means, on a multi-core environment, the Lambda can take advantage of multiple cores while streaming over a collection. Like if we consider a simple example:

Lambda with Parallel Stream

1
2
3
List<String> names = students.stream()
        .map(s -> s.getName().toUpperCase())
        .collect(Collectors.toList());

The map function can run in parallel in a multi-core environment to process multiple objects at once without us doing anything about that. For this, only thing which is needed is that the Operating system this program runs on must be multi-core. Once this condition is fulfilled, we can be sure that any operation which can be parallelized in the given statements, they will be done automatically.

5. Collections & Streams

The Collections framework is one of the most used Framework API in Java. The Collections allows us to collect similar objects into a data structure which may be optimised for a purpose. All examples up ahead require collections of objects, so imagine we have a collection of objects of type Student, like we defined earlier:

Student Collection

1
List students = getStudentObjectCollection();

We start by the new method stream() that was added to the Collection interface. Since all collections “extends” Collection, all Java collections have inherited this method:

Student Stream

1
2
List students = getStudentObjectCollection();
Stream stream = students.stream(); // a stream of student objects

Even though it looks like it, the Stream interface is not another regular type of collection. We can see Stream as a “data flow” abstraction which allows us to transform or manipulate the data it is containing. Unlike the other collections we have studied in Java, a Stream doesn’t allow us a direct access to the element it contains. Although if you want to access the elements, we can always transform the stream into one of the collections in Java and fulfill our purpose.

For demonstration purposes, we’ll see how our code would look like if we had to count how many odd-ID objects we have in our collection of students. First, let’s see how this can be done without the use of streams:

Counting odd IDs

1
2
3
4
5
6
7
long count = 0;
List students = getStudentObjectCollection();
for (Student s : students) {
    if (s.getId() % 2 == 1) {
        count++;
    }
}

Using a for loop, we created a counter that is incremented each time an odd ID is encountered in the list of students. We have written this type of code hundreds of time which spans over multiple lines for a very simple task.

We can write exactly the same code using a Stream in a single line as well:

Using Stream

1
2
List students = getStudentObjectCollection();
long count = students.stream().filter(student -> student.getId() % 2 == 1).count();

Doesn’t this look much neat and cleaner than the previous approach with a for loop? It all started by calling the stream() method which transformed the given collection into a Stream, all the other calls are chained together since most methods in the Stream interface were designed with the Builder Pattern in mind. For those not used to method chaining like these, it might be easier to visualize like this:

Visualising Stream

1
2
3
4
List students = getStudentObjectCollection();
Stream stream = students.stream();
stream = stream.filter(student -> student.getId() % 2 == 1);
long count = stream.count();

Let’s focus our attention in the two methods of the Stream we used, filter() and count().

The filter() method takes the condition by which we want to filter our collection and this condition is represented by a lambda expression who takes one parameter and returns a boolean:

Lambda Condition

1
student -> student.getId() % 2 == 1

Not by chance, the functional interface used to represent this expression, the parameter of the filter() method, is the Predicate interface. It has only one abstract method, boolean test(T t):

Functional Interface

1
2
3
4
5
6
7
@FunctionalInterface
public interface Predicate {
 
    boolean test(T t);
 
    // non-abstract methods here
}

The parameterized type T is representing the type of the element of our stream, that is, Student objects. After the filtering, all that is left is to call the count() method. There’s not much to it, it simply counts how many objects we have left in our stream after the filtering took place (we could have many more things besides just filtering). The count() method is considered a “terminal operation” and after it’s invoked that stream is said to be “consumed” and can no longer be used.

6. Downsides for Lambda Expressions

Although the code with Lambda Expressions looks very concise, there are some downsides to Lambdas as well. Let’s study about some of them here:

  • Can’t handle checked exceptions: Any code which throws checked exceptions should be wrapped in try-catch statements. But even if we do that, it’s not always clear what happens to the thrown exception.
  • Performance issues: Due to the reason that JIT cannot always optimize the forEach() + lambda to the same extent as plain loops, Lambdas can affect performance to a very slight extent.
  • Debugging Challenges: It is clear that with Lambdas, the code is not always as clear as it is concise. This makes the stack trace of the exceptions occurring in code and readability a little difficult.

Even though Lambdas have some downsides, they still make a great companion when you’re writing concise code.

7. Conclusion

Java Lambda Expressions appear (with different syntax) in all LISPs, Perl, Python, and sufficiently-recent versions of C++, Objective C, C# and Java 8, but notably not in C even though it has a way to deal with passing functions (or some excuse for them) around as parameters. They are a syntax element with particular semantics, and those semantics place more requirements on the runtime than C was designed to require.

We can read much more about Lambda expressions in this lesson which related deeply with Functional interfaces and also demonstrates the performance comparisons of using Parallel Streams with Lambda expressions and gain a deeper understanding about how Lambda expressions work with functional interfaces and can be used in simple statements to exploit the parallelism which is offered by a multi-core Operating System without having to understand the APIs working behind the scene.

Last updated on Feb. 17th, 2020

Shubham Aggarwal

Shubham is a Java EE Engineer with about 3 years of experience in building quality products with Spring Boot, Spring Data, AWS, Kafka, PrestoDB.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jay
Jay
5 years ago

Very confusion data. Try to explain with easy examples.

Greg Mudd
Greg Mudd
5 years ago

Very good, simple example showing a use for Streams as well as Lambda’s

Carl Ellis
Carl Ellis
5 years ago

The first example is great at telling how to implement a lambda, it would have been better to show it used in a small code example.

Developers beginning to use lambdas are best served seeing complete examples. Oracle’s tutorials on Java lambdas are a good learning resource.

zbig
zbig
5 years ago

“The map function can run in parallel in a multi-core environment to process multiple objects at once without us doing anything about that. For this, only thing which is needed is that the Operating system this program runs on must be multi-core.”

Well, I do not think so. Stream are by default serial.

Back to top button