Java 8: Streams - Group By Objects

{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.



Previous Post Next Post