Chapter 3 – Streams
-Streams
--Stream Characteristics
---Streams Do Not Store Elements
---Streams Can Be Unbounded
---Streams Do Not Modify the Original Source
---A Stream May Be Ordered
---Automatic Parallelization
--An Overview of Stream Methods
-Creating a Stream
--Using the Stream Interface’s of Method
--Using the Collection Class’ stream Method
--Using a Stream.Builder Interface
---Using the builder method
--Generating Infinite Streams
---Using the iterate Method
--Concatenating Streams
--Generating an Empty Stream
-Using Stream Methods
--The Part Example Class
--Using the forEach Method
---Using the forEachOrdered Method
--Using the map Method
---Using Other Map Methods
--Using the flatmap Method
--Using the filter Method
--Using the Match Type Methods
--Using the findFirst Method
---Using the findAny Method
---Using the Optional Class
--Using the reduce Method
--Using the Collector Interface and the collect Method
---Using the Collects Class
--Using the iterator Method
--Using the sorted Method
--Using the distinct Method
--Using the limit Method
--Using the skip Method
--Using the max, min, and count Methods
-Streams for Primitive Types
--Using the IntStream Interface
-Understanding Lazy and Eager Evaluation
--Using Short-Circuit Methods
-Using Parallel Stream Operations
-Conclusion
Streams
A stream can be thought of as a sequence of elements that can be processed on an element by element basis. The elements of a stream can be any data type. Streams are different from an array or collection in that they are not a data structure. They do not occupy permanent memory and are transient in nature.
The processing of the elements in a stream occurs by executing methods against the elements of a stream. A stream chains these method calls together. The output of one stream method is fed, or chained, to another method. This is sometimes referred to as fluent chaining. These methods will perform some operation on the stream such as removing some elements from the stream or converting one element type to another element type. A stream is essentially a wrapper around a collection of objects. Many stream operations use lambda expressions as covered in Chapter 2.
Do not confuse the concept of streams with that of input and output streams found in the java.io package. These are different concepts. An IO stream is generally considered to be a sequence of bytes.
The java.util.stream package has been added to Java 8. This package consists primarily of interfaces. The Stream interface and the objects implementing this interface provide the support for the implementation of streams.
There are two primary benefits to using streams:
- They are more succinct than an equivalent collection implementation
- They have the potential to improve performance by taking advantage of multiple processors
The code used by streams is more succinct than those using older techniques such as the iterator approach. The following shows two ways of listing only those parts whose price exceeds 0.15. The first approach uses an iterator and the second approach uses a stream. The stream approach is shorter and easier to follow. The filter method will remove those elements which do not meet the criteria specified by the predicate. Predicates were discussed in Chapter 2. The forEach method will apply the println method against each element.
// Iterator approach
List<Part> partsList = ...
Iterator<Part> partsIterator = partsList.iterator();
while(partsIterator.hasNext()) {
Part part = partsIterator.next();
if(part.getPrice() >= 0.15f) {
System.out.println(part);
}
}
// Stream approach
Stream<Part> partsStream = partsList.stream();
Predicate<Part> pricePredicate = p -> p.getPrice() > 0.15f;
partsStream.filter(pricePredicate).
forEach(System.out::println);
Using a stream instead of a collection means:
- No need for an explicit declaration of intermediate variables or storage
- Potential for lazy evaluation
- More flexibility in design using a more natural style (pipeline)
- Potential for automatic parallelization
In the previous example, the iterator approach required declaring temporary variables to hold intermediate values. This is not the case for the stream implementation. Lazy evaluation will be covered in more depth in Understanding Lazy and Eager Evaluation, but it essentially means that a stream is not necessarily forced to perform an operation against an element until it needs to.
In future examples we will see how multiple methods can be applied to the same stream in an easy to follow chain of invocations. This approach conforms more closely to how solutions to many problems are conceptualized. These types of solutions involve a series of operations applied successively to elements of a collection.
Performance gains are achieved using a parallel stream. This stream will split the problem into multiple instances based on the problem and the number of processors available. The old iterator approach and sequential streams give about the same performance. Parallel streams are normally much faster.
However, a parallel stream should not be used for small lists as the overhead of parallelism will negate any performance gains. Also, it is not always possible to simply switch from a sequential stream to a parallel stream. Often factors, such as the order the elements are processed in, or whether state information is maintained between operations, need to be considered.
A stream does not support the storage of data. It is a pipeline type of operation where data is passed from one operation to the next. They flow as a stream. However, it is easy to create arrays or lists from a stream.
Once a stream processed, it cannot be used again. The stream is said to be consumed. Since a stream is not an actual data structure, when we have processed it there is nothing left. Any temporary memory that may have been used by a stream is gone.
Stream Characteristics
There are several aspects of streams that need to be kept in mind during their use. These include:
- Streams do not store elements
- Streams can be unbounded
- Streams do not modify the original source
- Streams may be ordered
- Automatic parallelization is possible
Streams Do Not Store Elements
Streams do not store elements. Since streams are not data structures there is no need to store data. This is in contrast to collections which do store data. The elements of a stream are obtained from some source and are processed. When the processing is complete, the stream goes away. Its elements will go away unless moved to an array or collection separate from the stream.
When the stream has been processed, its elements are no longer available through the stream. The stream cannot be reused. However, it can be regenerated in most cases.
Streams Can Be Unbounded
Collections are of finite size, streams are unbounded. A stream can contain zero to an infinite number of elements. As we will see in Generating Infinite Streams, an infinite stream can be generated using several techniques. Though not truly infinite, a stream can run for a very long time if needed.
A stream works on a sequence of values. A generator can create these values which are fed into the stream and processed. The stream can be of any length and is considered to be unbounded.
Streams Do Not Modify the Original Source
The source of the elements for a stream frequently originates from a data structure such as a list or array. As such, the operations against the stream and its elements are not performed against the elements in the original data structure. Also, streams do not modify the original source.
A Stream May Be Ordered
A stream may be ordered or unordered. When a stream is created from an array, List, or some generator function, the stream is an ordered stream. In contrast, streams that are produced from a Set are unordered. This does not imply that a stream is ordered by some criteria such as being alphabetical. Rather, it is like a queue where elements that enter first are served first.
With ordered streams, most operations will preserve the original order. The sorted method does not preserve the original order but imposes a new order instead. The unordered method also removes order from a stream. An unordered stream is desirable for performance reasons when used with some parallel operations.
Automatic Parallelization
Streams have two modes: sequential and parallel. When specified to be parallel, the processing of the stream is automatically executed concurrently based on the number of processors available. The programmer does not need to explicitly setup multiple threads. This is discussed in Using Parallel Stream Operations.
An Overview of Stream Methods
The methods of a stream can be classified as either intermediary or terminal. With intermediary methods, the output of the method is fed to another method. That is, they return another stream. With terminal methods, the stream ends and a result is produced. It represents the end of a pipeline. A pipeline consists of a source, zero or more intermediate operations, and a terminal operation. Once a pipeline is terminated, it cannot be used again. Intermediary methods are typically evaluated lazily. This concept is further explored in Understanding Lazy and Eager Evaluation.
Intermediate methods can further be classified as either stateless or stateful. A stateless method will simply forward its result to the next element in the pipeline. A stateful method will capture some data and possibly wait for more data before proceeding.
Table 1 - Summary of Selected Stream Methods lists a set of Stream interface methods. These will be discussed in the subsections that follow.
Table 1 - Summary of Selected Stream Methods
Intermediate:
Method | Purpose | Return Value
filter | Return stream containing elements matching a Predicate | Stream<T>
map | Applies an operation against each element in the stream, possibly transforming it | Stream<U>
flatMap | Flattens out a stream | Stream<R>
sorted | Sorts the elements of a stream | Stream<T>
distinct | Returns a stream which contains no duplicate elements | Stream<T>
limit | Restricts the number of element in the returned stream | Stream<T>
skip | Skips over a specific number of elements in a stream | Stream<T>
Terminal:
Method | Purpose | Return Value
forEach | Performs some operation on each element of the stream and terminates the stream | void
reduce | Combines stream elements together using a BinaryOperator | T
collect | Puts the stream’s elements into a container such as a Collection | R
min | Finds the smallest element as determined by a Comparator | Optional<T>
max | Finds the largest element as determined by a Comparator | Optional<T>
count | Determines the number of elements in a stream | long
anyMatch | Determines whether at least one element of the stream matches a Predicate | boolean
allMatch | Determines whether every element of the stream matches a Predicate | boolean
noneMatch | Determines whether there are no elements of the stream matches a Predicate | boolean
findFirst | Returns the first element of a stream | T
findAny | Returns any element of a stream | T
iterator | Returns an Iterator for the stream | Iterator
toArray | Creates an array based on the elements of the stream | A[]
Creating a Stream
Before a stream can be used, it must be created. There are several ways of creating a stream including from data structures such as a list or an array. Many of the Stream interface’s methods return Stream objects. This is an important capability and permits the “chaining” of Stream methods. Here, we are more concerned with how a stream can be created from an ultimate “source” such as a list.
Using the Stream Interface’s of Method
The Stream interface’s static of method provides a convenient technique for creating streams from a list. In the following example, a stream of strings is created. The forEach method will display the elements of the stream.
Stream<String> stringStream =
Stream.of("alpha", "beta", "gamma", "delta");
stringStream.forEach((s)->System.out.print(s + " "));
The output of this sequence follows:
alpha beta gamma delta
Creating a stream of strings from a string literal can be accomplished using the String class’ split method as shown below:
stringStream
= Stream.of("drop the sword here".split(" "));
stringStream.forEach((s) -> System.out.println(s));
The following is printed:
drop
the
sword
here
We can easily create a stream of Parts using this method as follows. The Part class will be discussed in The Part Example Class.
Stream<Part> partStream = Stream.of(
new Part("Pencil", 100, 5, 0.15f, 500),
new Part("Eraser", 200, 3, 0.25f, 250),
new Part("Paper", 2000, 1, 0.03f, 1200));
partStream.forEach((p) -> System.out.println(p));
The output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
We can also use the of method with an array as illustrated below:
Part[] parts = {
new Part("Pencil", 100, 5, 0.15f, 500),
new Part("Eraser", 200, 3, 0.25f, 250),
new Part("Paper", 2000, 1, 0.03f, 1200));
};
partStream = Stream.of(parts);
partStream.forEach((p) -> System.out.println(p));
The output is the same as the previous example.
Using the Collection Class’ stream Method
The stream method can be applied to those classes that implement the Collection interface including the List class. In the following example, a list of numbers is created and the stream method is then applied to it. The contents of the stream are then displayed.
List list = Arrays.asList(1, 2, 3, 4, 5);
Stream stream = list.stream();
stream.forEach(x -> System.out.print(x + " "));
The output of this sequence follows:
1 2 3 4 5
In the next example, a list of Parts is created. The stream method is then applied against this list and the resulting stream is then displayed.
List<Part> partsList = Arrays.asList(
new Part("Pencil", 100, 5, 0.15f, 500),
new Part("Eraser", 200, 3, 0.25f, 250),
new Part("Paper", 2000, 1, 0.03f, 1200)
);
Stream<Part> partsStream = partsList.stream();
partsStream.forEach(p -> System.out.println(p));
The output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03
The stream method can also be applied against the Arrays class as shown below:
int[] nums = {2, 4, 6, 8};
IntStream intStream = Arrays.stream(nums);
intStream.forEach(x -> System.out.print(x));
The output of this sequence follows:
2 4 6 8
Using a Stream.Builder Interface
A stream can also be created using an instance of the Stream.Builder interface. This interface uses an accept method to add an element to a stream. The build method returns the stream.
In the following example, the class SampleStreamBuilder is declared that implements the Stream.Builder interface. The class uses a list to maintain a collection of items to be added to the stream. The accept method adds new elements to the list. The build method uses the Collection interface’s stream method to create the actual stream.
class SampleStreamBuilder implements Stream.Builder<String> {
List<String> list = new ArrayList<String>();
@Override
public void accept(String t) {
list.add(t);
}
@Override
public Stream<String> build() {
return list.stream();
}
}
The following code sequence creates an instance of SampleStreamBuilder and uses the accept method to add strings to the internal list. The build method is then invoked to return the stream. The forEach method is used to display the contents of the stream.
SampleStreamBuilder streamBuilder =
new SampleStreamBuilder();
streamBuilder.accept("Bob");
streamBuilder.accept("Sue");
streamBuilder.accept("Mary");
Stream<String> stream = streamBuilder.build();
stream.forEach((s)->System.out.println(s));
The output follows:
Bob
Sue
Mary
Using the builder method
The Stream interface also has a static method, builder, that returns an instance of a Stream.Builder. This convenience method is used below to duplicate the previous example:
Stream.Builder streamBuilder = Stream.builder();
streamBuilder.accept("Bob");
streamBuilder.accept("Sue");
streamBuilder.accept("Mary");
Stream<String> stream = streamBuilder.build();
stream.forEach((s) -> System.out.println(s));
The output of this sequence when executed will be the same as for the previous example.
Generating Infinite Streams
An infinite stream is one that, in theory at least, will produce an unlimited number of elements. One way of generating an infinite stream is to base the input off of the keyboard as shown below. This approach uses the Stream interface’s generate method. This method uses a Supplier functional interface implementation as an argument. Each word in a line of input is displayed separated by a backslash.
Scanner scanner = new Scanner(System.in);
Supplier<String> cliSupplier = () -> {
return scanner.next();
};
Stream.generate(cliSupplier).
forEach((c) -> System.out.print(c+"\\")
The supplier is the source of input. In this case it is the keyboard. The user will enter data line by line. One possible input/output sequence follows:
Once upon a time
Once\upon\a\time\
There was a giant tree
There\was\a\giant\tree\
Using the iterate Method
The Stream interface’s static iterate method can be used to generate an infinite stream. The method uses two arguments: an initial seed, and a UnaryOperator function to generate the next element. The following example displays the lower and upper case letters of the alphabet. In this example, the seed is a char and the operator increases it by one. The map method, discussed in Using the map Method, converts the char to a string containing the lower and upper case letters. The limit method restricts the output to 26 elements.
Stream.iterate('a', c -> c++).
map(c -> c.toString() + " " + c.toString().
toUpperCase()).
limit(26).
forEach(c -> System.out.print(c + " "));
The output of this code sequence follows:
a A b B c C d D e E f F g G h H i I j J k K l L m M n N o O p P q Q r R s S t T u U v V w W x X y Y z Z
Concatenating Streams
The source of a stream can be from combining two or more other streams. Streams can be concatenated using the static concat method. This method is illustrated below where two integer streams are created. The first stream consists of the first 10 even numbers and the second stream consists of the first 10 odd numbers. The limit method restricts the output of the iterate methods to 10 elements. The concat method takes two streams and returns a third.
Stream<Integer> evenStream =
Stream.iterate(2, n -> n+2).limit(10);
Stream<Integer> oddStream =
Stream.iterate(1, n -> n+2).limit(10);
Stream<Integer> numbers =
Stream.concat(evenStream,oddStream);
numbers.forEach(n -> System.out.print(n + " "));
The output will be as follows:
2 4 6 8 10 12 14 16 18 20 1 3 5 7 9 11 13 15 17 19
Generating an Empty Stream
An empty stream is one that does not contain any elements. It may be returned by some methods or can be created using the empty method as shown below:
Stream<Integer> empty = Stream.empty();
If the Stream interface’s count method returns 0, then the stream is empty.
Using Stream Methods
There are numerous methods that can be applied to a stream. These are typically used for such activities as listing elements of a stream, creating new streams after transforming existing elements, or filtering out certain elements.
The examples used for the Stream interface methods will, at times, use methods not yet covered. This is sometimes necessary to demonstrate the utility of a technique. When this occurs, a brief introduction to the method will be presented. A more complete coverage can be found in the corresponding method’s section. In this section we will examine most of the Stream interface’s methods.
The Part Example Class
For the examples in this chapter we will be using a Stream based on a Part class. The class is shown below. It possesses a number of getter/setter methods along with an overridden toString and equals method. A comparator method is provided to compare two parts.
public class Part {
private String name;
private int partNumber;
private int weight;
private float price;
private int quantity;
public Part(String name, int partNumber, int weight, float price, int quantity) {
this.name = name;
this.partNumber = partNumber;
this.weight = weight;
this.price = price;
this.quantity = quantity;
}
public Part(Part part) {
this(part.name, part.partNumber, part.weight,
part.price, part.quantity);
}
public Part() {
this("Default",12345,10,10.0f,0);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPartNumber() {
return partNumber;
}
public void setPartNumber(int partNumber) {
this.partNumber = partNumber;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public boolean equals(Part part) {
return this.name.equals(part.name);
}
public int comparator(Part part) {
return this.name.compareTo(part.name);
}
public String toString() {
return String.format(
"Name: %-8s Weight: %3d Part Number: %5d Price: %.2f Quantity: %5d",
this.name, this.weight, this.partNumber, this.price, this.quantity);
}
}
We will also create a List of parts defined below:
List<Part> partsList = Arrays.asList(
new Part("Pencil", 100, 5, 0.15f, 500),
new Part("Eraser", 200, 3, 0.25f, 250),
new Part("Paper", 2000, 1, 0.03f, 1200)
);
This partsList will be used as the basis for a number of the examples in this chapter. In some examples, more elements are added when these three parts are not sufficient to demonstrate a technique.
Using the forEach Method
This method is useful for processing each element of a stream. Its parameter must be compatible with the Consumer interface’s accept method. The following code sequence illustrates how each element of the stream can be displayed:
Stream<Part> partsStream = partsList.stream();
partsStream.forEach(System.out::println);
The output of this sequence follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
Alternatively we can increase the price for each part by 5% using the following lambda expression:
partsStream.forEach(x -> x.setPrice(x.getPrice()*1.05f));
However, this does not modify the original list as demonstrated below:
Stream<Part> partsStream = partsList.stream();
partsStream.forEach(x -> x.setPrice(x.getPrice()*1.05f));
for (Part part : partsList) {
System.out.println(part);
}
The contents of the partsList follows. It has not been changed.
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.26 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
However, we can use the map method to create a new stream by creating copies of the elements of the original stream. This is illustrated in Using the map Method.
One of the drawbacks with the forEach method is that you cannot break out of the loop as you can with a forEach statement. As with all streams, the stream can only be used once.
Using the forEachOrdered Method
When a forEach statement is used against a stream, it can be executed concurrently. However, the stream must be a parallel stream. One way of creating a parallel stream is with the parallelStream method as shown below:
partsList.parallelStream().
forEach(x ->System.out.println(x));
The order that the elements of a stream are processed in, by the forEach method, is nondeterministic. That is, it cannot be predicted in advance. This is more apparent for parallel streams where the stream is broken into sub-streams, processed individually, and then combined.
The purpose of the forEachOrdered method is to guarantee that the order will be observed. Otherwise, the use of these two methods is identical. The following sequence illustrates the use of this method:
partsStream = partsList.stream();
partsStream.forEachOrdered(System.out::println);
The output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.26 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
Using the map Method
The map method accepts a Function interface argument. This interface converts one element of a stream into a new element. To demonstrate the map method, we first declare a Function interface variable called mapper. The definition uses a lambda expression that creates a new Part based on an existing part, raises the price of each part by 5% and then returns the new part.
Function<Part, Part> mapper = (Part part) -> {
Part newPart = new Part(part);
newPart.setPrice(part.getPrice() * 1.05f);
return newPart;
};
The mapper reference variable is then used as the argument of the map method as shown below. The content of the newStream is then displayed.
Stream<Part> newStream = partsStream.map(mapper);
newStream.forEach(System.out::println);
The output of this sequence follows. The price of paper does not appear to increase due to the rounding of the output.
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.26 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
The previous two statements can be combined as follows:
partsStream.map(mapper).forEach(System.out::println);
The map function can be used to extract the part numbers from a stream. The next example uses the toArray method to generate an array Integers that holds the part numbers:
Integer[] partNumbers =
partsStream.map(Part::getPartNumber).
toArray(Integer[]::new);
for(Integer partNumber : partNumbers) {
System.out.println(partNumber);
}
The output follows:
100
200
2000
Using Other Map Methods
There are several other mapping methods available. These methods are summarized in Table 2 - Map Method Summary. Each of the argument functions uses an applyAsDataType function method. For example, the ToIntFunction interface uses an applyAsInt method.
Table 2 - Map Method Summary
Return Type | Method | Argument
DoubleStream | mapToDouble | ToDoubleFunction
IntStream | mapToInt | ToIntFunction
LongStream | mapToLong | ToLongFunction
The DoubleStream, IntStream, and LongStream interfaces represent streams of the corresponding data type.
The following example declares an array of doubles and then uses the Arrays class’ stream method to create a DoubleStream. The mapToInt method is then applied to each element. Its effect is to double the element’s value and return the result as an integer.
double numbers[] = {1.2, 34.3, 6.045, 45.32};
DoubleStream doubleStream = Arrays.stream(numbers);
doubleStream.
mapToInt(d -> ((int) d * 2)).
forEach(n -> System.out.print(n + " "));
The output follows:
2 68 12 90
The following uses the previously created partsStream. It computes the value of each part (the product of its price and quantity fields) and displays the total value as an integer. The sum method is a terminal method that, as its name implies, will total the elements of a stream.
int total = partsStream.
mapToInt(p -> (int)(p.getPrice()*p.getQuantity())).
sum();
System.out.println("Total value: " + total);
Total value: 173
Using the flatmap Method
The flatMap method is similar to the map method. The map method uses a function passed to it, applies that function to each element, and then returns a stream of the new elements. Sometimes the returned stream consists of sub-streams. The flatMap method performs similarly to the map method except it will “flatten” the sub-streams into a single stream.
The flatMap method will accept a function that is applied to each element generating a sub-stream. It will then combine these sub-streams into a single stream.
Consider the following code sequence. A list of parts, partsList, is created. A new list, mappedParts is created that consists of a list of a list of floats. This list will consist of sub-lists each listing the prices of a Part that is 10% less than a base price, the base price, and 10% more than a base price. This list is created using the asList method.
To create the mappedParts list, a stream is created from partsList. A map function is applied to the stream where a list is created using the asList method, and then the Collector’s toList method returns the list of lists. The Collector interface is discussed in Using the Collector Interface and the collect Method.
List<Float> partsList = Arrays.asList(
new Part("Pencil", 100, 5, 0.15f, 500).getPrice(),
new Part("Eraser", 200, 3, 0.25f, 250).getPrice(),
new Part("Paper", 2000, 1, 0.03f, 1200).getPrice()
);
List<List<Float>> mappedParts =
partsList.stream().
map(p -> Arrays.asList(p-=p*0.10f, p, p+=p*0.10f)).
collect(Collectors.toList());
System.out.println(mappedParts);
The output of this sequence follows:
[[0.135, 0.135, 0.14850001], [0.225, 0.225, 0.24749999], [0.026999999, 0.026999999, 0.029699998]]
The flatMap method is used in the following code sequence in place of the map method. In addition, the stream method is applied to the list produced by the asList method. This allows the flatMap method to flatten out the sub-streams into a single stream.
List<Float> flatParts = partsList.stream().
flatMap(p -> Arrays.asList(p-=p*0.10f, p, p+=p*0.10f).
stream()).
collect(Collectors.toList());
System.out.println(flatParts);
The output follows:
[0.135, 0.135, 0.14850001, 0.225, 0.225, 0.24749999, 0.026999999, 0.026999999, 0.029699998]
It differs from the previous technique in that it uses a different return type, it uses the flatMap method instead of the map method, and the stream method is used instead of the asList method.
Using the filter Method
The filter method, as its name implies, filters out or removes elements of a stream based on some set of criteria. The method’s argument is a predicate lambda expression. In this example, a predicate is defined to determine if the weight of a part is greater than or equal to three. The forEach method is then used to display the selected elements.
Predicate<Part> weightPredicate = p -> p.getWeight() >= 3;
partsStream.filter(weightPredicate).
forEach(System.out::println);
The output of this sequence is shown below:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
A further enhancement of this approach is to filter out those elements whose price is greater than or equal to 0.15 after the parts have been given a 5% increase. This is expressed succinctly as follows. The mapper variable was previously declared in Using the map Method.
Predicate<Part> pricePredicate = p -> p.getPrice() > 0.15f;
partsStream.map(mapper).filter(pricePredicate).
forEach(System.out::println);
Multiple filters can be used together as shown below. Here only those parts whose price is greater than or equal to 0.15 and whose weight exceeds 2 will be selected.
partsStream.map(mapper).
filter(pricePredicate).
filter(weightPredicate).
forEach(System.out::println);
The output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Name: Eraser Weight: 3 Part Number: 200 Price: 0.26 Quantity: 250
Using the Match Type Methods
There are three match type methods that will effectively act as a filter operation against a stream. Each of these methods accepts a predicate that is applied against each element of the stream and returns a boolean value reflecting the success of the predicate. These include:
- allMatch – Returns true if all of the elements satisfy the predicate or if the stream is empty. Otherwise it returns false.
- anyMatch – Returns true if any element of the stream matches the predicate. Returns false if the stream is empty or no elements are found that match the predicate.
- noneMatch – Returns a boolean value indicating whether any elements of the stream match the predicate.
Below, a stream is created based on the partsList and is used with a combination of predicate lambda expressions. While the example seems to execute these methods one after another, remember that a stream cannot be reused. To make this example work, the stream has to be recreated before each execution of a match method.
Stream<Part> partsStream = partsList.stream();
System.out.println(partsStream.
allMatch(p -> p.getQuantity()<50));
System.out.println(partsStream.
anyMatch(p -> p.getQuantity()<50));
System.out.println(partsStream.
noneMatch(p -> p.getQuantity()<50));
System.out.println(partsStream.
allMatch(p -> p.getWeight()>50));
System.out.println(partsStream.
anyMatch(p -> p.getQuantity()>1000));
System.out.println(partsStream.
noneMatch(p -> p.getPrice()>.35f));
The output of this sequence follows:
false
false
true
false
true
true
Using the findFirst Method
The findFirst method will return the first element of a Stream. However, the stream may be empty. To handle this situation, the findFirst method returns an Optional object. This object may or may not contain a null value. The isPresent method is used to determine if it contains a value or not. The Optional’s get method returns the value or null if none are present.
In the following sequence, the first part in the stream that is greater than or equal to 0.15 will be returned and then displayed.
Stream<Part> partsStream = partsList.stream();
Predicate<Part> pricePredicate = p -> p.getPrice() > 0.15f;
Part firstPart =
partsStream.filter(pricePredicate).
findFirst().
get();
System.out.println(firstPart);
The output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Using the findAny Method
The findAny method also returns an Optional value. This value may be empty or it may contain any element of the stream. The element selected is nondeterministic; meaning the element selected cannot be predicted.
The following example uses the previously defined partsList and pricePredicate. A stream is created and the predicate and findAny method is executed against it.
partsStream = partsList.stream();
Part anyPart =
partsStream.
filter(pricePredicate).
findAny().
get();
System.out.println(anyPart);
The output may appear as follows:
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
Using the Optional Class
The previous example assumes that a Part will be returned. A more robust technique to handle null values follows using the Optional class’ isPresent method:
Optional<Part> firstPart = partsStream.map(mapper).
filter(pricePredicate).
findFirst();
if (firstPart.isPresent()) {
System.out.println(firstPart.get());
} else {
System.out.println(
"No part meets the criteria specified");
}
This will duplicate the output of the earlier example.
The Optional class has several methods. One of its methods, orElse, will return a specific object if the value is null. In this example, the pricePredicate criteria is changed to filter out parts whose price is less than or equal to 0.3. For the partsStream, this eliminates all of the parts. The orElse method will return an object specified by its argument. Since firstPart will be empty, a new substitute part is created.
pricePredicate = p -> p.getPrice() > 0.3f;
firstPart = partsStream.
map(mapper).
filter(pricePredicate).findFirst();
Part part = firstPart.
orElse(new Part("Substitute", 99999, 10, 1.5f, 50));
System.out.println(part);
The output will be the substitute part:
Name: Substitute Weight: 10 Part Number: 99999 Price: 1.50 Quantity: 50
The Optional class also has an orElseGet method that returns an object if the Optional value is null. The argument of the method is a Supplier implementation. The next example uses a random number to select one of the partsList elements to return.
pricePredicate = p -> p.getPrice() > 0.3f;
Supplier<Part> supplier = () -> {
Random random = new Random();
return partsList.get(random.nextInt(partsList.size()));
};
Part part = firstPart.orElseGet(supplier);
System.out.println(part);
One possible output follows:
Name: Pencil Weight: 5 Part Number: 100 Price: 0.16 Quantity: 500
Another potentially useful Optional class method is the orElseThrow method. As the name implies, it will throw an exception if the Optional value is null. The argument of the method should be the exception to throw.
The following illustrates this method where a PartException is thrown. The nextInt’s argument has been set to 200 to increase the likelihood that an exception will be thrown. The exception is thrown when the index into partsList is too large.
class PartException extends Exception {
}
Supplier<Part> supplier = () -> {
Random random = new Random();
return partsList.get(random.nextInt(200));
};
try {
Part part = firstPart.orElseThrow(PartException::new);
System.out.println(part);
} catch (PartException ex) {
ex.printStackTrace();
}
If the get method’s argument exceeds the size of the partsList, the PartException will be thrown.
Using the reduce Method
The reduce method, as its name implies, will reduce a stream to a single value and returns it as Optional. There are three overloaded reduce methods. We will demonstrate two of these methods here.
The following example uses a BinaryOperator to perform the reduction. A stream is created from a list of integers. The reduce method reduces two arguments to their sum. This operation is applied consecutively to each element of the stream. The return value is an Optional. Its get method is used to return the effective sum of the elements of the stream.
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Optional<Integer> result = list.stream().
reduce((n1, n2) -> n1 + n2);
System.out.println(result.get());
The output of this sequence will be a 10.
The next example illustrates the same technique applied to strings. The effect is to concatenate the elements together separated by a dash.
List<String> nameList =
Arrays.asList("Bob", "Carol", "Ted", "Alice");
Optional<String> name = nameList.stream().
reduce((n1, n2) -> n1 + "-" + n2);
System.out.println(name.get());
The output of this example follows:
Bob-Carol-Ted-Alice
The following reduce method performs a similar summation operation but is based on an initial accumulator value as the first argument of the method. The second argument is a BinaryOperator that is applied to the elements of the stream. The effect of this example is to compute the product of the elements.
List<Integer> productList = Arrays.asList(1, 2, 3, 4);
Integer productResult = productList.
stream().
reduce(1, (n1, n2) -> n1 * n2);
System.out.println(productResult);
Its output will be 24.
Using the Collector Interface and the collect Method
The Collector interface is an abstract version of a reduce type operation. The reduction operation results in a mutable result. This can be performed in either a sequential or parallel manner. However, creating a Collector can be difficult. This task is made easier by the java.util.stream.Collectors class. It is a utility class whose methods return Collector objects and supports a number of reduction type operations.
The Stream interface’s collect method is useful when the elements of a stream need to be placed into a Collection, Map, or String. It frequently uses an instance of the Collector interface to gather elements of a stream.
There are two overloaded collect methods available. The first accepts a Collector and the second accepts three arguments: a Supplier and two BiConsumer objects. Let’s examine the use of a Collector first.
In this example, a stream of Parts is created. The map method is used to obtain the name of each part. This is followed by the use of the Collectors utility class’ toList method to create the actual list. The list is then assigned to the partList variable.
List<Part> partsList = Arrays.asList(
new Part("Pencil", 100, 5, 0.15f, 500),
new Part("Eraser", 200, 3, 0.25f, 250),
new Part("Pen", 500, 3, 0.35f, 350),
new Part("Paper", 2000, 1, 0.03f, 1200)
);
Stream<Part> partsStream = partsList.stream();
List<String> partList =
partsStream.
map(Part::getName).
collect(Collectors.toList());
for (String element : partList) {
System.out.println(element);
}
The output of this sequence will appear as shown below:
Pencil
Eraser
Pen
Paper
Another use of the Collectors class is to generate a set of values. In the following example a TreeSet of strings are created based on the name of the part.
partsStream = partsList.stream();
TreeSet<String> partTree =
partsStream.
map(Part::getName).
collect(Collectors.toCollection(TreeSet::new));
partTree.stream().forEach(p -> System.out.println(p));
The output of this sequence follows:
Eraser
Paper
Pen
Pencil
The creation and use of a Map is shown below. A map of a list of parts grouped by their weights is created. The contains method is used to determine if a grouping, where the weight is 3, exists. This list is then displayed.
partsStream = partsList.stream();
Map<Integer,List<Part>> partMap =
partsStream.
collect(Collectors.groupingBy(Part::getWeight));
if(partMap.containsKey(3)) {
partMap.get(3).forEach(p->System.out.println(p));
}
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
Name: Pen Weight: 3 Part Number: 500 Price: 0.35 Quantity: 350
Using the Collects Class
The Collects class possesses a number of useful methods for creating Collector objects from a stream. The types of operations available include:
- Averaging primitives
- Counting the number of elements in a stream
- Grouping by some value
- Joining elements into a string
- Mapping the element of a stream
- Producing a maximal or minimal element
- Partitioning a stream into a map
- Reducing elements of a stream
- Summing the elements of a stream or producing summary statistics for the stream
- Producing a Collector that contains a List, Map, or Set
While not demonstrated here, we will demonstrate some of these methods in later sections.
Using the iterator Method
The Iterator method is found in the Stream interface’s base interface BaseStream. Its intent is to provide a way of obtaining an Iterator object that can be used to iterate through the stream. This should not normally be needed as the Stream interface’s methods provide better support than the older approach.
The following example illustrates how the method can be used with a stream of integers. The first part shows the use of the iterator method and uses it to display the elements of the list in a while loop. The last two statements demonstrate how much easier it is to use the forEach method to achieve the same result.
List<Integer> numbers = Arrays.asList(10,20,30,40);
Stream<Integer> numberStream = numbers.stream();
Iterator iterator = numberStream.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
numberStream = numbers.stream();
numberStream.forEach(n -> System.out.print(n + " "));
The output of both techniques is identical and is shown below:
10 20 30 40
Using the sorted Method
The sorted method will return a stream that is sorted. There are two overloaded sort methods. If the stream executed against the element type supports a natural order, the sort method with no arguments can be used. For example, as shown below, a list of numbers is created, sorted, and then displayed:
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24);
numberList.stream().
sorted().
forEach(n -> System.out.print(n + " "));
The output is as follows:
5 12 23 24 54
If, however, the element does not have a natural order, or a different comparison technique is desired, then the second overloaded sort method can be used as shown below. This method takes a Comparator as its argument. In this example the parts are sorted by name in ascending order.
Stream<Part> partsStream = partsList.stream();
Comparator<Part> partComparator =
(p1, p2) -> p1.getName().compareTo(p2.getName());
partsStream.sorted(partComparator).
forEach(System.out::println);
The output of this sequence follows:
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25 Quantity: 250
Name: Paper Weight: 1 Part Number: 2000 Price: 0.03 Quantity: 1200
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15 Quantity: 500
Using the distinct Method
The distinct method is used to return a stream where any duplicate elements of the original stream are removed. Duplicate elements are determined by applying the equals method to the elements. The following code sequence using this method:
List<Integer> numbers =
Arrays.asList(19, 12, 23, 31, 12, 4, 23, 31);
numbers.stream().
distinct().
forEach(n -> System.out.print(n + " "));
The output is shown below:
19 12 23 31 4
Using the limit Method
Sometimes it is desirable to limit the number of elements processed in a stream. The limit method is used for this purpose. Its long argument determines how many elements will be returned.
The following example restricts output to the top 5 numbers in a list. A lambda expression is used to sort the numbers from high to low and then the top five numbers are displayed.
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24, 32, 89, 21, 83);
numberList.stream().
sorted((n1, n2) -> (n1>n2)?-1:1).
limit(5).
forEach(n -> System.out.print(n + " "));
The output follows:
89 83 54 32 24
The following example will return a list of numbers where the three largest and the three smallest numbers have been removed. This list will be in descending order. The stream, tmpStream, is created - first sorting the numbers in ascending order and then removing the last three elements using the limit method. The tmpStream is then used to create a stream in descending order and once again the limit method will cut off the last three elements of the stream.
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24, 32, 89, 21, 83);
Stream<Integer> tmpStream= numberList.stream().
sorted((n1, n2) -> (n1>n2)?1:-1).
limit(numberList.size() - 3);
tmpStream.
sorted((n1, n2) -> (n1>n2)?-1:1).
limit(numberList.size() - 6).
forEach(n -> System.out.print(n + " "));
The output will be as follows:
32 24 23
Using the skip Method
The skip method will skip over the elements at the beginning of a stream. The number of elements skipped is specified by its long argument.
The following example will duplicate the previous example but it uses the skip method instead. It will return a list of numbers where the three largest and the three smallest numbers have been removed. This list will be in descending order. The stream, tmpStream, is created first sorting the numbers in ascending order and then removing the first three elements using the skip method. The tmpStream is then used to create a stream in descending order and once again the skip method will remove the first three elements of the stream.
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24, 32, 89, 21, 83);
Stream<Integer> tmpStream = numberList.stream().
sorted((n1, n2) -> (n1>n2)?1:-1).
skip(3);
tmpStream.
sorted((n1, n2) -> (n1>n2)?-1:1).
skip(3).
forEach(n -> System.out.print(n + " "));
The output will be as follows:
32 24 23
Using the max, min, and count Methods
The max and min methods will return the largest and smallest elements, respectfully, from a stream as determined by their Comparator argument. These methods are easy to demonstrate using a list of integers as shown below. Lambda expressions are used for the Comparator to determine the relative ranking of the integers. These methods return an Optional, so the get method is used to return the integer value.
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24, 32, 89, 21, 83);
System.out.println(numberList.stream().
max((n1, n2) -> (n1>n2)?1:-1).get());
System.out.println(numberList.stream().
min((n1, n2) -> (n1>n2)?1:-1).get());
The first println statement will return 89 while the second returns 5.
The count method will return a long value reflecting the number of elements in the stream. A simple example follows:
List<Integer> numberList =
Arrays.asList(23, 54, 12, 5, 24, 32, 89, 21, 83);
System.out.println(numberList.stream().count());
A 9 will be displayed.
Streams for Primitive Types
The Stream interface is a generic type of interface and is useful for many types of problems. When a stream needs to be used with a primitive, a lot of boxing and unboxing is required. As a result, when a large amount of data needs to be processed, using the Stream interface may not be the best solution.
To address this problem, several stream interfaces designed to work with primitive data have been provided. These include: IntStream, LongStream and DoubleStream. These are often returned when mapping from a generic class to a primitive data type. Several functional interfaces have been added to support this type of conversion including ToIntFunction, IntConsumer, and IntSupplier. Other primitive stream interfaces, such as for boolean and short, were not added. It was felt that the three primitive streams were adequate and other primitives could be addressed using other techniques.
Primitive stream interfaces possess a number of common operations including, among others:
- count
- sum
- min
- max
- mean
There is also the IntSummaryStatistics class that provides summary statistics on a stream of integers. In addition, there are corresponding DoubleSummaryStatistics and LongSummaryStatistics classes.
Using the IntStream Interface
The IntStream interface supports operations using integers and possesses a number of useful methods. The following illustrates an implicit use of the IntStream interface. A stream of parts is created and the mapToInt method is applied against this. This will return an IntStream object. The sum method is then applied to the new primitive stream and returns the sum of the weights of all of the parts in the stream.
Stream<Part> partsStream = partsList.stream();
int sumOfWeights = partsStream.mapToInt(
(Part p) -> p.getWeight()).sum();
System.out.println("Sum: " + sumOfWeights);
The output will be as follows:
Sum: 9
The following explicitly uses an IntStream to achieve the same results:
IntStream tmpStream = partsStream.mapToInt(
(Part p) -> p.getWeight());
System.out.println("Sum: " + tmpStream.sum());
Understanding Lazy and Eager Evaluation
Streams are evaluated in a lazy fashion. This means that most operations against streams are deferred until results are actually needed. Without lazy evaluation, streams could not be infinite.
For example, consider the earlier findFirst example duplicated below. The map and filter operations process only one element at a time until the first match occurs. The entire stream is not processed. Intermediate methods are not executed until some terminal method is called.
Optional<Part> firstPart = partsStream.map(mapper).
filter(pricePredicate).findFirst();
An eager evaluation occurs when operations are performed on every element in a stream. Sometimes this is needed. For example, the forEach method processes every element in a stream.
There are times when it is better to process a stream in a lazy fashion and times when it needs to be processed eagerly. By moving looping operations inside streams and certain collections, the system is able to optimize these operations applying eagerness or laziness as appropriate.
Using Short-Circuit Methods
The evaluation of a stream is done in a short-circuit manner. An entire stream may not be processed but will terminate early when it can. Short-circuit methods include:
- anyMatch
- allMatch
- noneMatch
- findFirst
- findAny
- limit
- substream
Using Parallel Stream Operations
One of the benefits of using streams is their potential support of the concurrent execution of stream operations. Traditional collection methods are not always thread-safe. Often explicit thread synchronization mechanisms must be used to address the potential corruption of data. The use of synchronization can introduce thread contention issues. Parallel stream operations can avoid these issues if the collection being acted on is not modified.
Bear in mind that the concurrent execution of stream operations does not necessarily mean that an application will run faster. For small collections, the overhead involved in supporting parallel execution may be larger than the time required to perform the same set of operations in a sequential manner.
When a stream is parallelized, the run-time environment will split the stream into multiple parts, execute each part on a separate processor, and then combine the results. Operations on a stream are performed sequentially by default. To affect parallel execution either the Collection interface’s parallelStream method or the BaseStream interface’s parallel method needs to be used.
Let’s start with the following declarations of a list and stream:
List<Part> partsList = Arrays.asList(
new Part("Pencil", 100, 5, 0.15f),
new Part("Eraser", 200, 3, 0.25f),
new Part("Paper", 2000, 1, 0.03f),
new Part("Pen", 200, 3, 0.25f),
new Part("Staple", 3000, 1, 0.01f)
);
Stream<Part> partsStream = partsList.stream();
A sequential stream that filters out those parts whose weight is less than 3 is shown below:
partsStream.filter(
(p)->p.getWeight()>=3).
forEach(p -> System.out.println(p));
The same operation performed, using a parallel stream, uses the parallel method as seen here:
partsStream.parallel().
filter((p)->p.getWeight()>=3).
forEach(p -> System.out.println(p));
The output of both of these sequences follows and is identical for each example:
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25
Name: Pencil Weight: 5 Part Number: 100 Price: 0.15
However, converting a sequential stream to a parallel stream is not always this simple. Reduction type operations need to be handled more carefully. In the following example a Map is created whose key is an integer weight. The groupingBy method will return a list whose members have the same weight. These lists are then assigned to the map.
Map<Integer, List<Part>> groupByWeight =
partsStream.collect(
Collectors.groupingBy(Part::getWeight));
for(Part part : groupByWeight.get(3)) {
System.out.println(part);
}
The output follows:
Name: Eraser Weight: 3 Part Number: 200 Price: 0.25
Name: Pen Weight: 3 Part Number: 200 Price: 0.25
To achieve the same results using a parallel stream requires a couple of modifications as shown below. To improve the performance of the operation, a ConcurrentMap is needed and the groupingByConcurrent method should be used.
ConcurrentMap<Integer, List<Part>> groupByWeight =
partsStream.parallel().collect(
Collectors.groupingByConcurrent(Part::getWeight));
for (Part part : groupByWeight.get(3)) {
System.out.println(part);
}
To affect concurrent reduction, the following rules need to be considered:
- The stream is a parallel stream
- The collector used has the characteristic: CONCURRENT
- The stream should be unordered
For a stream to take advantage of multiple processors, a parallel stream needs to be used. However, when using a parallel stream:
- Be aware of adverse side effects
- Avoid stateful lambda expressions
A side effect can occur in a stream when one of its operations modifies the state of the application. Operations such as the println method and mutable reductions have side effects. Most side effects are handled within a stream. However, problems can occur when a stream operation modifies the source of the stream. This is called interference.
In the following example, an attempt is made to modify the original partsList, using the add method as part of the predicate operation:
partsList.stream().
filter((p)->{
partsList.add(p);
return p.getWeight()>=3;}).
forEach(p -> System.out.println(p));
This will generate a java.lang.UnsupportedOperationException exception when the statement is executed. Modifying a different list will not result in an exception.
A stateful lambda expression is one where the results are dependent on a state that might change during the execution of the lambda expressions. For example, the value returned by the following filter operation depends on some external condition:
boolean someCondition = …;
partsStream.parallel().
filter(p -> {
if (someCondition) {
return true;
} else {
return false;
}
}).
forEach(p -> System.out.println(p));
Conclusion
A stream is a collection of elements. The elements of a stream can be of any type. Operations are performed on its elements in a pipeline fashion. This can result in a more natural and concise solution to a problem. In addition, when executed as a parallel stream it will execute faster.
A stream must first be created. A stream can be created from multiple sources including arrays and lists. The length of a stream can be unbounded but once a stream has been processed it cannot be reused.
There are numerous operations that can be applied against the elements of a stream. These include operations such as transforming an element, filtering elements, reducing elements, or limiting the size of the stream. Operations can be chained together to create a pipeline. In addition, lambda expressions are frequently used with streams to improve the flexibility and expressiveness of streams.
When a stream is parallelized, care must be taken to ensure that it will function as intended. Any collectors used must be concurrent and the stream should be unordered. In the next chapter we will examine the improvements to how date and time are handled.
* * * * *