Java Streams: Declarative Data Processing in Java 8 and Beyond

Explore Java Streams, introduced in Java 8, which allow you to process data in a declarative style similar to SQL queries. Learn how streams simplify data handling and enable efficient, functional programming in Java.



Java - Streams

Stream is a new abstract layer introduced in Java 8. With streams, you can process data in a declarative way, similar to SQL statements. For instance, consider the following SQL statement:

SQL Example

SELECT max(salary), employee_id, employee_name FROM Employee
    

The SQL expression above automatically retrieves the details of the employee with the highest salary, without requiring the developer to perform any calculations. In contrast, when using the Java collections framework, developers typically need to write loops and perform repetitive checks, which can be inefficient, especially with the availability of multi-core processors. Writing parallel processing code can be error-prone.

To address these challenges, Java 8 introduced the concept of streams, enabling developers to process data declaratively while efficiently utilizing multi-core architectures without writing specialized code.

What is a Stream in Java?

A stream represents a sequence of objects from a source and supports aggregate operations. The characteristics of a stream include:

  • Sequence of Elements: A stream provides a set of elements of a specific type in a sequential manner, computing elements on demand. It does not store elements.
  • Source: Streams can take collections, arrays, or I/O resources as input sources.
  • Aggregate Operations: Streams support operations like filter, map, limit, reduce, find, and match.
  • Pipelining: Most stream operations return a stream itself, allowing their results to be pipelined. Intermediate operations take input, process it, and return output to the target, while the collect() method serves as a terminal operation marking the end of the stream.
  • Automatic Iterations: Stream operations handle iterations internally over source elements, unlike collections, which require explicit iteration.

Generating Streams in Java

With Java 8, the Collection interface provides two methods to generate a stream:

Syntax for Stream Generation

stream() - Returns a sequential stream from the collection.
parallelStream() - Returns a parallel stream from the collection.
    

For example:

Filtering Non-Empty Strings

List strings = Arrays.asList("xyz", "", "abc", "def", "ghi", "", "jkl");
List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
    

forEach Method

The forEach method allows you to iterate over each element in the stream. Below is an example of printing 10 random numbers using forEach.

Printing Random Numbers

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
    

map Method

The map method is used to transform each element to its corresponding result. The following code prints unique squares of numbers:

Unique Squares of Numbers

List numbers = Arrays.asList(4, 3, 3, 4, 9, 4, 5);
List squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
    

filter Method

The filter method eliminates elements based on specified criteria. Here’s how to count empty strings:

Counting Empty Strings

List strings = Arrays.asList("xyz", "", "abc", "def", "ghi", "", "jkl");
int count = strings.stream().filter(string -> string.isEmpty()).count();
    

limit Method

The limit method reduces the size of the stream. Here’s an example of printing 10 random numbers:

Limiting Random Numbers

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
    

sorted Method

The sorted method sorts the stream. Below is an example of printing sorted random numbers:

Sorted Random Numbers

Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
    

Parallel Processing

The parallelStream method is an alternative to stream for parallel processing. Here’s how to count empty strings using parallelStream:

Counting Empty Strings with Parallel Stream

List strings = Arrays.asList("xyz", "", "abc", "def", "ghi", "", "jkl");
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
    

Collectors

Collectors combine the results of processing stream elements. They can return a list or a string. For instance:

Using Collectors

List strings = Arrays.asList("xyz", "", "abc", "def", "ghi", "", "jkl");
List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("Filtered List: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("Merged String: " + mergedString);
    

Statistics

Java 8 introduces statistics collectors to calculate various statistics during stream processing:

Calculating Statistics

List numbers = Arrays.asList(3, 5, 2, 8, 6, 4);
IntSummaryStatistics stats = numbers.stream().mapToInt(x -> x).summaryStatistics();
System.out.println("Highest number in List: " + stats.getMax());
System.out.println("Lowest number in List: " + stats.getMin());
System.out.println("Sum of all numbers: " + stats.getSum());
System.out.println("Average of all numbers: " + stats.getAverage());
    

Java Streams Example

Here's a complete example demonstrating the use of streams:

Complete Java Streams Example

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
public static void main(String[] args) {
    List strings = Arrays.asList("xyz", "", "abc", "def", "ghi", "", "jkl");
    
    // Filter non-empty strings
    List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
    System.out.println("Filtered List: " + filtered);
    
    // Count empty strings
    long count = strings.stream().filter(string -> string.isEmpty()).count();
    System.out.println("Count of Empty Strings: " + count);
}
}
    

Conclusion

Java Streams offer a powerful way to work with data in a functional style. They simplify complex operations and improve performance, making data processing in Java both efficient and easy to implement.