Java is a “mature” language with traditions and rituals that can be compared to those of a religion. Just check out Reddit discussions on Java vs {{insert backend language here}}. As widespread as it is, the religion that is Java is a living and breathing faith with an active community, ever-changing and developing.
Some still recite prayers from ancient releases while others are eager to adopt new customs. One such custom, introduced with Java SE 8, is the use of Lambda expressions – a concept adopted from the realm of functional programming.
For seasoned practitioners of Java (also known as senior developers) accustomed to an object oriented approach, Lambda expressions may seem at first glance like some form of blasphemy. The OOP tradition is to execute functions only after defining them inside a class, then creating an object of the class to call the method. If it works, why fix it, right? Wrong.
Lambda expressions can make your code easier to understand, faster to write, and potentially less painstaking to maintain. If you’re not making use of them yet, perhaps now is a good time to take a leap of faith and learn how lambda expressions can make your code (and thus your life) better.
The concept of Lambda expressions was first introduced in the LISP programming language. In Java, Lambda expressions were added in Java SE 8. This was Java’s first step into functional programming and perhaps the most impactful feature added to the programming language with the Java SE 8 release.
Lambda expressions are essentially a block of code (an anonymous function) that can be passed to constructors or methods for subsequent execution. The constructor or method receives the Lambda as an argument.
Lambda expressions are primarily used to implement the abstract function of a functional interface (a class with a single abstract method only). Especially useful in collection libraries, Lambda expressions help iterate, filter and extract data from a collection. In addition, since Java Lambda expressions are treated as functions, the compiler does not create .class files for them.
In terms of implementation, Lambda expressions enable three main capabilities:
The syntax for Lambda expressions is surprisingly simple and flexible. To create a Lambda expression, just specify input parameters (if you want to – it’s not mandatory), the Lambda operator ->, then enter the expression or block of statements.
(arguments) -> {function body}
Java Lambda expressions consist of three components.
(Source: educative.io)
For example, the Lambda expression (a, b) -> {a + b}
will receive arguments a and b to return their sum. Of course, it can get a lot more complicated fairly fast.
Generally speaking, Lambda expressions make for cleaner, shorter and more readable source code with less obfuscation. How? By providing a smarter way to implement functional interfaces.
Back in the olden days before Java SE 8, when you needed to pass functionality to a method, you would use an anonymous class. This meant you would then have to define the method again to provide implementation instead of just implementing. To mitigate this issue, Java 8 introduced Lambdas. Their main objective? To actively increase the expressive power of Java as a programming language.
To better understand the benefits of Lambda expressions, it’s worth comparing them against methods in Java.
A method (or function) in Java has a few mandatory components: name, arguments (parameters), function body and return type. Lambda expressions have no names, nor do they specify the return type. Instead, the Java compiler is able to infer the return type by inspecting the function body and without explicit mention. Also, it doesn’t require the type of the method argument. Pretty smart, right?
A simple example for Lambda, would be using it instead of an anonymous Comparator. Suppose we have a Dish defined as such:
class Dish{ int id; String name; float price; public void printDish() { //... } }
Sorting a List of those Dishes can be done with an anonymous Comparator as such:
public static void anonymousComparatorSort(List<Dish> list) { Collections.sort(list, new Comparator<Dish>() { @Override public int compare(Dish p1, Dish p2) { return p1.name.compareTo(p2.name); } }); }
Which can also be done using a simple Lambda Expression:
public static void lambdaSort(List<Dish> list) { Collections.sort(list,(p1,p2)->{ return p1.name.compareTo(p2.name); }); }
Not a lot of code lines have been saved by this transition, only the definition of the anonymous class. This is the simplest of examples. The Comparator interface is already defined, but what if we have an example where the interface has yet to be defined?
Suppose instead of sorting a List, you would like to print all Dishes above a certain price. We will use the same Dish from earlier and define our search method using an interface and an anonymous class.
This will be the interface that defines the criteria of the search:
public interface DishCheck { public boolean test(Dish dish); }
And this will be the printing method that will take int a DishChecker
and print all dishes that pass the check.
public static void printDishes(List<Dish> list, DishCheck tester) { for (Dish dish : list) { if (tester.test(dish)) { dish.printDish(); } } }
Using the above method will require implementing an anonymous class when called:
public static void main(String[] args) { List<Dish> list = new ArrayList<Dish>(); //Populate the list //Print dishes with price equal to or greater than 4.5 printDishes(list, new DishCheck() { @Override public boolean test(Dish dish) { return dish.price >= 4.5; } }); }
It is possible to bypass the anonymous class with Lambda the same way we did earlier with the Comparator:
public static void main(String[] args) { List<Dish> list = new ArrayList<Dish>(); //Populate the list //Print dishes with price equal to or greater than 4.5 printDishes(list, (d1) -> { return d1.price >= 4.5f; }); }
Or we could completely bypass the printDishes
method and write it directly into the Lambda using the collections forEach
method:
public static void main(String[] args) { List<Dish> list = new ArrayList<Dish>(); //Populate the list //Print dishes with price equal to or greater than 4.5 list.forEach(d1 -> {if (d1.price >= 4.5) d1.printDish();}); }
This is done by allowing the Lambda expression to implement the Consumer Interface, a powerful tool that can allow an action to be performed on an item in the list.
It is also possible to write a more elaborate method that will allow using both a tester and a consumer on the list. Instead of the printDishes
method, consider the following:
public static void takeActionOnDishes( List<Dish> list, DishCheck tester, Consumer<Dish> action) { for (Dish dish : list) { if (tester.test(dish)) { action.accept(dish); } } }
Now when you call the method, you not only specify the criteria for the action, but also which action will be taken:
public static void main(String[] args) { List<Dish> list = new ArrayList<Dish>(); //Populate the list //Print dishes with price equal to or greater than 4.5 takeActionOnDishes( list, (d1) -> {return d1.price >= 4.5f;}, (d1) -> {d1.printDish();} ); }
Readable and maintainable code makes for happy developers and dev team leaders. When it comes to Java, Lambda expressions are one of the tools added to the language to enable just that by borrowing functionality from functional programming languages.