Java 8 Grouping with Multiple Keys

1.4k views Asked by At

I am looking for some help in grouping a list of objects from a list with multiple keys.

Basically, I have a list of users which contains a list of their orders and I want to be able to group them together using UserName and Address as the keys.

Example Data:

<UserList>
        <User>
            <Username>user123</Username>
            <Address>London</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>1</OrderNumber>
                    <Cost>3683446.6600</Cost>
                </TransactionList>
            </TransactionList>
        </User>
              <User>
            <Username>user123</Username>
            <Address>London</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>3</OrderNumber>
                    <Cost>500</Cost>
                </TransactionList>
            </TransactionList>
        </User>
               <User>
            <Username>user12356</Username>
            <Address>Manchester</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>6</OrderNumber>
                    <Cost>90000</Cost>
                </TransactionList>
            </TransactionList>
        </User>
              <User>
            <Username>user12356</Username>
            <Address>Manchester</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>10</OrderNumber>
                    <Cost>100</Cost>
                </TransactionList>
            </TransactionList>
        </User>
    </UserList>

I want to order it like this so that each user just has one instance and its related transactions are grouped in a list based off Username and Address:

<UserList>
        <User>
            <Username>user123</Username>
            <Address>London</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>1</OrderNumber>
                    <Cost>3683446.6600</Cost>
                </TransactionList>
                <TransactionList>
                    <OrderNumber>3</OrderNumber>
                    <Cost>500</Cost>
                </TransactionList>
            </TransactionList>
        </User>
         <User>
            <Username>user12356</Username>
            <Address>Manchester</Address>
            <TransactionList>
                <TransactionList>
                    <OrderNumber>6</OrderNumber>
                    <Cost>90000</Cost>
                </TransactionList>
                 <TransactionList>
                    <OrderNumber>10</OrderNumber>
                    <Cost>100</Cost>
                </TransactionList>
            </TransactionList>
        </User>
    </UserList> 
        

I have tried to do it using a map:

Map<String, Map<String,List<User>>> map;

map = userLists.getUserList().stream()
                .collect(Collectors.groupingBy(User::getUserName, Collectors.groupingBy(User::getAddress)));

This is slightly what I am looking to do but I was wondering if there was a better way using MultiKey Map or something like that and then iterate through and put a list of transactions if the key matched.

Thanks in advance for the help.

2

There are 2 answers

4
Nikolas Charalambidis On BEST ANSWER

Your approach is not wrong, the first step is correct: to group the users by the userName and then address. Personally, I'd invert it since more likely there are more users with a same address, but the order is not important in terms of achieving a corect result.

I notice the expected output looks like List<User> with reduced users with common characteristic (userName and address) and concatenated transactionList lists. Although a map with a composite key (ex. ${userName}_${address}) might seem helpful, I'd rather choose the List<User>, which is in comppliant with, by the way, what the expected output is like.

So, the second step is to iterate all these entries and reduce List<User> within the Map into a single user. Each inner entry will be associated with a single user (because of both userName and address). For-each is more than suitable for this task. There you go:

Map<String, Map<String,List<User>>> map = userLists.stream()
     .collect(Collectors.groupingBy(
            User::getUserName, 
            Collectors.groupingBy(User::getAddress)));

List<User> list = new ArrayList<>();                 // stored output

map.forEach((userName, groupedByAddress) -> {        // for each 'userName'
    groupedByAddress.forEach((address, users) -> {   // ... and each 'address'
        User userToAdd = new User();                 // ...... create an 'User'
        userToAdd.setUserName(userName);             // ...... with the 'userName'  
        userToAdd.setAddress(address);               // ...... with the 'address'
        users.forEach(user -> {                      // ...... and of each the user's
            userToAdd.getTransactionList()           // .......... concatenate
                .addAll(user.getTransactionList());  // .......... all the transactions
        });
    });
});

The Stream API is a good for grouping to get an intermediate result, however, for such futher reduction it would be very clumsy.

4
Naman On

What you are looking for is really a merging capability based on the name and address of the users being common. If you look into the Collectors.toMap API, it provides you a similar capability along with the key and value selection for the Map.

Collectors.toMap(
        u -> Arrays.asList(u.getUserName(), u.getAddress()),
        Function.identity(),
        User::mergeUsers
));

where the merge is it looks like you would expect it to be

static User mergeUsers(User user1, User user2) {
    List<Transaction> overall = new ArrayList<>(user1.getTransactionList());
    overall.addAll(user2.getTransactionList());
    return new User(user1.getUserName(), user1.getAddress(), overall);
}

and since you are only looking for the values of this Map, your output with a complete solution would be:

Collection<User> users = userLists.getUserList().stream()
        .collect(Collectors.toMap(
                u -> Arrays.asList(u.getUserName(), u.getAddress()),
                Function.identity(),
                User::mergeUsers
        )).values();