Grouping the Same Products in Java

RMAG news

First, one must understand the relationship between Product and Item. A Product can have multiple Items. Simply put, a Product is the product information, and an Item is the option information.

import java.util.List;

import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class Product {
String productId;

List<Item> items;

@Value
@Builder
public static class Item {
String itemId;

long itemQuantity;
}
}

In the sample products, there are a total of three Products, two of which have the same productId. At the Item level, itemId 1 and 2 also exist as duplicates. Ultimately, the goal is to group by the same productId and the same itemId.

public class Main {
public static void main(String[] args) {
/**
* [
* Product(
* productId=1,
* items=[
* Product.Item(itemId=1, itemQuantity=1),
* Product.Item(itemId=1, itemQuantity=1),
* Product.Item(itemId=2, itemQuantity=2)
* ]
* ),
* Product(
* productId=1,
* items=[
* Product.Item(itemId=3, itemQuantity=3),
* Product.Item(itemId=2, itemQuantity=2)
* ]
* ),
* Product(
* productId=2,
* items=[
* Product.Item(itemId=5, itemQuantity=5)
* ]
* )
* ]
*/

List<Product> products = getProducts();
}

private static List<Product> getProducts() {
return List.of(
Product.builder()
.productId(“1”)
.items(List.of(
Item.builder()
.itemId(“1”)
.itemQuantity(1)
.build(),
Item.builder()
.itemId(“1”)
.itemQuantity(1)
.build(),
Item.builder()
.itemId(“2”)
.itemQuantity(2)
.build()
)
)
.build(),
Product.builder()
.productId(“1”)
.items(List.of(
Item.builder()
.itemId(“3”)
.itemQuantity(3)
.build(),
Item.builder()
.itemId(“2”)
.itemQuantity(2)
.build()
)
)
.build(),
Product.builder()
.productId(“2”)
.items(List.of(
Item.builder()
.itemId(“5”)
.itemQuantity(5)
.build()
)
)
.build()
);
}
}

The ProductGroup class below holds the results of grouping by the same productId and the same itemId. It can have multiple ItemGroups per productId without duplicates, and the itemId within the ItemGroup also does not duplicate. Finally, the itemQuantity for the same itemId is summed up.

@Value
@Builder(access = AccessLevel.PRIVATE)
public class ProductGroup {
String productId;

List<ItemGroup> itemGroups;

@Value
@Builder(access = AccessLevel.PRIVATE)
public static class ItemGroup {
String itemId;

long totalQuantity;
}
}

Let’s now look at the logic for grouping. The first thing to do is to group the Products by productId.

public static List<ProductGroup> grouping(List<Product> products) {
return products.stream()
// first process Map<String, List<Product>>
.collect(Collectors.groupingBy(Product::getProductId, toList()))

.entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
.stream() // Stream<Map<K,V>.Entry<String, List<Product>>>

// second process
.map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
.toList();
}

After performing the first process, it is stored in a Map as described in the comment below. Using this Map, a ProductGroup is created by utilizing entrySet().stream().

public static List<ProductGroup> grouping(List<Product> products) {
return products.stream()

/**
* first process : Map<String, List<Product>> productListsById
* {
* 1=[
* Product(
* productId=1,
* items=[
* Product.Item(itemId=1, itemQuantity=1),
* Product.Item(itemId=1, itemQuantity=1),
* Product.Item(itemId=2, itemQuantity=2)
* ]
* ),
* Product(
* productId=1,
* items=[
* Product.Item(itemId=3, itemQuantity=3),
* Product.Item(itemId=2, itemQuantity=2)
* ]
* )
* ],
* 2=[
* Product(
* productId=2,
* items=[Product.Item(itemId=5, itemQuantity=5)]
* )
* ]
* }
*/

.collect(Collectors.groupingBy(Product::getProductId, toList()))

.entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
.stream() // Stream<Map<K,V>.Entry<String, List<Product>>>

// second process
.map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
.toList();

cf) If toUnmodifiableMap is used as shown below, a duplicateKeyException occurs if there are duplicate productIds.

Map<String, Product> productsById = products.stream()
.collect(Collectors.toUnmodifiableMap(Product::getProductId, Function.identity()));

In the subsequent secondary task, a ProductGroup is created. Then, moving on to the ItemGroup stage (makeItemGroups), the Items within a Product are first turned into a Stream using flatMap(product -> product.getItems().stream()), and then grouped again by itemId.

Using the results, an ItemGroup is created, and the totalQuantity is calculated by summing the quantities of the grouped Items.

private static ProductGroup makeEachProductGroups(Entry<String, List<Product>> productListById) {
return ProductGroup.builder()
.productId(productListById.getKey())
.itemGroups(makeItemGroups(productListById.getValue()))
.build();
}

private static List<ItemGroup> makeItemGroups(List<Product> products) {
return products.stream() // Stream<Product>
.flatMap(product -> product.getItems().stream()) // Stream<Product.Item>
.collect(groupingBy(Item::getItemId, toList())) // Map<String, List<Product.Item>>
.entrySet().stream() // Stream<Map<K,V>.Entry<String, List<Product.Item>>>
.map(it ->
ItemGroup.builder()
.itemId(it.getKey())
.totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
.build()
) // Stream<ProductGroup.ItemGroup>
.toList();
}

The final result obtained from the grouping is as follows.

/**
* second process : grouping
* [
* ProductGroup(
* productId=1,
* itemGroups=[
* ProductGroup.ItemGroup(itemId=1, totalQuantity=2),
* ProductGroup.ItemGroup(itemId=2, totalQuantity=4),
* ProductGroup.ItemGroup(itemId=3, totalQuantity=3)
* ]
* ),
* ProductGroup(
* productId=2,
* itemGroups=[
* ProductGroup.ItemGroup(itemId=5, totalQuantity=5)
* ]
* )
* ]
*/

There’s one important point to note in the makeItemGroups logic. After turning the Items within a Product into a Stream using flatMap(product -> product.getItems().stream()), it is essential to group them again by itemId.

It should not be done by grouping using the items of each product as follows.

private static List<ItemGroup> makeItemGroupsWrongWay(List<Product> products) {
return products.stream()
.flatMap(product -> // don’t do that each product
product.getItems().stream()
.collect(groupingBy(Item::getItemId, toList()))
.entrySet()
.stream()
.map(it ->
ItemGroup.builder()
.itemId(it.getKey())
.totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
.build()
))
.toList();
}

Leave a Reply

Your email address will not be published. Required fields are marked *