Here are examples of best practices for using lambda expressions in Java:
- Keep Lambdas Short and Simple:
// 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));
- Avoid Side Effects:
// 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
- 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
orsum
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 aStream
. 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 anInteger
object to its primitiveint
value. It’s necessary because themapToInt
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.
- Use Descriptive Variable Names:
// 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));
- Favor Readability over Conciseness:
// Concise but hard to read
people.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));
// More readable
people.sort(Comparator.comparingInt(Person::getAge));
- Limit Nesting:
// 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)));
- Use Lambda Expressions Judiciously:
// 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());
- Consider Alternatives:
// 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);
}
- Comment and Document:
// Complex lambda expression with a comment
people.sort(Comparator.comparingInt(Person::getAge) // Sort by age
.thenComparing(Person::getLastName)); // Then by last name
- Test and Validate:
// 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));
}
- Follow Language and Style Guidelines:
// 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.