Tishy Tash
Tishy Tash

Reputation: 357

Group data into a map using Java 8 streams

I want to group data by using Java 8 streams API. All the rows which have a parent_id should be grouped together. Below is the sample text file. The outcome should be a map where id will be an Integer and values will be respective grouped rows. For example, in below case, the result will be a map of 3 entries. 2 values against key 1, no value against key 2 and 1 value against key 3.

id      name    parent_id
1       A       (null)
2       B       1
3       C       1
4       D       (null)
5       E       (null)
6       F       5

Code snippet is:

Map<String, List<FileVO>> map= list.stream()
        .collect(groupingBy(FileVO::getParentId, toList()));

Output can be like: {A,{B,C}}, {D,{}},{E,{F}}.

Simple rule is: Where parentId is not null, those records should be grouped into one list. And this list will be treated as value in map. It's key will be parentId which is the actual id (the value of column id and it won't be null. Whereas parentId can be null. If a record has null parentId and no other record has its Id in their parentId column then it will be treated as a single object with key but null value.)

Upvotes: 2

Views: 1622

Answers (2)

Hadi
Hadi

Reputation: 17289

I think you can't do it in single stream.

Map<Integer, String> roots = list.stream()
            .filter(myObject -> myObject.getParentId() == null)
            .collect(Collectors.toMap(MyObject::getId, MyObject::getName));

out put is all parent by its id and name

{1=A, 4=D, 5=E}

and

Map<Integer, List<String>> groupByParentId = list.stream()
            .filter(myObject -> myObject.getParentId() != null)
            .collect(Collectors.groupingBy(MyObject::getParentId,
                    Collectors.mapping(MyObject::getName, toList())));

output is grouping by parentId

{1=[B, C], 5=[F]}

and final step is:

roots.forEach((k,v)->map.put(v,groupByParentId.getOrDefault(k,new ArrayList<>())));

update for stream version: complexity is O(n^2)

  list.stream()
            .filter(myObject -> myObject.getParentId() == null)
            .collect(Collectors.toMap(MyObject::getName, MyObject::getId))
            .forEach((k, v) -> map.put(k, list.stream()
            .filter(myObject -> myObject.getParentId() == v)
            .map(MyObject::getName)
            .collect(Collectors.toList())));

or also you can use non-stream way like this:(personally prefer non-stream version)

note: in this way roots is Map<String,Integer> roots

String root = "";
for (MyObject myObject : list) {
    if (myObject.getParentId() == null) {
       root = myObject.getName();
       map.put(root, new ArrayList<>());
    }
    if (roots.get(root).equals(myObject.getParentId())){
      map.computeIfAbsent(root, k -> new ArrayList<>()).add(myObject.getName());
    }
}

Upvotes: 2

Samuel Philipp
Samuel Philipp

Reputation: 11032

Here is a more complex solution for your problem:

In the first groupingBy() you use the parentId if available or the id if not:

Map<Integer, List<FileVO>> result = list.stream()
        .collect(Collectors.groupingBy(f -> Optional.ofNullable(f.getParentId()).orElse(f.getId())));

This will create groups of the files, which belong together:

{
  1: [
       {id: 1, name: "A", parentId: null},
       {id: 2, name: "B", parentId: 1},
       {id: 3, name: "C", parentId: 1}
  ],
  4: [
       {id: 4, name: "D", parentId : null}
  ],
  5: [
       {id: 5, name: "E", parentId : null},
       {id: 6, name: "F", parentId : 5}
  ]
}

In the second step you are going to find the parent element in each group. If you can ensure that in every list the first element is the parent (like in your example you can just use this:

Map<String, List<String>> result = list.stream()
        .collect(Collectors.groupingBy(f -> Optional.ofNullable(f.getParentId()).orElse(f.getId())))
        .entrySet().stream()
        .collect(Collectors.groupingBy(e -> e.getValue().get(0).getName(),
                Collectors.flatMapping(e -> e.getValue().stream().skip(1).map(FileVO::getName), Collectors.toList())));

This will just take the name of the first element (parent) and maps the names of all elements excluding the parent itself.

If you can not ensure that you need a more generic solution with this:

Map<String, List<String>> result = list.stream()
        .collect(Collectors.groupingBy(f -> Optional.ofNullable(f.getParentId()).orElse(f.getId())))
        .entrySet().stream()
        .collect(Collectors.groupingBy(
                e -> e.getValue().stream().filter(f -> f.getId() == e.getKey()).findAny().map(FileVO::getName).orElseThrow(),
                Collectors.flatMapping(
                        e -> e.getValue().stream().filter(f -> f.getId() != e.getKey()).map(FileVO::getName),
                        Collectors.toList())));

This is effectively the same but searches for the parent element by using the key of the map entry we created before.

Both solutions will return this for your example data:

{
  A: [B, C],
  D: [],
  E: [F]
}

Upvotes: 0

Related Questions