Streams let us do computation on the collection of data in a declarative way. In a declarative way of programming, you don’t specify how to do but you specify what to do. You can create a stream pipeline to perform a computation. A stream pipeline consists of :
- a source
- zero or more intermediate operations
- a terminal operation
Java streams are lazy. As a result, intermediate operations are performed only when the terminal operation is initiated.
Java stream pipeline
Let’s consider an example of finding all Java books author from the catalog of books. Using stream declarative construct you can write code as:
List<Book> books =
Catalog.books();
List<String> javaAuthors =
books.stream()
.filter(book -> book.getCategory().equals(JAVA))
.map(Book::getAuthor)
.collect(Collectors.toList());
Let’s try to understand the above stream pipeline in the context of – ‘a stream pipeline consists of a source, zero or more intermediate operations, and a terminal operation‘
Stream Source
in Java source of the stream could be an array, a collection, a generator function, an I/O channel, value range, etc. In the above example stream is created by calling stream()
method of the Collection
class.
Intermediate Operation
An intermediate operation transforms a stream into another stream. The intermediate operations are always lazy. Executing an intermediate operation such as filter()
does not actually perform any filtering, but instead creates a new stream. The new stream when traversed contains the elements of the initial stream that match the given predicate. Here, you have used filter(Predicate)
to filter out books by category. The map(Function)
transforms stream of books to stream of authors.
Terminal Operation
A terminal operation produces a result or side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used. Here, collect
is a terminal operation. This accumulates input elements into a List
.
Code sample
Let’s see some stream creation examples. You can find the code of this article on GitHub .
Creating a stream
There are many ways you can create a stream in Java. Let’s look at a few of them.
Stream from individual elements
Stream of(T… values)
– returns a sequential ordered Stream whose elements are the specified values.
Stream<String> numbers =
Stream.of("One", "Two", "Three");
All streams operations can execute either in serial or in parallel. The stream implementations in the JDK create serial streams unless parallelism is explicitly requested. For example,
JDK stream documentationCollection
has methodsCollection.stream()
andCollection.parallelStream()
, which produce sequential and parallel streams respectively; other stream-bearing methods such asIntStream.range(int, int)
produce sequential streams but these streams can be efficiently parallelized by invoking theirBaseStream.parallel()
method.
Stream ofNullable(T t)
– return a sequential stream containing a single element, if non-null, otherwise returns an empty stream.
Stream<String> numbers =
Stream.ofNullable("One");
Stream<String> empty =
Stream.empty();
The method Stream.ofNullable
provides a simplified null
check. For example – if you need to convert Collection<T>
to Stream<T>
you can do something like:
static Stream<Integer> toStream(Collection<Integer> numbers) {
return Stream.ofNullable(numbers)
.flatMap(Collection::stream);
}
The method ofNullable(T t)
was added in Java 9.
An alternative approach can be to use optional as:
return Optional
.ofNullable(numbers)
.stream()
.flatMap(Collection::stream);
Stream from Array
You can create a sequential Stream
with the specified array
as its source as:
String[] names =
new String[] {"Jack","Jill"};
Stream<String> stream1 =
Stream.of(names);
Stream<String> stream2 =
Arrays.stream(names);
Stream from a Collection
A sequential Stream
can be created by calling the default stream
method of collection
.
List<String> namesList =
List.of("Jack", "Jill");
Stream<String> stream =
namesList.stream();
Stream from a Builder
Using builder you can build an ordered stream by calling add
and build
method.
Stream<String> stream =
Stream.<String>builder()
.add("Jack")
.add("Jill")
.build();
Stream using Generate
You can use generate
method to create an infinite sequential unordered stream where each element is generated by the provided Supplier
. This is suitable for creating a constant stream, a stream of random elements, etc.
Stream<Integer> randomNumbers =
Stream
.generate(new Random()::nextInt)
.limit(10);
Stream using Iterate
You can create an infinite sequential ordered stream by calling a static factory method iterate
(Stream iterate(final T seed, final UnaryOperator f)
). The first element of the Stream is determined by seed and for n > 0, the element at position n will be the result of applying the function f
to the n-1.
Stream<Integer> even =
Stream
.iterate(0, n -> n + 2)
.limit(10);
Primitive Stream
Specialized primitive interfaces IntStream
, DoubleStream
, and LongStream
were added as part of Java 8 to handle primitive operations efficiently.
Let’s try to understand the need for primitive specialization with one example. Consider a Book
class
@Setter
@Getter
public class Book {
private String name;
private Category category;
private double price;
private String author;
private String publisher;
}
To find the accumulative price of all the books, you can do something like:
static double priceOfAllBooks(List<Book> books) {
Double allBooksPrice =
books.stream()
.map(Book::getPrice)
.reduce(0d, Double::sum);
return allBooksPrice;
}
In the above example, you first transform the stream of the book into the stream of Double
by calling map(Book::getPrice)
then the sum is calculated by calling terminal operation reduce(0d, Double::sum)
.
You can simplify this example by transforming Stream<Book>
to DoubleStream
as:
static double priceOfAllBooks(List<Book> books) {
double allBooksPrice =
books.stream()
.mapToDouble(Book::getPrice)
.sum();
return allBooksPrice;
}
In the above example mapToDouble
is an intermediate operation, which returns a DoubleStream
. The DoubleStream
is a stream of double, which defines handy methods such as sum
,min
,max
,etc.
Java has three primitive streams – IntStream
, DoubleStream
, LongStream
to perform stream operation int
, double
and long
primitive type.
Creating Primitive Streams
You can create a primitive stream from a regular stream using methods from the Stream
class. For example, mapToDouble
method converts a stream to DoubleStream
. Similarly, the Stream
class also has mapToInt
and mapToLong
method.
Let’s look at the example of creating primitive streams.
Of
You can create a Sequential ordered Stream
using the static factory method of
as:
IntStream intOne =
IntStream.of(1);
IntStream intOneTwo =
IntStream.of(1,2);
DoubleStream doubleOne =
DoubleStream.of(1);
LongStream longOneTwo =
LongStream.of(1,2);
Range
You can use the range
method from primitive specialization to create a sequential ordered stream.
IntStream oneToNine =
IntStream.range(1, 10);
// Range inclusive
IntStream oneToTen =
IntStream.rangeClosed(1, 10);
Generate
You can create an infinite sequential unordered primitive stream using by using the generate
method of primitive specialization stream. This is suitable for generating constants streams, streams of random numbers, etc.
IntStream tenOnes =
IntStream.generate(() -> 1).limit(10);
DoubleStream tenRandomDouble =
DoubleStream
.generate(() -> new Random()
.nextDouble()).limit(10);
Generating Fibonacci numbers
You can use generate
method for some interesting use cases. For example, the Fibonacci numbers can be generated as:
public class Fibonacci {
private int prev = 0;
private int curr = 1;
private int next() {
int temp = prev + curr;
prev = curr;
curr = temp;
return curr;
}
public IntStream stream() {
return IntStream
.generate(this::next);
}
}
//Caller
Fibonacci fibonacci =
new Fibonacci();
IntStream fibStream =
fibonacci.stream().limit(10);
Iterate
You can create an infinite sequential ordered stream using the iterate method. In IntStream
class, the iterate
method is defined as IntStream iterate(final int seed, final IntUnaryOperator f)
.
The iterate
method produces an infinite stream by applying function f
to an initial seed. This produces a Stream consisting of seed, f(seed),f(f(seed)), etc.
IntStream evenNumbers =
IntStream
.iterate(0, n -> n + 2)
.limit(10);
Other ways of creating Streams
There are many other ways a Stream can be created. For instance, you can combine two streams to create a new stream. Let’s look at a few examples.
Concat
You can use concat
operation of the stream to create a new stream. The elements of the new stream are all the elements of the first stream followed by all the elements of the second stream. The new stream is ordered if both of the input streams are ordered, and parallel if either of the input streams is parallel. If the resulting stream is closed, then both input streams are closed as well.
Stream<Integer> prime =
Stream
.concat(Stream.of(2, 3),
Stream.of(5, 7, 11));
We need to exercise caution with nested concatenation, Stream.concat(Stream.of(..), Stream(Stream.of(..),Stream.of(..))
, as it can result in a deep call chain or even worse StackOverflowError
.
String
Java 8 added utility methods to create a stream from the String object.
Lines
You can create a stream of lines separated by line terminator as:
static Stream<String> lines() {
return "Line separated by newline.\nAnother line."
.lines();
}
The above example creates a stream consisting of two elements:
- Line separated by newline.
- Another line.
Chars
You can use chars
method of String
class to create IntStream
of chars, as:
static IntStream chars() {
return "XYZ".chars();
}
Random
The Random
class has a couple of useful methods to generate streams of random numbers of primitive specialization types.
// Infinite Stream
IntStream randomInts =
new Random().ints();
// Fixed Size Stream
IntStream tenRandomInts =
new Random().ints(10);
// Infinite Stream
DoubleStream randomDoubles =
new Random().doubles();
// Fixed Size Stream
DoubleStream tenRandomDoubles =
new Random().doubles(10);
// Infinite Stream
LongStream randomLongs =
new Random().longs();
// Fixed Size Stream
LongStream tenRandomLongs =
new Random().longs(10);
Summary
Streams let us do computation on the collection of data in a declarative way. You can create a stream pipeline to perform a computation. A stream pipeline consists of :
- a source
- zero or more intermediate operations
- a terminal operation
There are many ways a stream can be created. For instance, you can create steam from an array, collection, or individual elements. You can even create a stream using generate, iterate, or builder method of the Stream class.
Java 8 also added specialized primitive streams. For instance, IntStream
, DoubleStream
, and LongStream
are primitive streams.
The primitive stream can be created from individual elements and using the range method of the primitive stream class. Similarly, the primitive stream can be created using a builder, iterate or generate method.
Discussion about this post