Java 8 Streams — GroupBy
Let’s understand how to group objects wisely
Java streams came into picture with JDK 1.8 release. I have already talked about this topic here: https://blog.devgenius.io/stream-api-in-java-8d30a52345ec
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? Isn’t that amazing?? Let’s see…
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 employees.
Employee e1 = new Employee("John", 38);
Employee e2 = new Employee("Tim", 33);
Employee e3 = new Employee("Andrew", 33);
Employee e4 = new Employee("Peter", 38);
Employee e5 = new Employee("Nathan", 22);
Employee e6 = new Employee("George", 23);
List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6);
This Employee has a name and age. We need to group these employee objects by age. How to achieve this??
Map<Integer, List<Employee>> employeesByAge = employees.stream()
.collect(Collectors.groupingBy(Employee::getAge));
We only need a function here…
Employee::getAge — Employee age getter as method reference [Function]
Output:
{
33=[
Employee{age=33, name='Tim'},
Employee{age=33, name='Andrew'}
],
22=[
Employee{age=22, name='Nathan'}
],
38=[
Employee{age=38, name='John'},
Employee{age=38, name='Peter'}
],
23=[
Employee{age=23, name='George'}
]
}
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("apple", "apple", "banana", "apple", "orange", "banana", "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]
This is very cleaner way right? We can achieve much better code clearness also following streams.
📔Group Custom Objects
Example 1️⃣
Let’s assume we have a list of items. Our pojo is Item which has a name, price and quantity.
class Item {
private String name;
private int qty;
private BigDecimal price;
public Item(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 items will be like this…
Arrays.asList(
new Item("apple", 10, new BigDecimal("9.99")),
new Item("banana", 20, new BigDecimal("19.99")),
new Item("orange", 10, new BigDecimal("29.99")),
new Item("watermelon", 10, new BigDecimal("29.99")),
new Item("papaya", 20, new BigDecimal("9.99")),
new Item("apple", 10, new BigDecimal("9.99")),
new Item("banana", 10, new BigDecimal("19.99")),
new Item("apple", 20, new BigDecimal("9.99"))
);
We need the item names grouped by the quantity. Even though here we have objects, finally we need only item names.
Let’s code…
Map<String, Integer> result = items.stream()
.collect(Collectors.groupingBy(Item::getName, Collectors.summingInt(Item::getQty)));
Item::getName — Item name getter as method reference [Function]
Collectors.summingInt(Item::getQty) — sums the quantity of each item using getter [Collector]
Output:
{
papaya=20,
orange=10,
banana=30,
apple=40,
watermelon=10
}
Example 2️⃣
Let’s assume we have a list of employees. Our Employee has a name and age.
public class Employee {
int age;
String name;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee employee = (Employee) o;
return age == employee.age && name.equals(employee.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Employee{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
We need to group the employee names by their age. How to do this using streams?
Map<Integer, List<String>> employeeNamesByAge = employees.stream()
.collect(Collectors.groupingBy(
Employee::getAge,
Collectors.mapping(Employee::getName, Collectors.toList())
)
);
Employee::getAge — Employee age getter as method reference [Function]
Collectors.mapping(Employee::getName, Collectors.toList()) — collects the elements using getter for name [Collector] => this will map employee names as a list.
Output:
{
33=[Tim, Andrew],
22=[Nathan],
38=[John, Peter],
23=[George]
}
Both the examples are suing the 2nd method of Collector class here…
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. I have explained main 3 methods in this article as much as I can. Hope you would understand and use this in your day to day programming. 😎 💪
Bye Bye ❤️