Exploring Lambda Expressions in Java: A Comprehensive Guide
Written on
My articles are accessible to everyone; those who are not members can view the complete article by clicking this link.
1. What is Lambda?
In Java, we can assign a "value" to a variable and perform operations with it. For instance:
Integer a = 1;
String s = "Hello";
System.out.println(s + a);
But what if you want to assign a "block of code" to a Java variable? For example, consider assigning the following code block to a variable named codeBlock:
Before Java 8, this was not feasible. However, with Java 8's Lambda feature, this can now be accomplished. The expression looks like this:
codeBlock = public void doSomething(String s) {
System.out.println(s);}
This syntax isn't very concise, so we can streamline the assignment by eliminating unnecessary declarations.
Now, we have elegantly assigned a "block of code" to a variable. This "block of code" or "function assigned to a variable" is referred to as a Lambda expression.
A question arises: what type should the variable codeBlock be? In Java 8, all Lambda types are interfaces, and the Lambda expression itself must implement this interface. Understanding that a Lambda expression represents an interface implementation is crucial. To clarify, we can add a type to codeBlock:
An interface that contains only one method is known as a "functional interface." To prevent others from adding additional methods to this interface, which would make it a "non-functional interface," you can annotate it with @FunctionalInterface.
This allows us to create a complete Lambda expression declaration.
2. What is the Purpose of Lambda Expressions?
The primary advantage of Lambda expressions is their ability to make code remarkably concise. Let's compare Lambda expressions with traditional Java implementations of the same interface:
These two methods are functionally equivalent, but it's clear that the Java 8 approach is more elegant and concise. Moreover, since Lambdas can be directly assigned to variables, they can also be passed as parameters to functions, while traditional Java requires explicit interface implementations:
In many scenarios, an interface implementation might only be needed once. Traditional Java 7 requires the definition of an interface that could clutter the environment. Conversely, Java 8's Lambda maintains a cleaner approach.
Lambda integrates with FunctionalInterface libraries, forEach, stream(), method references, and other new features to enhance code clarity.
Let's go through an example. Assume we have the Student class defined along with a List<Student>:
@Getter
@AllArgsConstructor
public static class Student {
private String name;
private Integer age;
}
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("Zeka", 18)
);
Now, if we want to print the names of all students aged 18, the traditional Lambda approach would require defining two functional interfaces and a static method to handle the logic:
@FunctionalInterface
interface AgeMatcher {
boolean match(Student student);}
@FunctionalInterface
interface Executor {
boolean execute(Student student);}
public static void matchAndExecute(List<Student> students, AgeMatcher matcher, Executor executor) {
for (Student student : students) {
if (matcher.match(student)) {
executor.execute(student);}
}
}
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("Zeka", 18)
);
matchAndExecute(students,
s -> s.getAge() == 18,
s -> System.out.println(s.getName()));
}
This code is relatively concise, but can we make it even more succinct? Absolutely! Java 8 provides a functional interface package that includes numerous functional interfaces (java.util.function).
Thus, we can use Predicate<T> and Consumer<T> instead of defining AgeMatcher and Executor.
Step 1: Simplify — Utilize functional interface packages:
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
for (Student student : students) {
if (predicate.test(student)) {
consumer.accept(student);}
}
}
The for-each loop in matchAndExecute can be replaced with Iterable's built-in forEach() method, which accepts a Consumer<T> parameter.
Step 2: Simplify — Replace the for-each loop with Iterable.forEach():
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
students.forEach(s -> {
if (predicate.test(s)) {
consumer.accept(s);}
});
}
Since matchAndExecute is merely performing operations on the List, we can eliminate it and directly use the stream() functionality. Many methods in stream() accept Predicate<T> and Consumer<T>.
Step 3: Simplify — Use stream() instead of static functions:
students.stream()
.filter(s -> s.getAge() == 18)
.forEach(s -> System.out.println(s.getName()));
This approach is significantly more concise than the original Lambda method. If we want to print all the details of a student instead, we can further simplify using method references.
Step 4: Simplify — Use method references instead of Lambda expressions in forEach:
students.stream()
.filter(s -> s.getAge() == 18)
.map(Student::getName)
.forEach(System.out::println);
This represents the most concise version I can offer.
There are still many aspects of Lambdas in Java to explore, such as leveraging their characteristics for parallel processing. This overview serves as a foundational introduction. Numerous tutorials on Lambda are available online—delve into them and practice. With time, your skills will undoubtedly improve.
Column
Explore more at the link below:
<div class="link-block">
<div>
<div>
<h2>Java Lambda And Stream</h2>
<div>
<h3>Explaining Java Lambda and Stream with Rich Examples</h3></div>
<div>
<p>medium.com</p></div>
</div>
</div>
</div>
Finally, if you found this article helpful, please give a clap and follow, thank you! (????)
I’m Dylan, and I look forward to progressing together with you! (????)