Lambda expressions were introduced in Java 8 and they became the talk of the town as soon as they arrived.
Java has evolved a lot with time. It has incorporated new ideas and programming paradigms as and when necessary. That is the main reasons why It is still the most used language worldwide.
Functional programming was on the rise when Lambda expressions were introduced in Java 8.
Java embraced functional programming by introducing several new features in Java 8 like Lambda Expressions
, Stream API
, Optional
etc.
In this article, you’ll learn what lambda expression is, how it works under the hood, and how to use it effectively in your programs.
The need for lambda expressions
Java is a pure object oriented programming language. Everything in Java is an Object with the exception of primitive types.
You can’t define top level functions (functions that don’t belong to a class) in Java. You can’t pass a function as an argument, or return a function from another function.
So, what’s the alternative?
Before lambda expressions were introduced, Developers used to use Anonymous class syntax for passing functionality to other methods or constructors.
Let’s see an example of anonymous class syntax. Consider the following Employee class -
class Employee {
private String name;
private int age;
// Constructor, Getters, Setters (Omitted for brevity)
}
Now, To sort a list of employees, we usually pass a custom comparator to the List.sort()
method as described in the following example -
List<Employee> employees = Arrays.asList(new Employee("Foo", 21),
new Employee("Bar", 25));
// Sort employees based on their age by passing an anonymous comparator.
employees.sort(new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return e1.getAge() - e2.getAge();
}
});
In the above example, we wanted to pass a single compare()
functionality to the sort()
method for comparing two Employees.
For doing this, we had to create an anonymous comparator object with the implementation of the compare()
function, and pass it in the sort()
method.
Consider another example of an anonymous Runnable -
// Create a thread by passing an Anonymous Runnable.
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
// Code to be executed inside the thread;
}
});
In this example, we wanted to create a thread and pass a function that needs to be executed by the thread.
For doing this, we had to create an anonymous Runnable object with the implementation of the run()
method and pass the object to the Thread()
constructor.
You’re getting the point right? Since you can’t pass functions directly as method arguments, You need to write all that boilerplate code all the time.
I agree that anonymous class syntax is more compact than defining a named class, instantiating it and then passing the instance as an argument. It is still too much for classes with only one method.
Can we do better? Is there a simpler way of passing a single functionality to other methods?
Well, Enter Lambda Expressions!
Introduction to Lambda Expression
Lambda expression allows you to pass functionality to other methods in a less verbose and more readable way.
Here is how you would write the earlier employee comparator example using lambda expression -
employees.sort((Employee e1, Employee e2) -> {
return e1.getAge() - e2.getAge();
});
If the method body consists of a single line, then you can omit the curly braces and the return keyword as well -
employees.sort((Employee e1, Employee e2) -> e1.getAge() - e2.getAge());
Moreover, Since Java is aware about the types of arguments from the surrounding context, you can omit the type declarations as well -
employees.sort((e1, e2) -> e1.getAge() - e2.getAge());
Whoa! Compare that with the earlier implementation without lambda expression. This is so concise, readable, and to the point.
What about the Runnable
example? Well, Here is how you would write that using lambda expression -
Thread myThread = new Thread(() -> {
// Code to be executed inside the thread
});
Syntax and Examples of Lambda Expressions
Lambda expressions in Java has the following syntax -
(type arg1, type arg2, type arg3, ...) -> (body)
Note that, the type declaration can be omitted from the arguments because the compiler can infer the types of arguments from the surrounding context -
(arg1, arg2, arg3, ...) -> (body)
Here are some examples of lambda expressions -
// Accepts no arguments and returns void
() -> System.out.println("Hello, World!");
// Accepts two int arguments and returns int
(int a, int b) -> a+b;
// Accepts an Integer and returns boolean
(Integer n) -> {
// (Checks if the number is prime or not)
if (n <= 1) return false;
for (int i=2; i <= Math.sqrt(n); i++)
if (n%i == 0)
return false;
return true;
};
Lambda Expressions Under the Hood
Introducing Functional Interfaces
Contrary to other functional programming languages, lambda expressions in Java do not correspond to functions.
Lambda expressions in Java are instances of Functional Interfaces. A functional interface is an interface that contains exactly one abstract method.
For example, Runnable
is a functional interface because it contains exactly one abstract method run()
. Similarly, Comparator
is a functional interface with a single abstract method compare()
.
Did you know? You can also define non-abstract methods inside an interface with the help of Java 8’s default keyword. Since default methods are not abstract, a functional interface can have multiple default methods.
Although, any interface with a single abstract method can be used as a lambda expression. To make sure that the interface meets the requirements of a functional interface, you should add a @FunctionalInterface
annotation like so -
@FunctionalInterface
interface MyFunctionalInterface {
void test();
}
The compiler throws an error if an interface annotated with @FunctionalInterface
annotation does not meet the requirements of a functional interface.
Java 8 comes with a bunch of built-in functional interfaces. All of them are defined in the java.util.function
package. Check out the Official Java Doc for more information.
Understanding the relation between lambda expressions and functional interfaces
Every lambda expression in Java is internally mapped to a functional interface. The functional interface to which a lambda expression will be mapped is determined by the compiler from its surrounding context at compile time.
Consider the following lambda expression for example -
// A lambda expression that accepts no arguments and returns void
() -> System.out.println("Hello, World!")
It can be mapped to any functional interface whose abstract method takes no arguments and returns void.
For example, It can be mapped to a Runnable
interface because a Runnable contains a single abstract method run()
that takes no arguments and returns void -
Runnable myRunnable = () -> System.out.println("Hello, World!");
Since our lambda expression maps to a Runnable
, we can use it in any context where an instance of Runnable
is required. For example, we can use it in Thread(Runnable target)
constructor as we did in our earlier example -
Thread myThread = new Thread(() -> System.out.println("Hello, World!"));
Let’s consider another lambda expression -
// A lambda expression that accepts a single argument and returns void
(value) -> System.out.println(value)
This lambda expression can be mapped to any functional interface whose abstract method takes a single argument and returns void.
There are many such built-in functional interfaces in Java to which the above lambda expression can be mapped -
IntConsumer myIntConsumer = (value) -> System.out.println(value);
LongConsumer myLongConsumer = (value) -> System.out.println(value);
DoubleConsumer myDoubleConsumer = (value) -> System.out.println(value);
Consumer<String> myStringConsumer = (value) -> System.out.println(value);
All of the above functional interfaces are defined in java.util.function
package.
Also note that - the type of the argument value
is inferred from the context at compile time. It’s type will be int
when it’s used with IntConsumer
, long
when it’s used with LongConsumer
and so on.
Since, this lambda expression can be mapped to all of the above functional interfaces, we can use it in any context where an instance of any of the above functional interfaces is required.
Conclusion
Lambda expression was one of the key features of Java 8. It was the first step towards functional programming in Java.
In this article, I’ve tried to explain the need for lambda expressions, how to use it in your programs and how it fits in the Java’s type System.
I hope this article was helpful to you. Thank you for reading folks. See you in the next post!