DEV Community

Cover image for Mastering Lambda Expressions in Java 8: A Comprehensive Guide
Dhanush
Dhanush

Posted on

Mastering Lambda Expressions in Java 8: A Comprehensive Guide

Introduction to Lambda Expressions in Java

Despite the many new Java versions released, Java 8 remains one of the most widely adopted versions due to its powerful and transformative features. Lambda Expressions, introduced in Java 8, are especially popular because they make Java more concise and efficient by enabling functional programming. This feature allows developers to replace verbose anonymous inner classes with streamlined syntax, making code more readable and maintainable.

In this guide, we’ll explore how Lambda Expressions simplify code, enhance data processing in collections, and enable Java developers to write modern, performant applications.

Simplify

Understanding Lambda Expressions: The Basics

At its core, a Lambda Expression is a way to represent a single abstract method of a functional interface in a much simpler syntax. This feature aligns with the Single Abstract Method (SAM) concept, which allows interfaces with a single unimplemented method to be treated as Lambda-compatible.

Lambda Syntax:
A Lambda Expression typically consists of three parts:

  1. Parameter List – The input values required for the function.
  2. Arrow Token (->) – Separates the parameters from the function body.
  3. Body – The code block that executes the operation, which can be an expression or a code block enclosed in curly braces.
(parameters) -> expression
(parameters) -> { statements; }
Enter fullscreen mode Exit fullscreen mode

Examples of Lambda Expressions:

  • A basic Lambda Expression that takes two integers and returns their sum:
  (int x, int y) -> x + y
Enter fullscreen mode Exit fullscreen mode
  • A Lambda Expression that takes a single string and prints it:
  (String message) -> System.out.println(message)
Enter fullscreen mode Exit fullscreen mode

The syntax of Lambda Expressions in Java is both flexible and intuitive, allowing developers to choose between a concise, one-liner format or a more detailed block when multiple lines of code are needed.


How Lambda Expressions Simplify Java Code

Before Java 8, implementing interfaces like Runnable or Comparator required anonymous inner classes. Lambda Expressions streamline this process, replacing boilerplate code with a more functional style. Here’s a comparison of how a Lambda Expression simplifies common tasks:

Example 1: Using Lambda Expressions with Runnable

Consider a simple Runnable implementation. Using an anonymous inner class would look like this:

Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello world one!");
    }
};
Enter fullscreen mode Exit fullscreen mode

With Lambda Expressions, this code can be simplified to:

Runnable r2 = () -> System.out.println("Hello world two!");
Enter fullscreen mode Exit fullscreen mode

Lambda Expressions with Common Functional Interfaces

Java 8 introduces a set of predefined functional interfaces in the java.util.function package. These interfaces, such as Predicate, Function, Consumer, and Supplier, provide a foundation for Lambda Expressions, allowing developers to leverage functional programming principles.

  1. Predicate – Represents a condition, returning true or false based on the input.
  2. Function – Accepts one argument and produces a result.
  3. Consumer – Performs an action on a single input without returning a result.
  4. Supplier – Provides an output without taking any input.

By using these interfaces with Lambda Expressions, Java developers can perform operations that are not only concise but also highly reusable.


Real-World Use Cases for Lambda Expressions

To see Lambda Expressions in action, let’s go through a few scenarios that showcase how they can replace verbose syntax, streamline common operations, and enhance readability.


Runnable Lambda Example

The Runnable interface in Java represents a task that can be executed by a thread. The class must define a method of no arguments called run. Here’s how a Lambda Expression simplifies a Runnable implementation.

Runnable with Anonymous Inner Class:

Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running with anonymous inner class");
    }
};
r1.run();
Enter fullscreen mode Exit fullscreen mode

Runnable with Lambda Expression:

Runnable r2 = () -> System.out.println("Running with Lambda Expression");
r2.run();
Enter fullscreen mode Exit fullscreen mode

Using Lambda Expressions reduces five lines of code to one, highlighting how they can simplify Java code.


Comparator Lambda Example

The Comparator interface is often used to define sorting logic for collections. With Lambda Expressions, defining custom sorting criteria becomes more concise and intuitive.

Comparator for Sorting a List of People by Surname:

List<Person> personList = Person.createShortList();
Collections.sort(personList, (p1, p2) -> p1.getSurName().compareTo(p2.getSurName()));
Enter fullscreen mode Exit fullscreen mode

Lambda Expressions make it easy to switch between sorting orders by changing the comparison logic, e.g., for descending order:

Collections.sort(personList, (p1, p2) -> p2.getSurName().compareTo(p1.getSurName()));
Enter fullscreen mode Exit fullscreen mode

This approach is especially useful in applications that require dynamic sorting, allowing developers to easily swap sorting criteria based on user input or other conditions.


Using Lambda Expressions with Event Listeners

In GUI programming, event listeners are commonly used to handle user actions. Traditionally, anonymous inner classes were required, resulting in lengthy code. Lambda Expressions, however, offer a cleaner way to implement these listeners.

ActionListener with Anonymous Inner Class:

testButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent ae) {
        System.out.println("Button clicked!");
    }
});
Enter fullscreen mode Exit fullscreen mode

ActionListener with Lambda Expression:

testButton.addActionListener(e -> System.out.println("Button clicked with Lambda!"));
Enter fullscreen mode Exit fullscreen mode

Lambda Expressions enable developers to directly implement ActionListener in a single line, enhancing readability and reducing boilerplate code.


Advanced Use Case: Filtering with Predicates

A common scenario in software applications is filtering data based on multiple criteria. In Java, this can be handled effectively by combining Lambda Expressions with the Predicate interface, allowing dynamic filtering of collections.

Consider a list of Person objects, where we want to filter based on different criteria, such as age and gender.

Defining a Predicate-based SearchCriteria Class:

public class SearchCriteria {
    private final Map<String, Predicate<Person>> criteriaMap = new HashMap<>();

    private SearchCriteria() {
        initializeCriteria();
    }

    private void initializeCriteria() {
        criteriaMap.put("allDrivers", p -> p.getAge() >= 16);
        criteriaMap.put("allDraftees", p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE);
        criteriaMap.put("allPilots", p -> p.getAge() >= 23 && p.getAge() <= 65);
    }

    public Predicate<Person> getCriteria(String criterion) {
        return criteriaMap.get(criterion);
    }

    public static SearchCriteria getInstance() {
        return new SearchCriteria();
    }
}
Enter fullscreen mode Exit fullscreen mode

The SearchCriteria class encapsulates common conditions for filtering lists, allowing flexibility in applying different filters to a single collection.

Using the Criteria in Filtering:

List<Person> people = Person.createShortList();
SearchCriteria search = SearchCriteria.getInstance();

List<Person> drivers = people.stream()
                             .filter(search.getCriteria("allDrivers"))
                             .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

This approach eliminates the need for multiple for loops, providing a cleaner, reusable, and more maintainable solution.

checkmate

Stream API and Collections with Lambda Expressions

Java 8’s Stream API revolutionizes how collections are processed, particularly with Lambda Expressions that enable efficient data filtering, transformation, and aggregation. Streams allow for lazy processing, where data is processed only when required, improving performance for large datasets.

Looping with forEach

The forEach method in the Stream API provides an alternative to the traditional for loop, enabling Lambda Expressions to be applied to each element in a collection. Here’s an example that iterates through a list of Person objects.

List<Person> people = Person.createShortList();
people.forEach(person -> System.out.println(person.getGivenName()));
Enter fullscreen mode Exit fullscreen mode

Using Method References:
For cases where an existing method can be reused, Java allows method references, a shorthand that enhances readability.

people.forEach(Person::printWesternName);
Enter fullscreen mode Exit fullscreen mode

Filtering, Mapping, and Collecting

The Stream API allows operations to be chained together, enabling developers to filter, map, and collect results in a single statement.

Example: Filtering and Collecting:

List<Person> draftees = people.stream()
                              .filter(search.getCriteria("allDraftees"))
                              .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

This code filters only male persons aged 18 to 25, using criteria defined in the SearchCriteria class.

Mapping and Transformation with map:
The map method transforms each element in a collection, such as by extracting or modifying properties.

List<String> names = people.stream()
                           .map(Person::getGivenName)
                           .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

Using map for Calculations:
The mapToInt and mapToDouble methods are helpful for numeric calculations.

double averageAge = people.stream()
                          .filter(search.getCriteria("allPilots"))


                          .mapToDouble(Person::getAge)
                          .average()
                          .orElse(0.0);
Enter fullscreen mode Exit fullscreen mode

Understanding Laziness and Eagerness in Streams

Streams support lazy and eager operations, with lazy operations (such as filter) only applying when needed. This laziness optimizes performance by processing only necessary elements.

  1. Lazy Operations: Applied only when a terminal operation (like collect) is reached.
  2. Eager Operations: Executed immediately on all elements, commonly used for aggregations.

Example of Lazy Evaluation:

people.stream()
      .filter(p -> p.getAge() > 30)
      .map(Person::getSurName)
      .forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

Only people with age greater than 30 are processed, and surnames are printed, demonstrating lazy filtering.


Parallel Processing with Streams

Java’s parallelStream method distributes tasks across multiple threads, offering significant performance gains for large data sets.

Example of Parallel Stream:

List<Person> people = Person.createShortList();
long count = people.parallelStream()
                   .filter(p -> p.getAge() >= 18)
                   .count();
Enter fullscreen mode Exit fullscreen mode

Parallel processing divides the workload, making operations on collections faster for computationally intensive tasks.

Mutation and Collecting Results

Since streams are inherently immutable, results need to be collected to retain them. The collect method provides a way to aggregate and retain the results of a stream operation.

Example:

List<Person> pilots = people.stream()
                            .filter(search.getCriteria("allPilots"))
                            .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

Here, filtered results are stored in a list for further processing, allowing developers to manage complex data flows in a structured way.

hack

Conclusion: The Power of Lambda Expressions in Java 8

Java 8’s Lambda Expressions, paired with the Stream API, represent a major shift toward functional programming, making code more concise, expressive, and maintainable. By replacing anonymous inner classes, enhancing collection processing, and supporting parallel operations, Lambda Expressions have become a cornerstone for writing modern Java applications.

Any corrections or additions to this post are welcome.

Thanks for reading!

                             Happy Coding😎
Enter fullscreen mode Exit fullscreen mode

Top comments (0)