Stream Operations Supported by the Java Streams API

Wednesday May 18th 2016 by Manoj Debnath

Take on the concept of streams from a comparative perspective; we'll illustrate some of its usage in regular Java programming.

Stream APIs is one of the most sophisticated implementations in Java. Stream APIs are mainly used in association with the collection framework. Sometimes, confusion arises in its usage due to such association. This is primarily because it inherently resembles the collection data structure and can be better understood when compared with it. If a stream is a collection of data elements, the generic collection is also a data structure that acts as a container for containment. They are complimentary and often used interchangeably in Java code and, for the sake of understanding, we'll compare and contrast them head on. This article takes on the concept of streams from a comparative perspective to illustrate some of its usage in regular Java programming.


A stream basically is a sequence of data elements that support sequential and parallel aggregate operations. Aggregate operations are like computing the sum of integer elements in a stream or mapping a stream according to their string length, and so forth. A stream supports two types of operation with reference to the way they pull data elements from the data source; one is called lazy or terminal operation and the other is called eager or intermediate operation. Lazy pulling means data elements are not fetched until required, and the strategy of the stream is, by default, lazy. Terminal operations are particularly suitable for working with huge streams of data because they are minimal on memory usage. Eager or intermediate operations are best for performance but uses a lot of memory. For faster response, data elements must be made available in memory. As result, the limitation of its extensiveness is restricted due to the overuse of memory. So, it may be fast but unsuitable for operating on huge sequence of data elements.

Although streams may seem somewhat similar to collections, there are many significant differences.


Unlike collections, streams are not built to store data. They are used on demand to pull data elements from the data source and pass it to the pipeline to proceed with further processing. Collection, on the other hand, is an in-memory data structure. That means data elements must exist in-memory to execute add/fetch an operation on it. In a way, a stream is more concerned with the flow of data, rather than the store of data, which is the main idea of the collections.

Infinite Streams

Because streams pull data on demand, it is possible to represent a sequence of infinite data elements. For example, stream operations can be plugged in to a data source that generates infinite data, such as reciprocating streams in a I/O channel. Collection, on the other hand, is limited due to the use of an in-memory store.


Both streams and collections operate on a number of elements, so the requirement of iteration is obvious. A stream is based on internal iteration; a collection is based on external iteration. Let's illustrate this with the help of an example. A simple aggregate operation on a collection can be as follows:

List<Integer> list =
   Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sum = 0;
for (int i : list) {
   if (i % 2 == 0) {
      i *= 2;
      sum += i;

The code uses a for-loop to iterate over the list of elements.

Observe the following code. There is no external iteration, although iteration is applied internally and, surprisingly, stream operation can be written with marked brevity by using lambda.

int sum2 =
               .filter(i -> i % 2 == 0)
               .map(i -> i * 2)
               .reduce(0, Integer::sum);

The sequential structure of the external iteration is not suitable for parallel execution through multiple threads. Java provides an excellent Fork/Join framework library to leverage the use of modern multiple core CPUs. But, using this framework is not that simple, especially for beginners. Streams, however, simplify some parallel execution functionality, such as the preceding code can be written as follows for parallel execution.

int sum3 = list.parallelStream()
               .filter(i -> i % 2 == 0)
               .map(i -> i * 2)
               .reduce(0, Integer::sum);

In this scenario, the stream is not only using internal iteration but also using multiple threads to do the filtering, multiplication, and summing operations in parallel. Using internal iteration, however, does not mean they cannot be iterated externally. The following external iteration is equally possible.

   if (i % 2 == 0) {
      i *= 2;
      sum += i;

The Architecture

Stream-related classes and interfaces can be found in the package. They are hierarchically arranged in the following manner.

Figure 1: Arrangement of the package

All sub interfaces of a stream have BaseStream as their base interface, which however is inherited from Autocloseable. Also, the package contains two classes—Collectors and StreamSupport—along with a few builder interfaces such as DoubleStream.builder, IntStream.builder, LongStream.builder, Stream.builder, and a Collector interface. In practice, streams are rarely used without a collection as their data source. They mostly go hand-in-hand in Java programming. Refer to the Javadoc for an elaborate description on each of the interfaces and classes in the hierarchy.

A Few Quick Snippets

The iterate() method of a stream is quite versatile and can be used in many ways. The method takes two arguments: a seed and a function. Seed refers to the first element of the stream and the second element is obtained by applying the function to the first element and so on. Suppose we want to find first ten odd natural numbers; we may write the following:

Stream.iterate(1L, n -> n + 2).limit(10)

This will print 1 3 5 7 9.

If we want to skip the first five and then print next five odd natural numbers, we may write the code as:

Stream.iterate(1L, n -> n + 2).skip(5).limit(5)

Now, it will print 11 13 15 17 19.

We can generate some random numbers with the generate() method as follows:


If we want a random integer, we may write:

Stream.generate(new Random()::nextInt).limit(10)

Java 8 has introduced many classes that return their content as a stream representation. The char() method in the CharSequence is an example.

String s="Please  Excuse My Dear Aunt Sally";

.filter(c -> Character.isUpperCase((char)c)
   && !Character.isWhitespace((char)c))
.forEach(c -> System.out.print((char)c));

This prints first character of the each word in the sentence: PEMDAS.

Streams can be directly obtained from the Arrays class as follows:

DoubleStream ds =
   double[]{10.234, 25.26, 3792.8755});

Stream<String> st = String[]
   {"Spade", "Heart", "Diamond", "Club"});


Beginning Java 8 Language Features, Kishori Sharan, Apress.


This is just the beginning; there can be numerous such examples. Stream APIs are excellent in minimizing the toil in programming, sometimes re-inventing the same wheel. When the API codes accompanies lambda, the result is strikingly terse. Many a line of code can be compressed within a single line. This article is an attempt to give a glimpse of what stream APIs are about and was never intended to be a comprehensive guidance. Future articles will explore many of its practical usages in more detail.

Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved