John Smith
John Smith

Reputation: 101

Java group a map by value where value is a List

I have a

Map<String,List<User>>map = new HashMap<>();
map.put("projectA",Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
map.put("projectC",Arrays.asList(new User(3,"Mo")));

Can use String instead of User.

String is a project Name but the same users can relate to different projects.

I would like to get sth like Map<User, List<String>> where the key will represent a distinct user and a value as a list of projects' names to which he/she relates.

Bob  = [projectA]
John = [projectA, projectB]
Mo   = [projectA, projectB, projectC]

TQ in advance for any piece of advice.

Upvotes: 0

Views: 570

Answers (2)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 28988

To reverse this Map, you need to iterate over its entries and for each distinct user create an entry containing a list of projects as a value in the resulting Map.

Java 8 computeIfAbsent()

This logic can be implemented using Java 8 methods Map.computeIfAbsent() and Map.forEach().

Map<String, List<User>> usersByProject = // initilizing the source map
Map<User, List<String>> projectsByUser = new HashMap<>();

usersByProject.forEach((project, users) ->
    users.forEach(user -> projectsByUser.computeIfAbsent(user, k -> new ArrayList<>())
         .add(project))
);

Stream API

Stream-based implementation would require a bit more effort.

The core logic remains the same. But there's one important peculiarity: we would need to generate from each entry of the source Map a sequence of new elements, containing references to a particular user and a project.

To carry this data we would need an auxiliary type, and a Java 16 record fits into this role very well. And quick and dirty alternative would be to use Map.Entry, but it's better to avoid resorting to this option because methods getKey()/getValue() are faceless, and it requires more effort to reason about the code. You can also define a regular class if you're using an earlier version of JDK.

public record UserProject(User user, String project) {}

That's how a stream-based solution might look like:

Map<String, List<User>> usersByProject = Map.of(
    "projectA", List.of(new User(1, "Bob"), new User(2, "John"), new User(3, "Mo")),
    "projectB", List.of(new User(2, "John"), new User(3, "Mo")),
    "projectC", List.of(new User(3, "Mo"))
);
    
Map<User, List<String>> projectByUsers = usersByProject.entrySet().stream()
    .flatMap(entry -> entry.getValue().stream().
        map(user -> new UserProject(user, entry.getKey()))
    )
    .collect(Collectors.groupingBy(
        UserProject::user,
        Collectors.mapping(UserProject::project,
            Collectors.toList())
    ));
        
projectsByUser.forEach((k, v) -> System.out.println(k + " -> " + v));

Output:

User[id=1, name=Bob] -> [projectA]
User[id=2, name=John] -> [projectA, projectB]
User[id=3, name=Mo] -> [projectA, projectC, projectB]

Upvotes: 0

f1sh
f1sh

Reputation: 11934

Just loop over the map's entries and the List inside of them:

public static void main(String[] args) {
  Map<String, List<User>> map = new HashMap<>();
  map.put("projectA", Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
  map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
  map.put("projectC",Arrays.asList(new User(3,"Mo")));

  Map<User, List<String>> result = new HashMap<>();
  for(Map.Entry<String, List<User>> e:map.entrySet()) {
    for(User u:e.getValue()) {
      result.putIfAbsent(u, new ArrayList<>());
      result.get(u).add(e.getKey());
    }
  }
  System.out.println(result);
}
public static record User(int id, String name) {}

prints

{User[id=1, name=Bob]=[projectA], User[id=2, name=John]=[projectB, projectA], User[id=3, name=Mo]=[projectB, projectA, projectC]}

Upvotes: 1

Related Questions