In this post, we will see an in-depth overview of Java 8 streams with a lot of examples and exercises.
Table of Contents
Introduction
You may think that Stream must be similar to InputStream
or OutputStream
, but that’s not the case.
A Stream
represents a sequence of elements supporting sequential and parallel aggregate operations. Stream does not store data, it operates on source data structures such as List, Collection, Array etc.
Most stream operations accept functional interfaces that make it a perfect candidate for lambda expressions.
If you are not well versed with functional interfaces, lambda expressions, and method references, you may want to read the following tutorials before moving ahead.
Types of Stream operations
There are two types of Stream operations.
Intermediate operations:
return a stream that can be chained with other intermediate operations with dot .Terminal operations:
return void or non stream output.
Let’s understand with the help of simple example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package org.arpit.java2blog.stream; import java.util.Arrays; import java.util.List; public class StreamOperations { public static void main(String[] args) { List<String> stringList = Arrays.asList("John", "Martin", "Mary", "Steve"); stringList.stream() .map((s) -> s.toUpperCase()) .forEach(System.out::println); } } |
Output:
MARTIN
MARY
STEVE
Here,
To perform a computation, stream operations are built into a Stream pipeline. Stream pipeline
consists of:
source
zero or more intermediate operations
terminal operation
.
In our example, Stream pipeline consists of:
Source
: stringList
1 Intermediate operation
: Map
1 terminal operation
: forEach
The below diagram will make it more clear.
map
is intermediate operation and foreach
is terminal opertion.
Most stream operations accept parameters as that describes user-defined behaviour, such as lambda expression map((s)->s.toUpperCase())
is passed to map operation.
To get correct behavior, streams parameters should be:
non-interfering:
Stream source should not be modified while execution of Stream pipline. You can learn more about interference.
Stateless:
In most cases, lambda expressions should be stateless. Its output should not depend on state that might change during execution of Stream pipeline. I have already covered stateful lambda expression in Parallel Stream tutorial.
Stream creation
There are multiple ways to create the Stream.
Empty Stream
empty()
method can be used to create an empty stream.
1 2 3 |
Stream s = Stream.empty() |
It is generally used to return Stream with zero elements rather than null.
Collection Stream
Stream can be created from Collection by calling .stream()
or .parallelStream()
1 2 3 4 5 6 7 |
List stringList=Arrays.asList("Andy","Peter","Amy","Mary"); stringList.stream() .map((s)->s.toUpperCase()) .forEach(System.out::println); |
stringList.stream()
will return you regular object stream.
Stream.of
You don’t need to create collection to get a Stream. You can also use .of()
1 2 3 |
Stream streamArray =Stream.of("X","Y","Z"); |
Stream.generate()
generate() method accepts Supplier for element generation. It creates infinite Stream and you can limit it by calling limit() function.
1 2 3 4 5 6 7 8 9 10 |
Stream<Integer> intStream=Stream.generate(() -> 1).limit(5); intStream.forEach(System.out::println); // Output // 1 // 1 // 1 // 1 // 1 |
This will create Integer stream with 10 elements with value 1.
Stream.iterate()
Stream.iterate() can also be used to generate infinite stream.
1 2 3 4 5 6 7 8 9 10 |
Stream<Integer> intStream = Stream.iterate(100 , n -> n+1).limit(5); intStream.forEach(System.out::println); // Output // 100 // 101 // 102 // 103 // 104 |
First parameter of iterate method represents first element of the Stream. All the following elements will be generated by lambda expression n->n+1
and limit() is used to convert infinite Stream to finite Stream with 5 elements.
Lazy evaluation
Streams are lazy; intermediate operation are not executed until terminal operation is encounterd.
Each intermediate operation generates a new stream, stores the provided operation or function. When terminal operation is invoked, stream pipeline execution starts and all the intermediate operations are executed one by one.
Let’s understand with the help of example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Stream<String> nameStream = Stream.of("mohan","john","vaibhav","amit"); Stream<String> nameStartJ = nameStream.map(String::toUpperCase) .peek( e -> System.out.println(e)) .filter(s -> s.startsWith("J")); System.out.println("Calling terminal operation: count"); long count = nameStartJ.count(); System.out.println("Count: "+ count); // Output // Calling terminal operation: count // MOHAN // JOHN // VAIBHAV // AMIT // Count: 1 |
In preceding output, you can see that unless and until terminal operation count is called, nothing was printed on console.
In the preceding example, peek() method is used to print the element of stream. peek()
method is generally used for logging and debugging purpose only.
Order of operations
Let’s see how stream processes the order of operations.
Could you guess output of the program?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Stream<String> nameStream = Stream.of("mohan","john","vaibhav","amit"); Stream<String> nameStartJ = nameStream.map( (s) -> { System.out.println("Map: "+s); return s.toUpperCase(); }) .filter( (s) -> { System.out.println("Filter: "+s); return s.startsWith("J"); } ); Optional<String> findAny = nameStartJ.findAny(); System.out.println("Final output: "+findAny.get()); |
Output will be:
Filter: MOHAN
Map: john
Filter: JOHN
JOHN
Here order of operations might be surprising. A common approach will be to perform intermediate operation on all elements and then perform next operation, but instead each element moves vertically.
This kind of behavior can reduce actual number of operation.
For example:
In preceding example, Strings vaibhav
and amit
did not go through map
and filter
operation as we already got result(findAny()
) with String john
.
Some of the intermediate operations such as sorted are executed on the entire collection. As succeding operations might depend on the result of sorted
operation.
Primitive Streams
Apart from regular Stream, Java 8 also provides primitive Stream for int, long and double.
Primitive Streams are:
- IntStream for int
- LongStream for long
- DoubleStream for double
All the primitive Streams are similar to regular Stream with following differences.
- It supports few terminal aggregate functions such
sum()
,average()
, etc. - It accepts specialized function interface such as IntPredicate instead of Predicate, IntConsumer instead of Consumer.
Here is an example of an IntStream.
1 2 3 4 5 6 7 8 |
int sum = Arrays.stream(new int[] {1,2,3}) .sum(); System.out.println(sum); // Output // 6 |
Convert Stream to IntStream
You may need to convert Stream to IntStream to perform terminal aggregate operations such as sum or average. You can use mapToInt()
, mapToLong()
or mapToDouble()
method to convert Stream to primitive Streams.
Here is an example:
1 2 3 4 5 6 7 8 |
Stream.of("10","20","30") .mapToInt(Integer::parseInt) .average() .ifPresent(System.out::println); // Output // 20.0 |
Convert IntStream to Stream
You may need to convert IntStream to Stream to use it as any other datatype. You can use mapToObj()
convert primitive Streams to regular Stream.
Here is an example:
1 2 3 4 5 6 7 8 |
String collect = IntStream.of(10,20,30) .mapToObj((i)->""+i) .collect(Collectors.joining("-")); System.out.println(collect); // Output // 10-20-30 |
Employee class
Consider a Employee
class which has two fields name
, age
, listOfCities
.
Here listOfCities
denotes cities in which Employee has lived so far.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
package org.arpit.java2blog.stream; import java.util.List; public class Employee implements Comparable<Employee>{ private String name; private int age; private List<String> listOfCities; public Employee(String name, int age,List<String> listOfCities) { super(); this.name = name; this.age = age; this.listOfCities=listOfCities; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public List<String> getListOfCities() { return listOfCities; } public void setListOfCities(List<String> listOfCities) { this.listOfCities = listOfCities; } @Override public String toString() { return "Employee [name=" + name + ", age=" + age + "]"; } @Override public int compareTo(Employee o) { return this.getName().compareTo(o.getName()); } } |
This Employee
class will be used in all succeeding examples.
Let’s create employeesList
on which we are going to perform intermediate and terminal operations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package org.arpit.java2blog.stream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class StreamGetListOfEmployees { public static void main(String[] args) { List<Employee> employeesList=getListOfEmployees(); // Write stream code here } public static List<Employee> getListOfEmployees() { List<Employee> listOfEmployees = new ArrayList<>(); Employee e1 = new Employee("Mohan", 24,Arrays.asList("Newyork","Banglore")); Employee e2 = new Employee("John", 27,Arrays.asList("Paris","London")); Employee e3 = new Employee("Vaibhav", 32,Arrays.asList("Pune","Seattle")); Employee e4 = new Employee("Amit", 22,Arrays.asList("Chennai","Hyderabad")); listOfEmployees.add(e1); listOfEmployees.add(e2); listOfEmployees.add(e3); listOfEmployees.add(e4); return listOfEmployees; } } |
Common intemediate operations
Map()
Map() operation is used to convert Stream<T> to Stream<R>. It produces one output result of type 'R'
for each input value of type 'T'
. It takes Function interface as parameter.
For example:
You have stream of list of employees and you need a list of employee names, you simply need to convert Stream
to Stream
.
1 2 3 4 5 6 7 8 9 |
List<String> employeeNames = employeesList.stream() .map(e -> e.getName()) .collect(Collectors.toList()); System.out.println(employeeNames); // Output // [Mohan, John, Vaibhav, Amit] |
You can also use map even if it produces result of same type.
In case, you want employee name in uppercase, you can use another map()
function to convert string to uppercase.
1 2 3 4 5 6 7 8 9 10 |
List<String> employeeNames = employeesList.stream() .map(e -> e.getName()) .map(s -> s.toUpperCase()) .collect(Collectors.toList()); System.out.println(employeeNames); // Output // [MOHAN, JOHN, VAIBHAV, AMIT] |
Filter()
Filter() operation is used to filter stream based on conditions. Filter method takes Predicate() interface which returns boolean value.
Let’s say you want to employees whose name starts with ‘A’.
You can write following functional code to achieve the same.
1 2 3 4 5 6 7 8 9 10 |
List<String> employeeNames = employeesList.stream() .map(e -> e.getName()) .filter(s -> s.startsWith("A")) .collect(Collectors.toList()); System.out.println(employeeNames); // Output // [AMIT] |
sorted()
You can use sorted() method to sort list of objects. sorted method without arguments sorts list in natural order. sorted() method also accepts comparator as parameter to support custom sorting.
💡 Did you know?
Natural order means sorting the list based on comparable interface implemented by list element type.
For example:
List
will be sorted on the basis of comparable interface implemented by Integer class.
Here is the sorted() method example
1 2 3 4 5 6 7 8 9 |
List<Employee> employees = employeesList.stream() .sorted() .collect(Collectors.toList()); System.out.println(employees); // Output // [Employee [name=Amit, age=22], Employee [name=John, age=27], Employee [name=Mohan, age=24], Employee [name=Vaibhav, age=32]] |
Here is the sorted()
method example with Comparator as a parameter.
1 2 3 4 5 6 7 8 9 |
List<Employee> employees = employeesList.stream() .sorted((e1,e2)->e1.getAge() - e2.getAge()) .collect(Collectors.toList()); System.out.println(employees); // Output // [Employee [name=Amit, age=22], Employee [name=Mohan, age=24], Employee [name=John, age=27], Employee [name=Vaibhav, age=32]] |
You can also rewrite this with method reference as below:
1 2 3 4 5 6 7 8 9 |
List<Employee> employees = employeesList.stream() .sorted(Comparator.comparing(Employee::getAge)) .collect(Collectors.toList()); System.out.println(employees); // Output // [Employee [name=Amit, age=22], Employee [name=Mohan, age=24], Employee [name=John, age=27], Employee [name=Vaibhav, age=32]] |
limit()
You can use limit() to limit the number of elements in the stream.
For example:
limit(3)
returns first 3 elements in the list.
Let’s see with the help of an example:
1 2 3 4 5 6 7 8 9 |
List<Employee> employees = employeesList.stream() .limit(3) .collect(Collectors.toList()); System.out.println(employees); // Output // [Employee [name=Mohan, age=24], Employee [name=John, age=27], Employee [name=Vaibhav, age=32]] |
Skip()
skip(int n)
method is used to discard first n elements from the stream.
For example:
skip(3)
discards first 3 elements from stream.
Let’s see with help of example:
1 2 3 4 5 6 7 8 9 |
List<Employee> employees = employeesList.stream() .skip(3) .collect(Collectors.toList()); System.out.println(employees); // Output // [Employee [name=Amit, age=22]] |
flatmap()
map()
operation generates one output for each input element.
What if you want more than one output for each input?
flatmap() operation is exactly used for this purpose. It is used to map multiple-output for each input.
For example:
We want to accumulate list of cities in which all employees have lived. One employee could have lived in multiple cities so that we may have more than one city for each employee.
Let’s see with help of example:
1 2 3 4 5 6 7 8 9 10 |
List<String> listOfCities = employeesList.stream() .flatMap(e -> e.getListOfCities().stream()) .collect(Collectors.toList()); System.out.println("listOfCities: " +listOfCities); // Output // listOfCities: [Newyork, Banglore, Paris, London, Pune, Seattle, Chennai, Hyderabad] |
Common terminal operations
foreach
foreach() is terminal operation which is used to iterate over collection/stream of objects. It takes consumer as a parameter.
Let’s say you want to print elements of the stream.
1 2 3 4 5 6 7 8 9 10 |
employeesList.stream() .forEach(System.out::println); // Output // Employee [name=Mohan, age=24] // Employee [name=John, age=27] // Employee [name=Vaibhav, age=32] // Employee [name=Amit, age=22] |
collect
collect()
is terminal operation which performs mutable reduction on the elements of Stream using Collector. Collectors is utility class which provides inbuilt Collector.
For example:
Collectors.toList()
provides a Collector which converts Stream to a list object.
Following code accumultates Employee names into a Arraylist
1 2 3 4 5 6 7 8 9 |
List<String> employeeNames = employeesList.stream() .map(Employee::getName) .collect(Collectors.toList()); System.out.println(employeeNames); // Output // [Mohan, John, Vaibhav, Amit] |
Reduce
The reduce operation combines all elements of Stream and produces single result.
Java 8 has three overloaded version of reduce method.
-
Optional<T> reduce(BinaryOperator<T> accumulator):
This method takesBinaryOperator
accumulator function.BinaryOperator
isBiFunction
where both the operands are of same type. First parameter is result till current execution, and second parameter is the current element of the Stream.Let’s find name of Person with minimum age.
1234567employeesList.stream().reduce( (e1,e2)-> (e1.getAge() < e2.getAge()? e1:e2)).ifPresent(System.out::println);// Output// Employee [name=Amit, age=22] T reduce(T identity, BinaryOperator
accumulator):
This method takes identity value and accumulator function. identity value is initial value of the reduction. If Stream is empty,then identity value is the result.
Let’s find sum of all ages of Employees123456789int sumAge = employeesList.stream().mapToInt(Employee::getAge).reduce(0, (age1,age2)-> (age1 + age2));System.out.println("Sum of ages of all Employees: "+sumAge);// Output// Sum of ages of all Employees: 105-
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner):
This method takes identity value and accumulator function and combiner. Combiner is mainy used in case of Parallel Streams. Combiner comibnes the result of sub-stream that run in Parallel.
count
count() is used to count number of elements in the stream.
1 2 3 4 5 6 7 8 9 10 |
long empCountStartJ = employeesList.stream() .map(Employee::getName) .filter(s -> s.startsWith("J")) .count(); System.out.println(empCountStartJ); // Output // 1 |
allMatch()
allMatch()
returns true when all the elements in the stream meet provided condition.
This is a short-circuiting terminal operation because operation stops as soon as it encounters any unmatched element.
1 2 3 4 5 6 7 8 9 |
boolean allMatch = employeesList.stream() .allMatch(e ->e.getAge()>18); System.out.println("Are all the employess adult: " +allMatch); // Output // Are all the employess adult: true |
nonMatch()
nonMatch()
returns true when all the elements in the stream do not meet provided condition.
This is a short-circuiting terminal operation because operation stops as soon as it encounters any matched element.
1 2 3 4 5 6 7 8 9 |
boolean noneMatch = employeesList.stream() .noneMatch(e ->e.getAge()>60); System.out.println("Are all the employess below 60: " +noneMatch); // Output // Are all the employess below 60: true |
anyMatch()
anyMatch()
returns true when any element in the stream meets provided condition.
This is a short-circuiting terminal operation because operation stops as soon as it encounters any matched element.
1 2 3 4 5 6 7 8 9 |
boolean anyMatch = employeesList.stream() .anyMatch(e ->e.getAge()>30); System.out.println("is any employee's age greater than 30: " +anyMatch); // Output // is any employee's age greater than 30: true |
min()
min(Comparator)
returns minimum element in the stream based on the provided comparator. It returns an object which contains actual value.
1 2 3 4 5 6 7 8 9 10 |
Optional<Employee> minEmpOpt = employeesList.stream() .min(Comparator.comparing(Employee::getAge)); Employee minAgeEmp = minEmpOpt.get(); System.out.println("Employee with minimum age is: " +minAgeEmp); // Output // Employee with minimum age is: Employee [name=Amit, age=22] |
max()
max(Comparator)
returns maximum element in the stream based on the provided comparator. It returns an object which contains actual value.
1 2 3 4 5 6 7 8 9 10 |
Optional<Employee> maxEmpOpt = employeesList.stream() .max(Comparator.comparing(Employee::getAge)); Employee maxAgeEmp = maxEmpOpt.get(); System.out.println("Employee with maxium age is: " +maxAgeEmp); // Output // Employee with maxium age is: Employee [name=Vaibhav, age=32] |
Parallel Streams
You can create Parallel Stream using .parallel()
method on Stream
object in java.
Here is an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int[] array= {1,2,3,4,5}; System.out.println("================================="); System.out.println("Using Parallel Stream"); System.out.println("================================="); IntStream intParallelStream=Arrays.stream(array).parallel(); intParallelStream.forEach((s)-> { System.out.println(s+" "+Thread.currentThread().getName()); } ); |
Here is a comprehensive article on Java 8 Parallel Stream.
Exercises
Let’s practice some exercises on Stream.
Exercise 1
Given a list of employees, you need to find all the employees whose age is greater than 30 and print the employee names.(Java 8 APIs only)
Answer:
Exercise 2
Given the list of employees, find the count of employees with age greater than 25?
Answer:
Exercise 3
Given the list of employees, find the employee whose name is John
.
Exercise 4
Given a list of employees, You need to find highest age of employee?
Answer:
Excercise 5
Given a list of employees, you need sort employee list by age? Use java 8 APIs only
Answer:
Excercise 6
Given the list of Employees, you need to join the all employee names with ","?
Answer:
Excercise 7
Given the list of employees, you need to group them by name
contents are really helpful.