Lambda Expressions in Java

Introduction

Lambda expressions were introduced in Java 8, released in March 2014, to support functional programming by enabling the creation of anonymous functions. They simplify the implementation of interfaces with a single abstract method, known as functional interfaces.

Advantages

  1. Conciseness: Lambdas reduces boilerplate code, making the code shorter and more readable.

  2. Readability: They make the code easier to understand by removing the need for anonymous class implementations.

  3. Ease of Use: Simplifies the use of APIs and libraries that rely on callbacks and event handlers.

  4. Parallel Processing: Enhances the ability to write parallel and concurrent code using the Stream API.

Disadvantages

  1. Debugging Difficulty: Lambdas can make stack traces harder to read, complicating debugging.

  2. Overhead: Initial learning curve for developers new to the concept.

  3. Limited Expressiveness: Not suitable for all situations, especially where complex logic is involved.

Simple Examples

  1. Example 1: Simple Lambda Expression

     // Traditional way using an anonymous class
     Runnable r1 = new Runnable() {
         @Override
         public void run() {
             System.out.println("Hello World");
         }
     };
    
     // Using lambda expression
     Runnable r2 = () -> System.out.println("Hello World");
    
  2. Example 2: Lambda with Parameters

     // Using lambda to define a Comparator
     Comparator<Integer> comparator = (a, b) -> a.compareTo(b);
    
     // Using the comparator
     List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
     Collections.sort(list, comparator);
    
  3. Example 3: Lambda in a Stream

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
     names.forEach(name -> System.out.println(name));
    
  4. Example 4: Filtering a List

     // Traditional way using an anonymous class
     List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
     List<String> filteredNames = new ArrayList<>();
     for (String name : names) {
         if (name.startsWith("A")) {
             filteredNames.add(name);
         }
     }
     System.out.println(filteredNames); // Output: [Alice]
    
     // Using lambda expression
     List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
     List<String> filteredNames = names.stream()
                                       .filter(name -> name.startsWith("A"))
                                       .collect(Collectors.toList());
     System.out.println(filteredNames); // Output: [Alice]
    

Complex Examples

  1. Example 1: Filtering and Mapping with Streams

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
     List<String> result = names.stream()
                                .filter(name -> name.startsWith("A"))
                                .map(String::toUpperCase)
                                .collect(Collectors.toList());
     System.out.println(result); // Output: [ALICE]
    
  2. Example 2: Summing Values with Streams

     List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
     int sum = numbers.stream()
                      .reduce(0, (a, b) -> a + b);
     System.out.println(sum); // Output: 15
    
  3. Example 3: Grouping and Counting with Streams

     List<String> items = Arrays.asList("apple", "banana", "apple", "orange", "banana", "banana");
     Map<String, Long> result = items.stream()
                                     .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
     System.out.println(result); // Output: {banana=3, orange=1, apple=2}
    

Examples Without Lambda

  1. Example 1: Sorting Without Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
     Collections.sort(names, new Comparator<String>() {
         @Override
         public int compare(String o1, String o2) {
             return o1.compareTo(o2);
         }
     });
     System.out.println(names); // Output: [Alice, Bob, Charlie]
    
  2. Example 2: Filtering Without Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
     List<String> result = new ArrayList<>();
     for (String name : names) {
         if (name.startsWith("A")) {
             result.add(name);
         }
     }
     System.out.println(result); // Output: [Alice]
    
  3. Example 3: Iterating Without Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
     for (String name : names) {
         System.out.println(name);
     }
     // Output:
     // Alice
     // Bob
     // Charlie
    

Examples Rewritten with Lambda

Now, let's rewrite the examples above but using lambda...

  1. Example 1: Sorting With Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
     // Explanation:
     // The traditional way used an anonymous inner class to define the Comparator.
     // With a lambda expression, we can write this in a more concise form.
     // The lambda `(o1, o2) -> o1.compareTo(o2)` directly provides the comparison logic.
     Collections.sort(names, (o1, o2) -> o1.compareTo(o2));
    
     System.out.println(names); // Output: [Alice, Bob, Charlie]
    
  2. Example 2: Filtering With Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
     // Explanation:
     // The traditional way involved iterating over the list and adding elements that match the condition to a new list.
     // Using streams and lambda expressions, we can achieve this in a more declarative way.
     // The `filter` method applies the lambda `name -> name.startsWith("A")` to each element in the stream.
     // The `collect(Collectors.toList())` method gathers the filtered elements into a new list.
     List<String> result = names.stream()
                                .filter(name -> name.startsWith("A"))
                                .collect(Collectors.toList());
    
     System.out.println(result); // Output: [Alice]
    
  3. Example 3: Iterating With Lambda

     List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
     // Explanation:
     // The traditional way used a for-each loop to print each element in the list.
     // With a lambda expression, we can use the `forEach` method to achieve the same result.
     // The lambda `name -> System.out.println(name)` is applied to each element in the list.
     names.forEach(name -> System.out.println(name));
    
     // Output:
     // Alice
     // Bob
     // Charlie
    

Conclusion

Lambda expressions in Java bring the power of functional programming to the language, making code more concise, readable, and expressive. They are particularly useful when working with collections and streams, providing significant advantages in terms of code clarity and simplicity. However, they come with a learning curve and can complicate debugging. By understanding and using lambda expressions effectively, developers can greatly enhance their productivity and the quality of their code.

0
Subscribe to my newsletter

Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

André Felipe Costa Bento
André Felipe Costa Bento

Fullstack Software Engineer.