What is a java stream
A java 8 stream is a sequence of elements usually from a collection. Collection may be an array, list, set, file or a sequence of elements.
A stream can perform different operations on its elements. These operations include iterating over its elements, counting total number of elements, filter some elements based on some condition, transform elements etc.
A java stream is represented by java.util.stream.Stream
interface and its implementation classes. Java stream api was introduced in java 8.
This java stream tutorial will explain how to create streams in different ways, how to iterate a stream and stream operations in detail with examples.
Create stream
There are 7 different methods of creating a stream as described ahead.
1. Using Stream.of()
java.util.stream.Stream
contains a static interface method of which accepts an array or a variable number of arguments and returns a stream of those arguments.
Argument values comprise the elements of the stream as shown below.
// create an array Integer[] arr = {1, 2, 3, 4}; // create a stream Stream<Integer> stream = Stream.of(arr);
Above code can also be shortened to
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
Signature of of()
method is
static <V> Stream<V> of(V... values)
where V is a generic type.
2. Stream over a collection
java.util.Collection
interface has a stream()
method which returns a stream of elements over the collection on which it is invoked.
stream()
is a default interface method added in java 8 to the Collection interface.
Since List and Set implement this interface, this method is available to them.
Creating a stream on a List is shown below.
//create a list List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); // create a stream Stream<Integer> stream = list.stream();
By default,
Stream.of()
method described above returns a stream over an array but you can also create an array stream using stream
method java.util.Arrays
class which accepts an array as argument as shown below.
// create an array Integer[] arr = {1, 2, 3, 4}; // create a stream Stream<Integer> stream = Arrays.stream(arr);
4. Using Stream builder
A stream can be created using a builder. In order to get a stream builder, invoke the static builder()
method.
This method returns an object of type java.util.stream.Stream.Builder
.
This builder object is then used to add elements to the stream using add
method and then create the stream using build()
method as shown below.
// create a builder Builder<Integer> builder = Stream.<Integer>builder(); // add elements builder.add(3).add(4); // create stream Stream<Integer> s = builder.build();
Above code can be reduced to a one-liner
Stream<Integer> s = Stream.<Integer>builder().add(3).add(4).build();
5. Empty stream
An empty stream does not contain any elements. It can be created using empty()
method from java.util.stream.Stream
interface.
empty
is once again a static interface method and returns a stream object with no elements.
Empty stream is generally used to return an object from a method that returns a stream instead of returning null
so as to avoid NullPointerException later as shown below.
Stream<Integer> getStream(Integer[] arr) { return arr.length > 0 ? Arrays.stream(arr) : Stream.empty(); }
6. From a file
java.nio.file.Files
class has a static lines()
method which accepts a file path returns a stream. This stream can then be iterated to read the contents of file line by line as shown below.
Stream<String> stream = Files.lines(Paths.get(filePath)); stream.forEach((line) -> System.out.println(line));7. Generate stream
A stream of a specific size can also be generated using static
generate()
method from java.util.stream.Stream
interface.Below example generates a stream of 10 integers. Integers are random numbers generated between a range.
Random random = new Random(); Stream<Integer> stream = Stream.generate(() -> { // generate random numbers between 0 and 99 return random.nextInt(100); }).limit(10);
Note that generate()
accepts an argument of type java.util.function.Supplier which is a functional interface and hence can be implemented using a Lambda expression as shown above.
Iterating a stream
A stream can be iterated using forEach() method. This method accepts an argument of type java.util.function.Consumer
interface.
This is a functional interface having a single method accept()
that takes an argument but returns nothing.
Below example shows how to iterate an array of integers using forEach()
method.
// array Double[] nums = {22.4, 34.7, 1.2, 0.4}; // get stream Stream<Double> stream = Arrays.stream(ages); // iterate stream stream.forEach(v -> System.out.println(v));
forEach
is a terminal operation. You will learn more about terminal operation in the coming sections.
Stream pipeline
A stream can be visualized as a pipeline with source of elements at one end and a result at the other end with different operations in between as depicted below.
Source could be any one of the ways of stream creation explained in the last section. There are many different operations that can be applied on a stream classified as Terminal and Non-terminal covered in following section.
Stream operations
A stream can be processed to perform a varying number of operations. All the operations on a stream can be categorized into following two categories.
Non-terminal operations
These operations are used to modify or remove or transform stream elements based on some condition.
All non-terminal operations return another stream with modified elements.
This is the reason that multiple terminal operations can be chained together. Non-terminal operations are also called intermediate operations.
Following are some non-terminal operations that can be performed on a stream.
1. filter()
Stream elements can be filtered or removed based on a certain condition using filter operation. This operation is applied using filter()
method which accepts an argument of type java.util.function.Predicate
.
This is a functional interface having a method test()
which accepts a single value and tests it for some condition. It returns true
if the value passes the test, false
otherwise.
Example of filter operation on stream is given below.
// array of ages Integer [] ages = {24, 43, 32, 68, 61, 29, 33}; // get stream over array Stream<Integer> stream = Arrays.stream(ages); // filter out ages more than 60 stream.filter((v) -> v < 60).forEach(v -> System.out.println(v));
Above example filters integer array and allows only those elements that are greater than 60. Notice the argument to filter()
method is a Lambda expression as an implementation of test()
method of java.util.function.Predicate
interface.
The lambda expression returns true
for elements that are less than 60, thus allowing to pass the test and elements greater than 60 are filtered out.
2. map()
Map operation is used to transform the elements of a stream. Map operation is applied using map()
method.
This method accepts a function that is applied to its argument and returns a new stream with modified elements.
Suppose you want to calculate square of each element of an array. Application of map()
method for this is shown below.
Integer [] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(ages); // calculate square of stream elements stream.map(v -> v * v).forEach(v -> System.out.println(v));
map()
may also be applied on objects to return a customized value.
Below example creates an array of student objects and uses map()
to return only the name of students.
import java.util.stream.Stream; class Student { int roll; String name; public Student(int r, String n) { roll = r; name = n; } } public class MapStreamExample { public static void main(String[] args) { // create student objects Student s1 = new Student(1,"A"); Student s2 = new Student(2,"B"); // create array Student[] arr = {s1,s2}; // get stream of students Stream<Student> stream = Arrays.stream(arr); // get only names of students stream.map(st -> st.name).forEach(v -> System.out.println(v)); } }
3. distinct()
As name suggests, this intermediate operation removes duplicate elements from the stream.
This method does not accept any arguments and returns a new stream with the duplicate elements removed. Example,
Integer [] nums = {24, 43, 32, 24, 68}; Stream<Integer> stream = Arrays.stream(ages); // fetch unique elements stream.distinct().forEach(v->System.out.println(v));
Above code removes duplicate element(24) from the array.
4. peek()
This method returns the elements of the stream as it is without any modification or filtering. It accepts a java.util.function.Consumer
as argument and returns a new stream.
Since a consumer can not return a value, peek()
can not modify the value of elements.
Peek is an intermediate operation which is used for debugging to look at the elements of a stream as they flow through the stream pipeline during multiple intermediate operations. Example,
Integer [] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(ages); // calculate square of stream elements stream.filter(v -> v <20). peek(v -> System.out.println(v)). map(v -> v * v).forEach(v -> System.out.println(v));
5. limit()
This terminal operation is used to reduce or limit the number of elements in a stream.
limit()
method takes an integer as argument and limits the count of elements in the stream to this integer. Example,
Integer [] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(numbers); // only 2 elements stream.limit(2).forEach(v -> System.out.println(v));
Above example will print the first two stream elements.
6. flatMap()
Flatmap non-terminal operation is used to generate a stream whose each element is another stream produced by applying an operation on each element.
It is used to join elements of multiple streams together in a single stream. Final stream contains the contents of the streams of all its elements.
Flat map operation is performed by using flatMap()
method. flatMap()
accepts an argument of type java.util.Function
interface.
This interface has a single method apply()
which accepts an argument and returns a value. Thus, apply()
can be used as a mapping function that is applied to its argument.
Signature of flatMap()
is as below.
Stream flatMap(Function mapper);
Scenarios where flatMap()
is useful are:
- Extracting words from lines of a file or a list/array of strings.
- Converting a multi-dimensional array to single dimension.
- Collect elements of multiple lists in one list.
Example of flatMap()
to get words from lines of file is given below.
// get lines from file Stream<String> lines = Files.lines(Paths.get("e://testfile.txt")); // get stream of words from lines Stream<String> words = lines. flatMap(line -> Stream.of(line.split(" "))); // iterate over word stream words.forEach(w -> System.out.println(w));
In the above example, flatMap()
is invoked on the lines of file. Implementation of java.util.function.Function
is supplied as a Lambda expression to flatMap()
.
This expression splits each line on a blank space and converts the resulting array into a stream of words. Thus, final stream returned by flatMap()
is a stream of words of lines in file.
Summary of all the non-terminal or intermediate stream operations is summarized below.
Name | Description | Signature |
---|---|---|
filter() | Select elements from stream based on a condition. | long count(); |
map() | Transform stream elements to return different values. | Stream<T> map(Function mapper); |
distinct() | Remove duplicate elements from stream. | Stream<T> distinct(); |
peek() | Used to look at stream elements. Generally used for debugging between intermediate operations. | Stream<T> peek(Consumer c) |
limit() | Restrict the count of stream elements. | Stream<T> limit(long size) |
flatMap() | Returns a stream whose each element is a stream produced by applying a function to each element. | Stream flatMap(Function mapper); |
Here, T is the generic type.
As the name suggests, these operations are applied at the end of the stream. In other words, applying these operations terminates the stream.
These operations return a single result and after a terminal operation is applied, no other operation can be applied over the stream, neither the stream can be re-used.
Trying to use the stream after a terminal operation has been applied will result in
java.lang.IllegalStateException: stream has already been operated upon or closed
Below is a list of terminal stream operations.
1. count()
It returns the total number of elements in the underlying stream. Internally count()
performs iteration of stream elements. Example,
Integer [] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(numbers); System.out.println(stream.count()); // prints 4
2. collect()
This method iterates over the elements of the stream and puts them into a collection. The collection is specified as an argument to this method using java.util.stream.Collectors
class. Example,
Integer[] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(numbers); List<Integer> list = Stream.of(numbers). map(n -> n * n).collect(Collectors.toList());
Above example calculates the square of array elements and adds them to a list. It is also possible to convert array to a list using collect by simply removing the map()
intermediate operation.
collect()
method can also be used to convert an array to a set by using toSet
method of Collectors as shown below.
Integer[] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(numbers); Set<Integer> set = Stream.of(numbers).collect(Collectors.toSet());
3. anyMatch()
anyMatch()
method accepts an argument of type java.util.function.Predicate
which is a test condition.
It applies the given test to all the elements of stream and returns true
if any element matches the condition and false
if no elements meet the condition.
anyMatch()
returns true
for the first matching element, it does not check further elements.
It can be used to search or check if an element exists in an array. Example,
class Student { int roll; String name; public Student(int r, String n) { roll = r; name = n; } } public class Main { public static void main(String[] args) { Student s1 = new Student(1,"A"); Student s2 = new Student(2,"B"); Student[] attendance = {s1, s2}; // check if student is absent boolean isPresent = Arrays.stream(s). map(st -> st.roll). anyMatch(rollNum -> rollNum == 3); System.out.println(isPresent ? "Present" : "Absent"); } }
Above example creates an array of students and checks if a student with roll number 3 is present or not.
Note the usage of map()
method to return roll number from student object.
Also note that an operation receives the return value from the previous intermediate operation and not the actual stream element.
If that would not be the case, anyMatch
should have received an object instead of an integer.
anyMatch()
will return true
if all the stream elements match the given condition and false
if even a single element does not meet the condition. Example,
Integer [] numbers = {5, 10, 15, 30}; boolean multiples = Stream.of(numbers).allMatch(e -> e % 5 == 0); System.out.println(multiples ? "All multiples of 5" : "All are not multiples of 5");
Above code checks if all array elements are multiples of 5.
5. noneMatch()
This method also accepts an argument of type java.util.function.Predicate
and returns true
if all the elements do not match the given condition and false
if a single element matches the condition or the stream is empty.
noneMatch()
works opposite to allMatch()
. Example,
String[] arr = {"bowler", "orange", "round"}; // check for string less than 5 characters boolean lengthy = Arrays.stream(arr). noneMatch(str -> str.length() < 5); // returns true System.out.println(lengthy ? "No lesser than 5" : "Found lesser than 5");
In the above example, noneMatch()
checks the length of array elements and returns true if all the elements are greater than 5 characters.
6. findFirst()
This method returns the first element of the stream. It returns an object of java Optional which is empty if the stream contains no element.
If the stream has at least one element, then the first element can be retrieved using get()
method of returned optional object. Example,
String[] arr = {"bowler", "orange", "round"}; Optional<String> element = Arrays.stream(arr).findFirst(); System.out.println("First element is: " + element.get());
7. findAny()
This method returns a random element from the stream. findAny()
also returns a java.util.Optional
object.
It is empty if the stream does not contain any elements otherwise, use its get method for getting the value of element. Example,
String[] arr = {"bowler", "orange", "round"}; // random element Optional<String> element = Arrays.stream(arr).findAny(); System.out.println("Any element is: " + element.get());8. reduce()
This terminal operation is used to perform an operation on the elements of the stream to return a single value.
This method may be used to calculate the sum of elements of an integer array or to print an array meaningfully as shown below.
String[] arr = { "bowler", "orange", "round" }; Optional<String> element = Arrays.stream(arr). reduce((a, b) -> a + ", " + b); System.out.print(element.get());
reduce()
method in this example accepts an argument of type java.util.function.BinaryOperator
which is also a functional interface having apply()
method.
This method accepts two arguments and returns a value as implemented in the above example using a Lambda expression.
Above code snippet will print
bowler, orange, round
reduce()
method example for calculating the sum of array elements is given below.
Integer[] numbers = { 5, 10, 15, 30 }; Integer sum = Stream.of(numbers).reduce(0, (a, b) -> a+b); System.out.println(sum); // prints 60
This example uses an overloadedreduce()
method which accepts an identity value and an object of type java.util.function.BinaryOperator
as arguments and returns a single result after applying the operation on stream elements.
Identity value is 0 for addition and subtraction, 1 for multiplication and division.
9. max()
max()
method is used to return maximum value from among stream elements.
In order to determine maximum element, max accepts and object of java.util.Comparator
interface as argument. Example,
Integer[] numbers = { 5, 10, 15, 30 }; Integer max = Stream.of(numbers).max((v1, v2) -> v1 - v2).get(); // 30
max()
returns an Optional
, hence use its get
method to get the result.
10. min()
Similar to max()
, min()
method is used to determine minimum value from stream of elements. It also uses a comparator to determine minimum value. Example,
Integer[] numbers = { 5, 10, 15, 30 }; Integer max = Stream.of(numbers).min((v1, v2) -> v1 - v2).get(); // 5
Below is a summary of terminal operations of a stream.
Name | Description | Signature |
---|---|---|
count() | Calculates the number of elements in stream. | long count(); |
collect() | Add stream elements to a collection. | T collect(Collector collector); |
anyMatch() | Checks stream elements for a matching condition. Returns true if a single element matches. |
boolean anyMatch(Predicate p); |
allMatch() | Checks stream elements for a matching condition. Returns true if all element match the condition. |
boolean allMatch(Predicate p); |
noneMatch() | Checks stream elements for a matching condition. Returns true if all elements do not match the condition. |
boolean noneMatch(Predicate p); |
findFirst() | Returns the first stream element. Element can be retrieved using get() method of Optional . |
Optional<T> findFirst(); |
findAny() | Returns a random stream element. Element can be retrieved using get() method of Optional . |
Optional<T> findAny(); |
reduce() | Used to convert the stream to a single value. | 1. Optional<T> reduce(BinaryOperator b); 2. Optional<T> reduce(T identity, BinaryOperator b); |
max() | Find maximum element from the stream. Max value depends on the comparator implementation supplied as argument. | Optional<T> max(Comparator c); |
min() | Find minimum element from the stream. Min value depends on the comparator implementation supplied as argument. | Optional<T> min(Comparator c); |
Here, T is the generic type.
Streams are lazy
Streams add processing to data structures but this processing is only performed when required. This means that intermediate operations are performed only when a terminal operation is used.
If there is no terminal operation, then no action is performed on the stream. Thus, streams are lazy. Example,
Integer [] numbers = {5, 12, 15, 29}; Stream<Integer> stream = Arrays.stream(ages); // calculate square of stream elements stream.filter(v -> v <20).map(v -> v * v);
Above code example applies two operations filter and map on the stream but no terminal operation. Thus, no processing is performed.
Also, intermediate operations are only performed as required. Example,
if you use filter()
, map()
and findAny()
over a stream of 100 numbers, then filter()
and map()
will not be executed 100 times, only 1 time.
This is because findAny()
terminal operation returns a random element and will be completed after first invocation, thus eliminating the need for executing filter()
and map()
once again.
Stream benefits
At first look, you may find streams as an overhead and unnecessary effort since one may achieve the same result without using stream.
But streams have following two important benefits.
- Streams make the code concise and cleaner. If you want to find smallest number in an integer list, then following code would be required.
int max = Integer.MIN_VALUE; for(int num : list) { if(num > max) { max = num; } }
Same can be achieved using stream as
list.stream().max((v1, v2) -> v1 – v2).get();
which is certainly cleaner.
- Streams become very useful when multiple intermediate operations are chained together to achieve a result.
Getting the same result without streams would become too complex.
That is all on streams introduced in java 8. Hit the clap if you found it useful.