The why, how, and what of Java streams

public static void main(String[] args) {
	Stream.of("Row, row, row your boat,",
		"gently down the Java stream!",
		"Merrily, merrily, merrily, merrily,",
		"functional programming is but a dream!")
		.forEach(System.out::println);
}

Java was my main programming language throughout high school and college, but by the time I graduated from college in 2013, the concept of Java streams did not yet exist. (We were still on Java 7.) I could write all sorts of awesome programs using for and while loops. If those constructs could help me get my projects done, and if I could receive my computer science degree without relying on streams, then what kinds of value do Java streams provide?

To aid us in answering this question, we ask another one: given a collection of objects, can we come up with a functional-based, more readable approach to processing those objects?

Not much later, on March 18, 2014, Java 8 was released, and with it came Java streams. What Java 8 was not packaged with was a sudden change in the way that we developers think about and approach solutions to problems. For me, it is easy to think in terms of imperative statements. Over time, however, I have found increasingly greater value in including functional programming in my Java development.

Example of imperative programming vs using streams

Let’s consider a simple example where we have a list of names provided to us, and we want to convert all of them to all-caps. We also want to exclude any names that came in as blank (empty or all whitespace). An imperative-style implementation might look like this:

public static List<String> toUpperCase(List<String> names) {
	List<String> uppercaseNames = new ArrayList<>();
	for (String name : names) {
		if (!name.isBlank()) {
			uppercaseNames.add(name.toUpperCase());
		}
	}
	return uppercaseNames;
}

The above code block represents how I was programmed to think when it comes to converting all the strings to uppercase and omitting blank strings. But what about a developer inspecting this code for the first time? The inspector’s thought process as he or she reads it might be as follows:

It looks like we’re creating a new list here called uppercaseNames; I imagine this is going to contain all the names but in uppercase. Then next we have a for loop. This is looping through all the names. For each name we check if it is not blank, and if so, we add the uppercase version of the name to uppercaseNames. Finally, it returns the list of uppercase names once it’s populated.

Upon reading the above thought process, I notice some red flags. First, the inspector imagines that uppercaseNames is going to contain all the names in uppercase, but the inspector does not know that for sure until he or she inspects the for loop. Also, it takes a while before the inspector sees how the uppercase names are being populated. Moreover, the thought process is quite verbose and needs to be simplified.

Instead, the thought process should more closely match how I would verbally explain the code to someone in a way that is easy to understand. The easier it is to explain the code to others, the less prone we are to bugs.

Let’s simplify the above thought process. The following is what the inspector might think if the code were more readable:

It looks like we’re streaming the list of names, filtering out the blank ones and mapping the rest to uppercase, before returning all the uppercase names as a list.

And this is the code that the inspector would be reading:

public static List<String> toUpperCase(List<String> names) {
	return names.stream()
		.filter(x -> !x.isBlank())
		.map(String::toUpperCase)
		.toList();
}

The code above uses Java streams. Not only is it easier to understand and review this code; the code is also less bug-prone for these reasons:

  • No nesting is involved.
  • The underlying collector populates the list for us; we do not have to do it ourselves.
  • Java streams don’t let us modify the source (names), whereas we do not have that protection when using for or while constructs.

How do Java streams work?

It is important to know that a stream pipeline has the following components, in order:

  1. The source
  2. Zero or more intermediate operations
  3. The terminal operation

The elements that enter a stream must come from somewhere. That somewhere is known as the source. Common examples of sources are collections (list, set, etc.), arrays, and hardcoded sequences of values; another example can be the lines of a file. The first step of creating a stream pipeline is to create a Stream object directly tied to the source.

The next step is to process the elements through the pipeline using intermediate operations, which return modified streams in which each element coming out of the previous stream gets processed. Some examples of intermediate operations from the Stream class are:

  • map, which applies a mapping function to each element
  • filter, which drops elements from the pipeline if they do not satisfy the given criteria
  • distinct, which drops duplicate elements from the pipeline
  • limit, which returns a stream that is truncated to include no more than the given number of elements.
  • sorted, which returns a stream where the elements are sorted

The last part is the terminal operation, which traverses the stream to produce a result or side effect. (The stream is traversed in full or in part, depending on the operation.) In the above example, the terminal operation toList creates a list from the elements in the stream after they were processed. Some other examples of terminal operations are:

  • toArray, which dumps all the stream elements into an array
  • findFirst, which returns an Optional for the first element of the stream (if the stream isn’t empty)
  • sum, which returns the sum of the integer elements in the stream (applies to IntStream)
  • collect, which feeds all the stream elements into some Collector, which in turn produces a result
  • forEach, which performs some action on each stream element

Leave a Reply

Your email address will not be published. Required fields are marked *