{getToc} $title={Table of Contents}
Java 8 : Steam - GroupBy |
Introduction
Let’s understand how to group objects wisely. Java streams came into
picture with JDK 1.8 release. Actually, nowadays we cannot ignore
collections and streams when we are working with Java. They saves our lives a
lot. Among the streams APIs available, I thought to discuss a little bit more
on grouping of objects as I feel it’s very useful to achieve things in
a cleaner way.
Let’s say we have set of Students. We need to group them by their age. In
traditional way, what will be done to achieve this? Can you imagine? We may
have to follow several steps. What if I say, using streams groupingBy, we can
do this in 1 line of code?
Streams collect method accepts a
Collector. This method is used to collect grouped objects. There are 3 grouping
methods supported using overloading in Collectors class. Let’s see their
usages deeply…
// 1st method public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> var0) // 2nd method public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> var0, Collector<? super T, A, D> var1) // 3rd method public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> var0, Supplier<M> var1, Collector<? super T, A, D> var2)
Group By with Function
Here we will use the 1st method which is accepting only a Function as method
arguments.
Let’s imagine we have a list of students.
Studetn model class
This Student has a name and age.
package streama.groupby; public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } 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; } @Override public String toString() { return "Student {name=" + name + ", age=" + age + "}"; } }
In main class: StudentStreamGroupBy
Let's create some new student name and age.
Student s1 = new Student("Jeff", 33); Student s2 = new Student("Tim", 33); Student s3 = new Student("Jonh", 35); Student s4 = new Student("Porter", 38); Student s5 = new Student("Candon", 35); Student s6 = new Student("Haman", 23);
List<Student> students = Arrays.asList(s1, s2, s3, s4, s5, s6);
Group these students objects by age
We need to group these students objects by age. How to achieve this?
We only need a function here…
Map<Integer, List<Student>> studentsByAge = students.stream()
.collect(Collectors.groupingBy(Student::getAge));
Student::getAge — Student age getter as method reference [Function]
Let's print out the list of students group by age.
System.out.println(studentsByAge);
Output:
{ 33=[ Student {name=Jeff, age=33}, Student {name=Tim, age=33 } ], 35=[ Student {name=Jonh, age=35}, Student {name=Candon, age=35 } ], 38=[ Student {name=Porter, age=38} ], 23=[ Student {name=Haman, age=23} ] }
Isn’t this great guys?? Using simple 1 line of code we did it!
Group By with Function and Collector
Here we will use the 2nd method which is accepting a Function and a Collector
as method arguments.
Group Simple Objects
Imagine we have a list of fruit names. In this list, same fruit name can be
placed multiple times which means it has duplicates. We need to count the
number of occurrences of each fruit. How to do this?
List<String> fruitNames = Arrays.asList("mango", "mango", "banana", "apple", "orange", "banana", "papaya", "papaya", "papaya");
Traditional approach:
Map<String, Integer> fruitMap = new HashMap<>(); for (String f : fruitNames) { if (fruitMap.containsKey(f)) fruitMap.put(f, fruitMap.get(f) + 1); else fruitMap.put(f, 1);}
Streams GroupingBy approach:
Map<String, Long> result = fruitNames.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
Function.identity() — represents the item in the [Function]
Collectors.counting() — counts the elements [Collector]
Output:
{ papaya=3, orange=1, banana=2, apple=1, mango=2 }
This is very cleaner way right? We can achieve much better code clearness also
following streams.
Group Custom Objects
Let’s assume we have a list of products. Our pojo is Product class which has a
name, price and quantity.
package streama.groupby; import java.math.BigDecimal; public class Product { private String name; private int qty; private BigDecimal price; public Product(String name, int qty, BigDecimal price) { this.name = name; this.qty = qty; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getQty() { return qty; } public void setQty(int qty) { this.qty = qty; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } @Override public String toString() { return "Item{" + "name='" + name + '\'' + ", qty=" + qty + ", price=" + price + '}'; } }
List of products will be like this…
List<Product> products = Arrays.asList( new Product("skirt", 10, new BigDecimal("9.99")), new Product("pants", 20, new BigDecimal("19.99")), new Product("television", 10, new BigDecimal("29.99")), new Product("air-con", 10, new BigDecimal("29.99")), new Product("refrige", 20, new BigDecimal("9.99")), new Product("fan", 10, new BigDecimal("9.99")), new Product("coffee", 10, new BigDecimal("19.99")), new Product("pure drinking water", 20, new BigDecimal("9.99")) );
We need the product names grouped by the quantity. Even though here we have
objects, finally we need only product names.
Let’s code and print out the result...
Map<String, Integer> result = products.stream() .collect(Collectors.groupingBy(Product::getName, Collectors.summingInt(Product::getQty))); System.out.println(result);
Product::getName — Item name getter as method reference [Function]
Collectors.summingInt(Item::getQty) — sums the quantity of each item using
getter [Collector]
Output:
{ fan=10, pants=20, skirt=10, pure drinking water=20, television=10, coffee=10, refrige=20, air-con=10 }
Group By with Function, Supplier and Collector
Here we will use the 3rd method which is accepting a Function, a Supplier
and a Collector as method arguments.
Let’s assume we need to find the item names grouped by their prices. What we
can do to achieve it?
List<Item> items = getItemsList(); Map<BigDecimal, Set<String>> result = items.stream() .collect( Collectors.groupingBy( Item::getPrice, Collectors.mapping(Item::getName, Collectors.toSet()) ) );
Item::getPrice
— Item price getter as method reference
[Function]
Collectors.mapping(Item::getName, Collectors.toSet())
— collects the elements using getter for price [Collector] => this will
map item names as a set.
If we need to sort this output based one prices?
What kind of data structure we can use?
It’s TreeMap!!!
What is role of supplier?
— You know that supplier is not taking anything but returning some value.
Right?
Then we have to just supply a new TreeMap..That means we can use 3rd method
I have mentioned.
List<Item> items = getItemsList(); Map<BigDecimal, Set<String>> sortedItemsByPrice = items.stream() .collect( Collectors.groupingBy( Item::getPrice, TreeMap::new, Collectors.mapping(Item::getName, Collectors.toSet()) ) );
Output:
{ 9.99=[papaya, apple], 19.99=[banana], 29.99=[orange, watermelon] }
Following this method, we can apply the same rule for our employee map also!
We can group them by age and sort…
Map<Integer, Set<String>> sortedEmployeesByAge = employees.stream() .collect(Collectors.groupingBy( Employee::getAge, TreeMap::new, Collectors.mapping(Employee::getName, Collectors.toSet()) ) );
Output:
{ 22=[Nathan], 23=[George], 33=[Tim, Andrew], 38=[John, Peter] }
So, results are sorted based on the map keys! This is one use of the 3rd
method. Simply we can enhance the output using a supplier. If we need to
maintain insertion order, we can simply supply a LinkedHashMap as a supplier
like this =>
LinkedHashMap::new
This is all about grouping using streams guys! There are several use cases
like this where we need this concept. We can write very concise and readable
codes using groupingBy methods.
Conclusion
Hope this article was helpful.