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();
3. Stream over an array
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.
stream pipelineSource 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.

A stream pipeline could have zero or more non-terminal operations but it should have atleast 1 terminal operation.

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.

Terminal operations
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.

4. allMatch()
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.

Leave a Reply