Java 8 streams allow developers to extract precise data from a large collection, using a set of predefined operations.

Before the release of Java 8, using the term "stream" in Java would automatically be associated with I/O. However, Java 8 introduced a stream that can be referred to as a set of computational steps chained together in what is commonly referred to as a "stream pipeline."

This article will introduce you to Java 8 streams and demonstrate how they can be useful in your projects.

What Is a Stream?

A stream is a Java interface that takes a source, conducts a set of operations to extract specific data, then provides that data to the application for use. Essentially, it allows you to extract specialized data from a collection of generalized data.

How Streams Work

A stream pipeline always begins with a source. The type of source is dependent on the type of data that you're dealing with, but two of the more popular ones are arrays and collections.

To transform the collection into an initial stream, you'll need to add the stream() function to the source. This will place the source into the stream pipeline where several different intermediate operations (such as filter() and sort()) can operate on it.

After all the required intermediate operations are conducted, you can introduce a terminal operation (such as forEach()), which will produce the previously extracted data from the source.

Life Without Streams

Java 8 was released in 2014, but before that, Java developers still needed to extract specialized data from a collection of general data.

Let’s say you have a list of random characters that are combined with random numbers to form unique string values, but you only want the values that start with the character “C” and you want to arrange the result in ascending order. This is how you would extract that data without streams.

Related: What You Need to Know About Using Strings in Java

Filtering and Sorting Values Without Streams Example

        
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) {

//declare and initialize the array list
List<String> randomValues = Arrays.asList(
"E11", "D12", "A13", "F14", "C15", "A16",
"B11", "B12", "C13", "B14", "B15", "B16",
"F12", "E13", "C11", "C14", "A15", "C16",
"F11", "C12", "D13", "E14", "D15", "D16"
);

//declare the array list will store needed values
List<String> requiredValues = new ArrayList<>();

//extracting the required values and storing them in reqquiredValues
randomValues.forEach(value -> {
if(value.startsWith("C")) {
requiredValues.add(value);
}
});

//sort the requiredValues in ascending order
requiredValues.sort((String value1, String value2) -> value1.compareTo(value2));

//print each value to the console
requiredValues.forEach((String value) -> System.out.println(value));
}
}

You'll also need to declare and initialize the array list whether you’re using streams or some other method of extraction. What you wouldn’t need to do if you were using streams is declare a new variable to hold the required values, nor create the other five plus lines of code in the example above.

Related: How to Create and Perform Operations on Arrays in Java

The code above produces the following output in the console:

        
C11
C12
C13
C14
C15
C16

Life With Streams

In programming, efficiency speaks to producing the same result with significantly less code. This is exactly what a stream pipeline does for a programmer. So the next time someone asks: “why is it important to use streams in your project?” Simply put: “streams support efficient programming.”

Continuing with our example above, this is how introducing streams transforms the entire program.

Filtering and Sorting Values With a Stream Example

        
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {

//declare and initialize the array list
List<String> randomValues = Arrays.asList(
"E11", "D12", "A13", "F14", "C15", "A16",
"B11", "B12", "C13", "B14", "B15", "B16",
"F12", "E13", "C11", "C14", "A15", "C16",
"F11", "C12", "D13", "E14", "D15", "D16"
);

//retrieves only values that start with C, sort them, and print them to the console.
randomValues.stream().filter(value->value.startsWith("C")).sorted().forEach(System.out::println);
}
}

The code above demonstrates just how powerful the stream interface is. It takes a list of random array values and transforms it into a stream using the stream() function. The stream is then reduced to an array list that contains the required values (which is all the values starting with C), using the filter() function.

As you can see in the example above, the C values are randomly arranged in the array list. If you were to print the stream at this point in the pipeline, the value C15 would be printed first. Therefore, the sort() function is introduced to the stream pipeline to rearrange the new array in ascending order.

The final function in the stream pipeline is a forEach() function. This is a terminal function that's used to stop the stream pipeline and produces the following results in the console:

        
C11
C12
C13
C14
C15
C16

Stream Intermediate Operations

There's an extensive list of intermediate operations that can be used in a stream pipeline.

A stream pipeline always starts with a single source and a stream() function, and always ends with a single terminal operation (though there are several different ones to choose from.) But in between these two sections is a list of six intermediate operations that you can use.

In our example above, only two of these intermediate operations are used---filter() and sort(). The intermediate operation that you choose will depend on the tasks you wish to perform.

If any of the values that begin with “C” in our array list above were in lowercase, and we performed the same intermediate operations on them, we would get the following result.

Performing Filter and Sort Operations on Lowercase Values Example

        
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) {

//declare and initialize the array list
List<String> randomValues = Arrays.asList(
"E11", "D12", "A13", "F14", "C15", "A16",
"B11", "B12", "c13", "B14", "B15", "B16",
"F12", "E13", "C11", "C14", "A15", "c16",
"F11", "C12", "D13", "E14", "D15", "D16"
);

//retrieves only values that start with C, sort them, and print them to the console.
randomValues.stream().filter(value->value.startsWith("C")).sorted().forEach(System.out::println);
}
}

The code above will produce the following values in the console:

        
C11
C12
C14
C15

The only problem with the output above is that it doesn’t accurately represent all the values in our array list. A good way to fix this little error is to introduce another intermediate operation to the stream pipeline; this operation is known as the map() function.

Using the Map Function Example

        
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) {

//declare and initialize the array list
List<String> randomValues = Arrays.asList(
"E11", "D12", "A13", "F14", "C15", "A16",
"B11", "B12", "c13", "B14", "B15", "B16",
"F12", "E13", "C11", "C14", "A15", "c16",
"F11", "C12", "D13", "E14", "D15", "D16"
);

//transforms all lower case characters to upper case,
//retrieves only values that start with C, sort them, and print them to the console.
randomValues.stream().map(String::toUpperCase).filter(value->value.startsWith("C")).sorted().forEach(System.out::println);
}
}

The map() function transforms an object from one state to another; in our example above it transforms all the lowercase characters in the array list to uppercase characters.

Placing the map() function just before the filter() function retrieves all the values that begin with C from the array list.

The code above produces the following result in the console, successfully representing all the values in the array list.

        
C11
C12
C13
C14
C15
C16

The other three intermediate operations that you can use in your applications include:

  • peek()
  • limit()
  • skip()

Java 8 Streams Facilitate the Creation of Efficient Code

With Java 8 streams you can extract extra specific, relevant data from a large source with one line of code. As long as you include the initial stream() function and a terminal operator, you can use any combination of intermediate operations that provide fitting outputs for your goal.

If you’re wondering about the line of code enclosed within our filter() function; it's known as a "lambda expression." Lambda expressions are another feature introduced with Java 8, and it has a lot of nuggets that you might find useful.