Introduction to Java Lambda.
Introduction
Java 8 introduced a significant feature named Lambda that enables the developers to use functional programming constructs in Java. Experienced Java professionals (including me), who are deep-rooted in using object-oriented ways of solving problems, find it difficult to grasp concepts of Lambda and other functional constructs. I read several blogs and book excerpts before understanding how to use Lambdas in day to day programming work.
In this post, let us look at what Lambdas are and how to approach and solve problems in a more functional way using Lambda expressions. After finishing this article, you will be well equipped to start using these concepts in your day to day programming. At least, you will be at ease understanding the program flow when you see a codebase littered with functional programming constructs.
So What is Lambda?
First thing first. Functionality wise, there is nothing that Lambda adds to Java. Any code that you write with Lambda, can be written in the old version of Java with lengthy and verbose code. Whatever you can solve with Java 8 Lambda, can be solved with the previous version of Java. Lambda’s help us to write to concise code and to pass the behavior to methods. I will explain that in more detail later.
Below is the definition of Lambda and its characteristics:
- The reason we call Lambda an anonymous function is because it doesn’t belong to any class.
- It doesn’t have any name. All it has is a list of parameters and body.
- It is a piece of code that stands by itself which doesn’t get executed until invoked explicitly.
- The primary benefit of Lambda is that it enables us to pass the code around to other methods.
- Lambda expressions take the below form: (parameter list) -> { body }
- An arrow symbol (->) separates the parameter list and body. If the body has only one statement, then curly braces can be omitted. If the body has multiple statements, then wrap them around curly braces.
- The last important thing to remember is that the Lambda expression can be used only in the context of Functional Interfaces. Which means, lambda expression can be passed to a method that accepts an instance of Functional Interface. We will talk about Functional Interfaces in a bit.
Example and Syntax
For example, Java 7 way of declaring a method.
public int add(int a, int b){
return a+b;
}
Using Lambda, we can write the same method as below:
(a,b) -> a+b
or
(a,b) -> { return a+b; }
As you can see, Lambda’s removed the need for declaring the below things
- No need for Access modifiers as it is not a method in a class.
- No need to declare the types of variables passed.
- No need to declare the return type.
Where can you use Lambdas.
Now that we have what is a Lambda and how to declare it, let’s see where and when to use it. Note that just by declaring a lambda expression, doesn’t mean that it gets executed immediately. Lambda code can be passed to any method which accepts a Functional Interface. We will get to this in a bit.
Let’s look at a few more examples. All the below examples use a below Person java class as below.
public class Person {
private String firstName;
private String lastName;
private int age;
private String city;
public Person(String firstName, String lastName, int age, String city) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.city = city;
}
// getters and setters
}
For example, let’s say that you need to sort a list of Persons by their Age in Ascending order.
First Iteration: Java 7 typical Implementation using Custom Comparator. Here we implement a custom comparator and use its instance to sort the list of persons.
class NameComparator implements Comparator<User>{
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
}
List<Person> persons = // list of Persons
persons.sort(new NameComparator());
Second Iteration: Java 7 Implementation using Anonymous classes. Java has a cool feature of creating a class on demand without declaring it as a separate class. As seen below, we are creating an instance of Comparator Interface inline. Anonymous classes are the Java 7 way of Lambda which is used to pass the functionality to the methods without declaring it in a separate class.
persons.sort(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
Third Iteration: Java 8 using Lambdas.
Comparator<Person> c = (p1, p2) -> p1.getAge() - p2.getAge();
persons.sort(c);
OR Even better
persons.sort((p1, p2) -> p1.getAge() - p2.getAge());
As you see above, the code is clear and concise. How does the compiler knows the type of objects p1 and p2 and also the return type? It is because the Comparator interface is a Functional Interface.
What is a Functional Interface.
Functional interface is another name for Single Abstract Method Interfaces. As the name suggests, any interface with just one abstract method is called as Functional Interface.
The Functional Interfaces has just one abstract method but can have any number of default or static methods. The key is that it has only one method. This is how the compiler derives the types of parameters and also the return type of lambda expression. If the interface had multiple methods, then the compiler would not know which method to bind for the given lambda expression. So Functional Interface is a key concept that you need to understand to leverage the power of Lambda.
Java 8 has a new annotation called @FunctionalInterface. This annotation is just used to designate and alert the developers that this interface is a functional interface. Even without the annotation, if an interface has just only method, it is called as Functional interface and Lambda expressions will work the same way. But in the future, if a developer adds a new method to that interface, then that breaks the Lambda code. Hence, to designate an interface as functional interface, annotate the interface with @FunctionalInterface annotation.
So now let’s create our own functional interface and see how we can pass the behavior using Lambda Expression. Let’s take Person class that was described earlier. Let’s try to filter and print persons using two different conditions.
- Person who lives in the city London.
- Person who lives in the city London, with age above 50 and name starting with C.
First, let’s define our Functional Interface PersonFilter and also a method that accepts a PersonFilter that filters the persons.
interface PersonFilter{
public abstract boolean filter(Person p);
}
public void filterAndPrint(List<Person> persons, PersonFilter personFilter){
for(Person p: persons){
if(personFilter.filter(p)){
System.out.println(p.toString());
}
}
}
Now to filter the Persons, using Java 7 Interface Implementation, create an implementation of the interface and pass to the filterAndPrint method:
class PersonfilterByCity implements PersonFilter{
@Override
public boolean filter(Person p) {
return p.getCity().equalsIgnoreCase("London");
}
}
filterAndPrint(persons, new PersonfilterByCity());
Filter persons by City, Age, and LastName using Java 7 Anonymous Class
filterAndPrint(persons, new PersonFilter() {
@Override
public boolean filter(Person p) {
return p.getCity().equalsIgnoreCase("London") && p.getAge() > 50 && p.getLastName().startsWith("C");
}
});
But with Java8, using Lambdas we just have to pass the behavior as follows.
filterAndPrint(persons, p -> p.getCity().equalsIgnoreCase("London"));
filterAndPrint(persons, p -> p.getCity().equalsIgnoreCase("London") && p.getAge() > 50 && p.getLastName().startsWith("C"));
As seen above, the Lambda expression accepts one parameter of type Person and returns a boolean which matches the signature of PersonFilter.filter method. This is how the compile binds the Lambda expression to the method.
Java Inbuilt Functional Interfaces
Now that we have seen what the functional interfaces are and how to use them, it is a sheer waste of time to write our own functional interfaces with just one method. Just to use the Lambda expressions, we may end up writing several interfaces ( that has only one method) in our codebase which is unnecessary. To alleviate this pain, Java 8 has some inbuilt Functional Interfaces that we can use instead of defining our own Functional interfaces.
As you have seen above and with your own experience, the methods take parameters and either return a value or not. In the example above, the filter method takes one parameter of type Person and returns a boolean. There could be another interface with a method that takes one parameter and doesn’t return anything. So the methods can be some variation of parameters to be accepted and return type. Also as you see in the examples, Lambda expression doesn’t refer to the method name and it doesn’t care what the method names are.
Java 8 has a lot of inbuilt functional interfaces that we can use instead of writing our own. Below are few of the most used inbuilt interfaces ( we have 20 more variations)
Predicate - It has a test() method that takes one argument of type T and returns a boolean. Consumer - It has a accept() method that takes one argument of type T and doesn’t returns anything. Supplier - It has a get() method that takes no argument but returns an object of type T. Function<T,R> - It has a apply() method that takes one argument of type T and returns an object of type R.
So our previous example where we wrote our functional interface, can be rewritten as follows using one of the inbuilt Functional Interface. Personfilter.filter method accepted one parameter and returns a boolean. This is exactly what Predicate is. So below is the updated code using Predicate.
public void filterAndPrint(List<Person> persons, Predicate<Person> predicate){
for(Person p: persons){
if(predicate.test(p)){
System.out.println(p.toString());
}
}
}
// No change in the Lambda expression to invoke this method.
filterAndPrint(persons, p -> p.getCity().equalsIgnoreCase("London"));
Few more examples of using Inbuilt Functional Interfaces.
List<Person> persons = Arrays.asList(new Person("Mike","Gatting",54, "Baltimore"),
new Person("Tim","Brown",51,"London"),
new Person("Greg","Chappel",52, "London"),
new Person("Zoe","Hart",23,"Hongkong"),
new Person("Wade","Earl",97,"Texas"),
new Person("Lavaon","Hayes",11,"Mumbai"),
new Person("Lemon","Breeland",6,"Amsterdam"),
new Person("Brick","Breeland",34,"Paris"));
// Prints each person's first name. forEach accepts a Consumer
persons.forEach(p -> System.out.println("Person name is " + p.getFirstName()));
// Prints each persons first name and last name concatenated. Uses Function
Function<Person,String> f = p -> p.getFirstName()+","+p.getLastName();
persons.forEach( p -> System.out.println(f.apply(p)));
// Prints each persons first name and last name concatenated. Uses Consumer.
Consumer<Person> cons = p -> System.out.println(p.getFirstName()+","+p.getLastName());
persons.forEach(cons);
Conclusion
As you have seen, Lambda expressions are a powerful way to write concise code and to pass behaviors to methods. I hope this explanation helped you to understand the concepts of Lambda and how to use them.
Happy coding.