Efficient Java Lambdas and Streams

Streamlining your Java Code with Lambdas and Streams

Dharmesh Rathod
Simform Engineering
5 min readJul 5, 2023

--

Streamlining Your Java Code with Lambdas and Streams

Java Lambdas and Streams are powerful features introduced in Java 8 that greatly enhance the functionality and expressiveness of the language. Together, They enable developers to write concise, functional-style code for processing collections of data.

This blog aims to provide a comprehensive introduction to Java Lambdas and Streams. It covers the fundamental concepts, syntax, and usage of Lambdas and Streams in Java programming.

What is Lambda?

In Java, a lambda expression is a concise syntax used to represent an anonymous function, typically an interface with a single abstract method. Introduced in Java 8, lambda expressions facilitate functional programming and improve language expressiveness.

The syntax typically includes parameters, an arrow token, and a body, following a general format.

(parameters) -> { body }

Parameters: These are the inputs to the lambda expression. They can be optional if the lambda expression doesn’t require any inputs.

For example, the below syntax represents a lambda expression with no parameters.

() -> { System.out.println("Hello!"); }

Arrow Token: The arrow token -> separates the parameters from the body of the lambda expression. It indicates the transition from input to output.

Body: This part contains the code to be executed when the lambda expression is invoked. It can be a single expression or a block of code enclosed in curly braces {}. For instance, (x, y) -> x + y is a lambda expression that takes two parameters (x and y) and returns their sum.

Lambda expressions are commonly utilized with functional interfaces, which have a single abstract method. They enable passing behavior as an argument to methods, resulting in more concise and expressive code. Here’s an example of using a lambda expression with the Comparator functional interface:

List<String> names = Arrays.asList("John", "Alice", "Bob");
Collections.sort(names, (a, b) -> a.compareTo(b));

In this example, the lambda expression (a, b) -> a.compareTo(b) is used as the second argument for the Collections.sort method. It represents a comparison function that compares two strings (a and b) using their natural order.

Lambda expressions offer a powerful tool for functional programming in Java, allowing you to write code that is both concise and readable by expressing behavior in a compact form.

What is Streaming?

In Java, a Stream is a sequence of elements that can be processed in a declarative and functional manner. It enables performing operations on data collection, including filtering, mapping, sorting, and reducing, with conciseness and efficiency. Streams were introduced in Java 8 as a new abstraction for manipulating arrays and collections.

Here are some key characteristics of Java Streams:

A Sequence of Elements: A Stream represents a sequence of elements. It can be created from various data sources, including collections, arrays, I/O channels, or even generator functions.

Pipelining: Streams support a method chaining technique called pipelining. You can apply multiple operations on a Stream in a fluent and compact manner, where the output of one operation becomes the input for the next operation.

Lazy Evaluation: Stream operations are typically evaluated lazily. This means that intermediate operations (e.g., filtering or mapping) are not executed immediately but only when a terminal operation (e.g., forEach, collect, or reduce) is invoked on the Stream. Lazy evaluation allows for optimization and avoids unnecessary computations.

Immutable Data: Streams themselves are immutable. When you perform an operation on a Stream, it doesn’t modify the original data source. Instead, it produces a new Stream with the desired transformations.

Internal Iteration: Unlike external iteration, such as using a for loop, Streams employ internal iteration. With Streams, there’s no need to explicitly write looping constructs. Instead, you specify the desired operations on the elements, and the Stream internally handles the iteration process.

Here’s an example that demonstrates the usage of Streams:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * 2)
.sum();
System.out.println(sum); // Output: 60

In this example, we create a Stream from a List of numbers. We then apply a series of operations on the Stream: filtering out odd numbers, mapping each even number to its double, and calculating the sum of the resulting elements. The result is printed as the output.

Java Streams offer a robust and expressive approach to handling data collection. They advocate for a functional and declarative programming style, resulting in more readable and concise code. Moreover, they frequently provide enhanced performance by utilizing internal optimizations like parallel execution.

Filtering and Collecting:

List<String> fruits = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
List<String> filteredFruits = fruits.stream()
.filter(fruit -> fruit.startsWith("b"))
.collect(Collectors.toList());
System.out.println(filteredFruits); // Output: [banana]

In this example, we create a Stream from a list of fruits. We apply a filter operation to select only the fruits that start with the letter “b”. Finally, we collect the filtered fruits into a new list using the collect method.

Mapping:

List<String> names = Arrays.asList("John", "Alice", "Bob");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); // Output: [JOHN, ALICE, BOB]

In this example, we create a Stream from a list of names. We use the map operation to transform each name to its uppercase version using a method reference (String::toUpperCase). The resulting uppercase names are then collected into a new list.

Reducing:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 15

In this example, we create a Stream from a list of numbers. We use the reduce operation to calculate the sum of the numbers by accumulating them using the Integer::sum method reference. The initial value of the sum is 0, and the resulting sum is printed as the output.

These examples showcase some common operations performed with Java Streams, including filtering, mapping, and reducing. Remember that Streams offer a wide range of operations, including sorting, distinct, flatMap, and more, allowing you to manipulate and process data in a flexible and efficient manner.

Conclusion

They compactly express behavior and process data, enhancing code quality and maintainability. Leveraging them enables a functional programming approach with optimized performance.

Overall, they are valuable tools for efficient data manipulation and expressive behavior in Java.

Follow Simform Engineering blogs for more such insights.

--

--