2

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 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks so much for the response. This has been really useful. I had to update your code slightly to create an instance of transaction before adding to it as I got a null pointer at .allAll(user.getTransaactionList());. Would this also work if I were to have 3 keys? Say for example Username. Line 1 Address, City ?
Yes, the principle would be same. The only difference is another downstream in Collectors.groupingBy, and one more nested for-each loop. Don't forget to the correct "layering": city first, then address, then username...
Thanks so much, will give that a go!
Happy coding :)
1

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();

4 Comments

Would this work if there was hundreds of Users though?
Yes, it would. What makes you doubtful of it?
You're passing in User user1 and User user2 into the merge method and specifically saying which user transaction to add
Well, yeah the merge function would be called for users with the same username and address. So, the transactions would be all added to a final user instance created from that merge function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.