Blog Concurrency and Multithreading Java Java 8 Features

Write Better Java Code with Lambda Expressions: A Developer’s Guide to Best Practices

Here are examples of best practices for using lambda expressions in Java:

  1. Keep Lambdas Short and Simple:
Java
// Too complex for a lambda expression
Person person1 = new Person("Neelabh", "Singh", 30);
        Person person2 = new Person("Amit", "Maurya", 30);
        Person person3 = new Person("Neeraj", "Maurya", 30);
        List<Person> list = Arrays.asList(person1, person2, person3);
        
        
Collections.sort(list, (a, b) -> {
    int result = a.getFirstName().compareTo(b.getFirstName());
    if (result == 0) {
        result = a.getLastName().compareTo(b.getLastName());
    }
    return result;
});

// Better as a method reference
list.sort(Comparator.comparing(Person::getFirstName)
                     .thenComparing(Person::getLastName));
list.sort(Comparator.comparing(Person::getFirstName)
                     .thenComparing(Person::getLastName));
  1. Avoid Side Effects:
Java
// Bad: Lambda modifies a local variable
int[] sum = new int[1]; // Effectively final
list.forEach(i -> sum[0] += i); // Modifies sum, side effect

// Good: Lambda operates only on its input
int sum = list.stream().mapToInt(Integer::intValue).sum();

Explanation

Here’s why modifying a local variable within a lambda expression is generally considered bad practice:

1. Violates Functional Principles

  • State Changes: Lambda expressions align best with functional programming, where the focus is on operations transforming inputs into outputs without altering external state. Modifying external variables introduces side effects.
  • Unpredictability: Side effects make it harder to understand the code’s behavior accurately, as the result of the lambda isn’t solely determined by its input.

2. Potential Concurrency Issues

  1. Race Conditions: In multi-threaded environments, multiple threads might attempt to modify sum[0] simultaneously, leading to unpredictable results. To avoid this, you’d need complex synchronization mechanisms, undermining the conciseness of the lambda.

3. Reduced Readability

  • Implicit Changes: Someone reading the code might not immediately spot that the sum variable is being modified within the lambda. This can make the code harder to debug or reason about, especially in more complex scenarios.

4. Limits Streams API Power

  • Alternative Solutions: The Streams API provides elegant and efficient ways to achieve the same result without side effects. Using methods like reduce or sum often leads to cleaner, more functional code.

Key Point: While technically possible to modify variables within a lambda, it’s generally best to keep lambdas pure – depending only on their inputs and focusing on producing outputs.

Let’s break down the “Good” example step-by-step to understand why it’s superior.

1. list.stream()

  • Creates a Stream: This converts your list (likely a collection, such as an ArrayList) into a Stream. A stream represents a sequence of elements you can process sequentially or in parallel.

2. mapToInt(Integer::intValue)

  • Transformation: The mapToInt operation applies a function to each element of the stream.
  • Integer::intValue: This method reference is a shorthand for a lambda expression that simply unboxes an Integer object to its primitive int value. It’s necessary because the mapToInt method expects to work with primitive integers.

3. .sum()

  • Final Calculation: This terminal operation calculates the sum of all the int values in the stream after the transformation.

The beauty of this approach lies in its:

  • Declarative Style: You describe what you want to achieve (get the sum) rather than manually specifying how to iterate and accumulate values.
  • Focus on Inputs/Outputs: Each stage of the stream pipeline takes an input and produces a new output, without side effects.
  • Immutability: The original list remains unmodified. Intermediate streams represent transformed versions of the data.

  1. Use Descriptive Variable Names:
Java
// Poor variable names
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
nums.forEach(n -> System.out.println(n * n));

// Better variable names
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(number -> System.out.println(number * number));
  1. Favor Readability over Conciseness:
Java
// Concise but hard to read
people.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));

// More readable
people.sort(Comparator.comparingInt(Person::getAge));
  1. Limit Nesting:
Java
// Nested lambdas are hard to read
list.stream()
    .filter(i -> i % 2 == 0)
    .map(i -> i * i)
    .forEach(i -> System.out.println(i * (i -> i + 2)));

// Better as separate methods
list.stream()
    .filter(MyUtils::isEven)
    .map(i -> i * i)
    .forEach(i -> System.out.println(applyOperation(i)));
  1. Use Lambda Expressions Judiciously:
Java
// Lambda expression is a good fit
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(n -> n * n)
                                      .collect(Collectors.toList());

// Method reference is more readable
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> greetings = names.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());
  1. Consider Alternatives:
Java
// Lambda expression
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(n -> n * n)
                                      .collect(Collectors.toList());

// Loop is more readable for simple operations
List<Integer> squaredNumbers = new ArrayList<>();
for (int n : numbers) {
    squaredNumbers.add(n * n);
}
  1. Comment and Document:
Java
// Complex lambda expression with a comment
people.sort(Comparator.comparingInt(Person::getAge) // Sort by age
                      .thenComparing(Person::getLastName)); // Then by last name
  1. Test and Validate:
Java
// Unit test for a lambda expression
@Test
public void testSquareLambda() {
    Function<Integer, Integer> square = n -> n * n;
    assertEquals(4, square.apply(2));
    assertEquals(9, square.apply(3));
    assertEquals(4, square.apply(-2));
}
  1. Follow Language and Style Guidelines:
Java
// Java style guidelines for lambda expressions
Collections.sort(list, Comparator.comparingInt(Person::getAge) // Space after ,
                                 .thenComparing(Person::getLastName));

// Team's coding conventions
list.stream()
    .filter(n -> n % 2 == 0) // Use lambda expressions for simple operations
    .map(MyUtils::square) // Use method references for better readability
    .forEach(System.out::println);

These examples demonstrate how to apply best practices for using lambda expressions in Java, ensuring readability, maintainability, and efficiency.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over