As you work with Java 8 Streams, you’ll often need to perform various operations on your data, such as filtering, mapping, and sorting. 

These operations are called intermediate operations, and they’re important for efficient data processing. 

In this guide, you’ll learn about the different intermediate operations available in Java 8 Streams, including code snippets and real-life examples to help you understand how to use them effectively. 

By the end of this guide, you’ll be able to transform and manipulate your data with ease, making you a more proficient Java developer.

Understanding Intermediate Operations

A key concept in Java 8 Streams is intermediate operations, which are methods that transform a stream into another stream. 

These operations do not produce a final result, but instead, return a new stream that can be further processed.

Types of Intermediate Operations

A wide range of intermediate operations are available in Java 8 Streams, including filtering, mapping, flat mapping, distinct, sorting, peeking, limiting, and skipping. 

Knowing the different types of intermediate operations and their use cases is important for effective stream processing.

OperationDescription
FilteringFilters out unwanted elements from a stream
MappingTransforms elements in a stream
Flat MappingFlattens nested collections into a single stream
DistinctRemoves duplicates from a stream
SortingSorts elements in a stream

1. Filtering and Mapping

Now that you have a basic understanding of intermediate operations, let’s dive deeper into two important operations: filtering and mapping.

filter(): Filtering out Unwanted Elements

Filtering out unwanted elements from a stream is a common operation in data processing. 

You can use the filter() method to remove elements that don’t meet certain conditions. 

For example, if you have a stream of integers and you want to filter out even numbers, you can use the following code:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); 
List oddNumbers = numbers.
                  stream().
                  filter(n -> n % 2!= 0).
                  collect(Collectors.toList());

This code will result in a list of odd numbers: [1, 3, 5]

You can use filter() to remove invalid data from a dataset, ensuring that your data is clean and reliable.

map(): Transforming Elements in a Stream

Example of transforming elements in a stream is converting strings to uppercase. 

You can use the map() method to apply a transformation function to each element in the stream.

List words = Arrays.asList("hello", "world", "java"); 
List uppercaseWords = words.
                      stream().
                      map(String::toUpperCase).
                      collect(Collectors.toList());

This code will result in a list of uppercase words: 

[“HELLO”, “WORLD”, “JAVA”]`.

You can use map() for data normalization, ensuring that your data is in a consistent format.

2. FlatMapping and Distinct

Despite the power of filtering and mapping, you may encounter situations where you need to process complex data structures or remove duplicates from your stream. 

This is where flat mapping and distinct operations come into play.

flatMap(): Flattening Nested Collections

Even when working with collections, you may have nested collections that need to be flattened into a single stream. 

The flatMap() operation is designed specifically for this purpose. 

It takes a function that returns a stream as an argument and applies it to each element in the original stream, flattening the resulting streams into a single stream.

For example, suppose you have a list of lists of strings and you want to create a single stream of strings:

List<List> nestedList = Arrays.asList(
                               Arrays.asList("a", "b", "c"), 
                               Arrays.asList("d", "e", "f"),  
                               Arrays.asList("g", "h", "i") 
                        ); 
List flatList = nestedList.
                stream().
                flatMap(list -> list.stream()).
                collect(Collectors.toList());

In this example, the flatMap() operation takes a function that returns a stream of strings from each inner list, and then flattens these streams into a single stream of strings.

distinct(): Removing Duplicates from a Stream

An crucial operation in data processing is removing duplicates from a stream. 

The distinct() operation does exactly that, returning a new stream that contains only unique elements.

Duplicates can arise from various sources, such as data entry errors or redundant data processing. 

By using the distinct() operation, you can ensure that your stream contains only unique elements, which is particularly important in data analysis and reporting.

For example, suppose you have a stream of strings and you want to remove duplicates:

List list = Arrays.asList("a", "b", "c", "b", "d", "e", "e"); 
List distinctList = list.
                    stream().
                    distinct().
                    collect(Collectors.toList());

In this example, the distinct() operation removes duplicates from the original stream, resulting in a new stream that contains only unique strings.

3. Sorting and Peeking

Unlike other intermediate operations, sorting and peeking allow you to manipulate and inspect the elements in your stream without actually consuming them.

sorted(): Sorting Elements in a Stream

Any time you need to process a stream in a specific order, you can use the sorted() method to sort its elements. 

This method returns a new stream that contains the same elements as the original stream, but in a sorted order. Here’s an example:

List numbers = Arrays.asList(5, 2, 8, 1, 9); 
List sortedNumbers = numbers.
                     stream().
                     sorted().
                     collect(Collectors.toList()); 
System.out.println(sortedNumbers); // [1, 2, 5, 8, 9]

peek(): Sneaking a Peek at Intermediate Results

Now that you’ve seen how to sort elements in a stream, let’s talk about how to inspect them during processing. 

The peek() method allows you to perform an action on each element in a stream without actually consuming it. 

This method is useful for debugging and logging purposes.

For instance, if you want to print the intermediate results of a stream pipeline, you can use peek() to log each element as it’s processed:

List words = Arrays.asList("hello", "world", "java", "streams"); 
words.
stream().
filter(word -> word.length() > 4).
peek(word -> System.out.println("Processing: " + word)).
map(String::toUpperCase).
forEach(System.out::println);

In this example, the peek() method is used to print each word as it’s processed, allowing you to see the intermediate results of the stream pipeline.

4. Limiting and Skipping

After exploring various intermediate operations, you’re now ready to learn about limiting and skipping elements in a stream.

limit(): Limiting the Number of Elements in a Stream

With the limit() operation, you can restrict the number of elements in a stream to a specified size. 

This is particularly useful when you need to sample data or test a subset of your data. 

For example, if you have a stream of integers and you want to take the first 10 elements, you can use the limit() operation as follows:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); 
List limitedNumbers = numbers.
                      stream().
                      limit(10).
                      collect(Collectors.toList());

skip(): Skipping Elements in a Stream

Skip operation is particularly useful when you need to exclude a certain number of elements from your data, such as skipping headers in a file or ignoring initial errors in a log.

List words = Arrays.asList("apple", "banana", "cherry", "date", 
                           "elderberry", "fig", "grape", "honeydew"); 
List skippedWords = words.
                    stream().
                    skip(5).
                    collect(Collectors.toList());

Elements that are skipped are not processed further in the pipeline. 

This means that any intermediate operations that come after skip() operation will only see the elements that were not skipped. 

Pros and Cons of Intermediate Operations

Your intermediate operations can greatly impact the performance and efficiency of your Stream processing. 

It’s important to weigh the advantages and disadvantages of each operation to make informed decisions.

/media/b53350491a40c717f14c20fede0da746

Advantages of Intermediate Operations

Operations like filtering, mapping, and sorting enable you to process data in a flexible and efficient manner. 

By applying these operations, you can transform and refine your data to extract valuable insights and improve overall data quality.

Disadvantages of Intermediate Operations

Intermediate operations can add complexity to your code, making it challenging to debug and maintain. 

Moreover, incorrect usage of these operations can lead to performance degradation and unexpected results.

Understanding the trade-offs between different intermediate operations is important for utilizing their benefits while minimizing their drawbacks. 

Final Words

Summing up, you’ve now explored the world of intermediate operations on Java 8 Streams, which enable you to manipulate and refine your data streams in various ways. 

By mastering these operations, you’ll be able to write more efficient, concise, and readable code that tackles complex data challenges with ease.