Comprehensive Guide to Java Streams

The release of Java 8 introduced a myriad of new features. One of them was Java streams. It is revolutionizing data processing with its functional and declarative approach. It enables developers to efficiently perform both sequential and parallel aggregate operations on a sequence of objects. 

Java streams enable top Java development companies to deliver highly efficient applications with concise and expressive code. Streams are an integral aspect of Java programming and offer a multitude of benefits when used right. 

This detailed guide helps you make the most out of the processing capabilities of Java streams. 

1. Overview of Stream in Java

A stream is a method for processing data in a sequence. Imagine having a list of elements upon which you want to perform operations. Stream is a technique from Java that enables you to implement various operations like mapping, sorting, and filtering for handling collections of data efficiently. 

Stream isn’t a traditional data structure but it offers a high-level declarative API for working with various data structures like I/O channels, arrays, and collections. It functions like an assembly line, allowing data to pass through different stages to get completely processed. 

However, the stream is designed to be lazy, which means it would only perform essential operations. It can’t modify your data but helps make your code clean and concise. It’s important to note that every stream can be used only once. After that, you need to create a new one.

​2. Advantages of Java Streams

​Advantages of Java Streams

Stream is popular for the plethora of benefits it offers during Java development. Some benefits of using streams in Java are discussed below: 

2.1 Lazy Evaluation

Stream execution only processes intermediate operations when a terminal operation is invoked. This feature is called lazy evaluation. It helps improve efficiency and performance by avoiding unnecessary computations. Lazy evaluation also leads to reduced memory consumption which is helpful when handling large operations or datasets. 

2.2 Declarative Programming

Developers can now focus on achieving desired outcomes without worrying about implementation details. With a declarative style, they can use the high-level, expressive syntax of streams to perform complex data manipulations and transformations. This approach eliminates the need to follow a step-by-step procedure for data processing anymore.

2.3 Parallel Processing

One of the biggest advantages of using streams is the ability to perform parallel processing, thanks to its multi-core architecture. This helps enhance the performance of Java apps when working with large sets of data. As the name parallel processing suggests, streams would divide the data into multiple parts and then process them concurrently. 

Further Reading on: Pros and Cons of Java

3. How to Create Java Stream?

There are many methods for creating a Java stream. This section explores a few of those techniques: 

3.1 Getting a Stream From a Collection

Here are two options for obtaining a stream from a collection. We can either use .stream() or .parallelStream(). 

Example: Parallel Stream

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Stream<Integer> paralletStream1 = list.parallelStream();
paralletStream1.forEach(System.out::println);
Example : .stream()
List<Integer> list = new ArrayList(Arrays.asList(1,2,3,4,5));
boolean containSeven = list.stream().anyMatch(v -> v== 7);

3.2 Creating a Stream From Arrays

Unlike collections, there are no convenient methods in arrays. To work with arrays more efficiently, we can use the static method stream() from the arrays class to create a stream. Let’s see how you can use an array of strings to obtain a stream of type String.

String[] names = {"Sakshi", "Aditi", "Suraj"};
Stream<String> stream = Arrays.stream(names);

Example:  1

String[] array = {"A", "B", "C"};
Stream<String> stream2 = Stream.of(array);

Example: 2

int[] numbers = {3,6,9,12,15,18};
List<Integer> numbers = Arrays.stream(numbers).boxed().collect(Collectors.toList());

The stream.of() method in Java 8 is a static factory method used to create a stream from its arguments. When you pass an array to stream.of(), the array is treated as a single element in the stream.

3.3 Obtaining Stream From Files

We can use the Files.lines() to create lines of a file. Take the following code for example;

String filePath = "java.txt";
        try {
	Stream<String> lines = Files.lines(Paths.get(filePath))
            long count = lines.filter(l -> l.contains("JavaTech")) .count(); 
            System.out.println("No of lines containing JavaTech: " + count);
        } catch (IOException e) {
            e.printStackTrace();
        }

Here, we are reading a file named(java.txt) line by line using Java’s Files.lines() method, which returns a stream of lines from the file. The goal is to count how many lines in the file contain the word “JavaTech”.

If the file contains lines with the word “JavaTech”, the program will print the number of such lines. Otherwise, it will print 0. The program also handles any potential IOException that may occur if the file is not found or cannot be read.

4. Type of Operations on Java Streams

Java streams provide a way to perform a number of operations on data sets and elements. These stream operations can be easily categorized into two different types: intermediate and terminal operations.  

4.1 Intermediate Operations

Intermediate operations utilize lazy execution. They are executed only if a terminal operation is encountered. A terminal operation lazily executes the intermediate operation, pipeline, and terminates it. The intermediate operation generates an output stream. sorted(), map(), and filter() are to name some methods of intermediate examples. 

Java Stream Intermediate Operations

Let’s take a closer look at the various types of stream intermediate operations with their examples.

1. filter()

The filter() method checks all stream elements and returns those that match the given predicate. Therefore, it filters out the elements based on a condition.

Example :

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Now, we need to filter out only even numbers

List<Integer> evenNumbers = numbers.stream() .filter(num -> num % 2 == 0) .collect(Collectors.toList());

2. map()

The map() method performs the given operation at all the stream elements and returns a transformed stream due to the applied function. 

Example :

List<String> programmingLanguages = Arrays.asList("Java", "Python", "Kotlin","C");
List<String> upperCaseProgrammingLanguages = programmingLanguages.stream()
.map(String::toUpperCase) .collect(Collectors.toList());

3. sorted()

The sorted() method sorts the stream elements as per their natural (default) order or according to the order defined by a comparator. 

Example: 1

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 6);

Now sort numbers in ascending order

List<Integer> sortedNumbers = numbers.stream() .sorted().collect(Collectors.toList());

Example: 2 (descending order)

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 3, 7);
List<Integer> sortedDescendingNumbers = numbers.stream().sorted((a, b) -> b - a)  .collect(Collectors.toList());

4. distinct()

The distinct() method returns a stream of different or unique elements after removing the duplicate ones. It applies the hashCode() and equals() methods to extract the distinct elements from the stream.

Example:

List<String> fruitsList= Arrays.asList("grapes", "orange", "grapes", "mango", "banana");
List<String> distinctFruitsList = fruitsList.stream().distinct() .collect(Collectors.toList());

5. peek()

The peek() method allows you to perform an action on the given stream and returns a new stream consisting of the original stream elements. It’s often used for debugging or logging intermediate results.

Example:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().filter(num -> num % 2 == 0).peek(num -> System.out.println("number: " + num).count();

4.2 Terminal Operations

Terminal operation is the final action performed on stream. Terminal operations are executed eagerly and are performed to get a single result like a collection, a primitive type, or an object. It always returns whether concrete types or creates a side effect. 

Terminal operation is the one that triggers all the lazy executions in the stream. And it marks the end of the stream, making the stream unusable afterward. Terminal operations include many methods such as findFirts(), collect(), and forEach().

Java Stream Terminal Operations

Here is a comprehensive list of Java Stream Terminal Operations, along with its relevant examples.

1. forEach()

The forEach() method sequentially acts on each of the stream values. It is useful for iterating over elements and producing side effects printing, modifying variables, or updating data.

Example:

List<String> stringNumberList = new ArrayList<>();
        stringNumberList .add("one");
        stringNumberList .add("two");
        stringNumberList .add("three");
        stringNumberList .add("Four");
        stream<String> stream = stringNumberList .stream();
        stream.forEach(System.out::println);

2. findFirst()

The findFirst() method retrieves the first element from the stream as an instance of an Optional object.

Example :

List<Integer> numbers = Arrays.asList(1, 3, 5, 4, 2 ,4 ,8 ,7); 
Optional<Integer> firstOdd = numbers.stream() .filter(num -> num % 2 != 0) .findFirst();

3. collect()

The collect() method collects the output of the stream into a mutable result container such as Lists or Sets. The collection is done based on the Collector parameter.

Example:

List<String> springFrameworks= Arrays.asList("SpringCore", "Springboot", "SpringMVC");
Set<String> collectedFrameWorksNames = springFrameworks.stream() .collect(Collectors.toSet());

4. anyMatch()

The anyMatch() compares the stream elements with the given predicate and returns true if any of the stream elements matches the specified condition otherwise, false.  

Example:

List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 8);
boolean hasEven = numbers.stream() .anyMatch(num -> num % 2 == 0);

5. reduce()

The reduce() method reduces the stream elements into a single value. It takes one to three parameters with a binary operator as the compulsory parameter. The binary operator specifies the reduction operation.

Example:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream() .reduce(0, (a, b) -> a + b);

5. Conclusion

Java stream is a robust tool for manipulating data in a functional and declarative style. This blog explores the concept of Java streams and their benefits. It also provides three different methods for obtaining a stream. Lastly, it sheds some light on basic types of stream operations. Understanding the concept, operations, and best practices helps Java developers write readable, more expressive, and efficient code. 

profile-image
Rakshit Toke

Rakshit Toke is a Java technology innovator and has been managing various Java teams for several years to deliver high-quality software products at TatvaSoft. His profound intelligence and comprehensive technological expertise empower the company to provide innovative solutions and stand out in the marketplace.

Comments

  • Leave a message...