Stream API -most useful operations
Stream API was one of the major additions to Java 8. A Stream can be defined as a sequence of elements from a source that supports aggregate operations on them. The source can be collections or arrays that provide data to a stream.
Stream is not a data structure itself. It is a bunch of operations applied to a source. It has basically two types of operations :
- Intermediate operations- These methods do not produce any results. They usually accept functional interfaces as parameters and always return a new stream. Some examples of intermediate operations are , , etc.
- Terminal operations- These methods produce some results, e.g., , etc.
Streams operations further classification:
Note :We would use these three lists throughout our article:
employeeList has Employees with name, age, salary and country.
intList has some random integers.
stringList has some uppercase and lowercase strings.
- Filtering- The filtering operations filter the given stream and returns a new stream, which contains only those elements that are required for the next operation. This is an intermediate operation.
- To filter out Employee whose age is greater than 30:
employeeList.stream().filter(emp -> emp.getAge() >30).ForEach(System.out::println);
2. Slicing- slicing operations are intermediate operations, and, as the name implies, they are used to slice a stream. It includes distinct() , limit(), skip().For example take this intList:
- distinct values incase list has duplicates - intList.stream().distinct().forEach(System.out::println);
- limiting the returned stream to return only upto n values- intList.stream().distinct().limit(3).forEach(System.out::println);
- skipping the first n (here its 2 )values- intList.stream().distinct().skip(2).forEach(System.out::println);
3. Mapping- Mapping operations are those operations that transform the elements of a stream and return a new stream with transformed elements.Map() and are most commonly used methods. For ex: take stringList which takes String as input,
- To print names from the list in upperCase —
stringList.stream().map(name -> name.toUpperCase()) .forEach(System.out::println); - To get length of words , the input is a string and output is an integer, We use the mapToInt() method instead of map(),it will return IntStream instead of Stream.
stringList.stream().mapToInt(name -> name.length()).forEach(System.out::println);
4. Matching- Matching operations are terminal operations that are used to check if elements with certain criteria are present in the stream or not. Mostly three operations of matching are used : , allMatch(), noneMatch(). Consider the employeeList , lets see different matching operations:
- To check if there is any person residing in a particular country:
boolean canadian= employeeList.stream().anyMatch(p -> p.getCountry().equals(“Canada”)); - To check if all the persons are residents of a particular country:
boolean allIndian = employeeList.stream().allMatch(p -> p.getCountry().equals(“India”)); - To check if all the persons are not residents of a particular country:
boolean noneMexican = employeeList.stream().noneMatch(p -> p.getCountry().equals(“Mexico”));
5. Finding- Finding operations are terminal operations that are used to get the matched element instead of just verifying if it is present or not. There are two basic finding operations in streams, i.e., findAny(), . For example:
- To Filter employee living in India and get the first match.
Optional<Employee> person = employeeList.stream().filter(p -> p.getCountry().equals(“India”)).findFirst();
findAny() is used similar to findFirst() but in cases where we are not concerned about which element is returned , like parallel streams. If we use the findFirst() method in the parallel stream, it can be very slow.
6. Reduction- Reduction stream operations are those operations that reduce the stream into a single value. When we need to perform operations where a stream reduces to a single value, for example, maximum, minimum, sum, product, etc. sum(), min(), max(), count() etc. are some examples of reduce operations. explicitly asks you to specify how to reduce the data that made it through the stream.
- To find sum of all Employee salaries.
Optional<Integer> totalSalary = employeeList.stream().map(p -> p.getSalary()).reduce((a,b) -> a+ b); - Above sum can also be found by using sum() but for that we need to convert the stream to IntStream and we could directly use sum().
int totalSal = employeeList.stream().mapToInt(p -> p.getSalary()).sum(); - Same way we can also use min() ,max() when we want max or min element from any stream.
Optional<Integer> max = intList.stream().max(Comparator.naturalOrder());
Note: If the stream is of a custom object, we can provide a custom comparator as well.
7. Collect- It is terminal method. We can create our own collector implementation or We can use the predefined implementations provided by the . Collectors has various methods like — toList(), toMap(), toSet(),CollectingAndThen() , , partitioningBy(), minBy(), maxBy() etc. We will look into these in details by dividing these into three categories:
Collection Operations:
- toList()- to collect stream into List:
List<String> empName = employeeList.stream().map(emp -> emp.getName().collect(Collectors.toList()); - toSet()- to collect stream into Set:
Set<String> empName = employeeList.stream().map(emp -> emp.getCountry()).collect(Collectors.toSet()); - toMap()- to collect stream into Map:
Map<String,Integer> nameMap = list.stream().collect(Collectors.toMap(s -> s , s -> s.length(),(s1,s2) -> s1));
Note: toMap() throws exception incase list has duplicate elements, to avoid this use above mentioned implementation with additional parameter to choose first one if there are duplicates(like s1,s2 ->s1). - toCollection(Supplier<C> collectionFactory)- to collect input elements into new Collection:
LinkedList<String> empName = employeeList.stream().map(emp -> emp.getName()).collect(Collectors.toCollection(LinkedList::new)); - collectingAndThen()- This method returns a Collector that accumulates the input elements into the given Collector and then performs an additional finishing function. For ex: making the list unmodifiable here:
List<Employee> unmodifiableList = employeeList.stream().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
Aggregation Operations:
- counting()- to get count of employees :
long count = employeeList.stream().filter(emp -> emp.getAge() > 30).collect(Collectors.counting()); - minBy()- to get employee with min salary :
Optional<Employee> emp =employeeList.stream().collect(Collectors.minBy(Comparator.comparing(Employee::getSalary))); - maxBy()- to get employee with max salary :
Optional<Employee> employee = employeeList.stream().collect(Collectors.maxBy(Comparator.comparing(Employee::getSalary))); - Collectors.summingInt(ToIntFunction Mapper)- to get sum of salaries of all employees :
int count = employeeList.stream().collect(Collectors.summingInt(emp -> emp.getSalary())); - Collectors.averagingInt(ToIntFunction Mapper)- to get average of salaries of all employees :
double avg=employeeList.stream().collect(Collectors.averagingInt(emp -> emp.getSalary())); - joining()- Joining all the strings with space in between.
String joinedString = Stream.of(“welcome”, “to ”“Java” , “world”).collect(Collectors.joining());
Note: we can use other overloaded version of joining method as well,which allow us to provide delimiters and prefix and suffix strings. - summarizingInt(ToIntFunction Mapper)- to get summary statistics of any collection :
IntSummaryStatistics summarizingInt = employeeList.stream().collect(Collectors.summarizingInt(Employee::getSalary));
Note: output will come something like —
IntSummaryStatistics{count=6, sum=248000, min=23000, average=41333.333333, max=67000}
Grouping Operations :
Grouping operations are one of the most important features of streams because they can help you complete a task within 2–3 lines, which otherwise would have taken a lot of coding.
groupingBy()- to group employees by country :
Map<String,List<Employee>> empMap = employeeList.stream().
collect(Collectors.groupingBy(Employee::getCountry));
Note : There are lot more usecases for this method such as:
- If we need to get a Map where the key is the name of the country and the value is the sum of salaries of all of the employees of that country :
Map<String, Integer> empMap = employeeList.stream().collect(Collectors.groupingBy(Employee::getCountry, Collectors.summingInt(Employee::getSalary))); - We can also store the result in a set instead of list with the overloaded version of the groupingBy() :
Map<String, Set<Employee>> empMap = employeeList.stream().collect(Collectors.groupingBy(Employee::getCountry, Collectors.toSet())); - If we need to group on multiple conditions. Then we can provide another groupingBy() as downstream, for ex: employees are grouped by country and age by using the groupingBy() method twice.
Map<String, Map<Integer,List<Employee>>> empMap = employeeList.stream().collect(Collectors.groupingBy(Employee::getCountry, Collectors.groupingBy(Employee::getAge))); - If we need to get a Map where the key is the name of the country and the value is the Employee object that has max salary in that country.
Map<String, Optional<Employee>> empMap = employeeList.stream().collect(Collectors.groupingBy(Employee::getCountry, Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))));
partitioningBy(): is used to partition a stream of objects(or a set of elements) based on a given predicate and returns a Map<Boolean,List<T>>.Since the key is a boolean it only takes true/false values. Under the true key, we will find elements that match the given predicate.
- If we need to partition the employeeList based on age :
Map<Boolean, List<Employee>> empMap = employeeList.stream().collect(Collectors.partitioningBy(emp -> emp.getAge() > 30));
This was all about some of the very useful stream methods. These are only for a quick check and would need some more detailed practice.
Reference :
Thanks ! Happy learning :)