简体   繁体   中英

Java stream collect to Map<String, Map<Integer, MyObject>>

I'm using Java 11 and I have a List<MyObject> called myList of the following object:

public class MyObject {
  private final String personalId;
  private final Integer rowNumber;
  private final String description;
<...>
}

and I want using streams to collect these objects into a Map<String, Map<Integer, List<MyObject>>> (with following syntax: Map<personalId, Map<rowNumber, List<MyObject>>> ) and I don't want to use Collectors.groupBy() , because it has issues with null values.

I tried to do it using Collectors.toMap() , but it seems that it is not possible to do it

myList
.stream()
.Collectors.toMap(s -> s.getPersonalId(), s -> Collectors.toMap(t-> s.getRowNumber(), ArrayList::new))

My question is it possible to make a Map<String, Map<Integer, List<MyObject>>> object using streams without using Collectors.groupBy() or should I write a full method myself?

In your case I would create the maps first and then loop through the elements in this list as shown:

Map<String, List<MyObject>> rows = new HashMap<>(); 
list.forEach(element -> rows.computeIfAbsent(element.personalId, s -> new ArrayList<>()).add(element));

You can use computeIfAbsent in order to create a new list/map as a value of the map before you can put your data in.


The same is with the second data type you created:

Map<String, Map<Integer, MyObject>> persons = new HashMap<>();
list.forEach(element -> persons.computeIfAbsent(element.personalId, s -> new HashMap<>()).put(element.rowNumber, element));

Here is a way to solve this with streams. But note that the objects must have a unique personId / rowNumber :

Map<String, List<MyObject>> rows = list.stream().collect(
        Collectors.toMap(element -> element.personalId, 
        element -> new ArrayList<MyObject>(Arrays.asList(element))));

As well as for the other map:

    Map<String, Map<Integer, MyObject>> persons = list.stream().collect(
            Collectors.toMap(e -> e.personalId, 
            e -> new HashMap<>(Map.of(e.rowNumber, e))));

Map<String, Map<Integer, List<MyObject>>> object using streams without using Collectors.groupingBy()

By looking at the map type, I can assume that a combination of personalId and rowNumber is not unique, ie there could be multiple occurrences of each combination (otherwise you don't need to group objects into lists). And there could be different rowNumber associated with each personalId . Only if these conclusions correct, this nested collection might have a very vague justification for existence.

Otherwise, you probably can substitute it multiple collections for different use-cases, for Map<String, MyObject> - object by id (if every id is unique):

Map<String, MyObject> objById = myList.stream()
    .collect(Collectors.toMap(
        MyObject::getPersonalId,
        Function.identity()
    ));

I'll proceed assuming that you really need such a nested collection.

Now, let's address the issue with groupingBy() . This collector uses internally Objects.requireNonNull() to make sure that a key produced by the classifier function is non-null .

If you tried to use and failed because of the hostility to null-keys , that implies that either personalId , or rowNumber , or both can be null .

Now let's make a small detour and pose the question of what does it imply if a property that considered to be significant (you're going to use personalId and rowNumber to access the data, hence they are definitely important) and null means first of all?

null signifies the absence of data and nothing else. If in your application null values have an additional special meaning, that's a design flaw. If properties that are significant for managing your data for some reason appear to be null you need to fix that.

You might claim that you're quite comfortable with null values. If so let pause for a moment and imagine a situation: person enters a restaurant, orders a soup and asks the waiter to bring them a fork instead of spoon (because of they have a negative experience with a spoon, and they are enough comfortable with fork).

null isn't a data, it's an indicator of the absence of data, storing null is an antipattern. If you're storing null it obtains a special meaning because you're forced to treat it separately.

To replace personalId and rowNumber that are equal to null with default values we need only one line of code.

public static void replaceNullWithDefault(List<MyObject> list,
                                          String defaultId,
                                          Integer defaultNum) {
    
    list.replaceAll(obj -> obj.getPersonalId() != null && obj.getRowNumber() != null ? obj :
        new MyObject(Objects.requireNonNullElse(obj.getPersonalId(), defaultId),
            Objects.requireNonNullElse(obj.getRowNumber(), defaultNum),
            obj.getDescription()));
}

After that can use the proper tool instead eating soup with a fork, I mean we can process the list data with groupingBy() :

public static void main(String[] args) {
    List<MyObject> myList = new ArrayList<>(
        List.of(
            new MyObject("id1", 1, "desc1"),
            new MyObject("id1", 1, "desc2"),
            new MyObject("id1", 2, "desc3"),
            new MyObject("id1", 2, "desc4"),
            new MyObject("id2", 1, "desc5"),
            new MyObject("id2", 1, "desc6"),
            new MyObject("id2", 1, "desc7"),
            new MyObject(null, null, "desc8")
        ));
    
    replaceNullWithDefault(myList, "id0", 0); // replacing null values
    
    Map<String, Map<Integer, List<MyObject>>> byIdAndRow = myList // generating a map
        .stream()
        .collect(Collectors.groupingBy(
            MyObject::getPersonalId,
            Collectors.groupingBy(MyObject::getRowNumber)
        ));
    
    byIdAndRow.forEach((k, v) -> { // printing the map
        System.out.println(k);
        v.forEach((k1, v1) -> System.out.println(k1 + " -> " + v1));
    });
}

Output:

id0
0 -> [MyObject{'id0', 0, 'desc8'}]
id2
1 -> [MyObject{'id2', 1, 'desc5'}, MyObject{'id2', 1, 'desc6'}, MyObject{'id2', 1, 'desc7'}]
id1
1 -> [MyObject{'id1', 1, 'desc1'}, MyObject{'id1', 1, 'desc2'}]
2 -> [MyObject{'id1', 2, 'desc3'}, MyObject{'id1', 2, 'desc4'}]

A link to Online Demo

Now, please pay attention to the usage of groupingBy() did you notice its conciseness. That's the right tool which allows generating even such a clumsy nested map.

And now we're going to eat the soup with a fork! All null properties would be used as is:

public static void main(String[] args) {
    List<MyObject> myList = new ArrayList<>(
        List.of(
            new MyObject("id1", 1, "desc1"),
            new MyObject("id1", 1, "desc2"),
            new MyObject("id1", 2, "desc3"),
            new MyObject("id1", 2, "desc4"),
            new MyObject("id2", 1, "desc5"),
            new MyObject("id2", 1, "desc6"),
            new MyObject("id2", 1, "desc7"),
            new MyObject(null, null, "desc8")
        ));
    
    Map<String, Map<Integer, List<MyObject>>> byIdAndRow = myList // generating a map
        .stream()
        .collect(
            HashMap::new,
            (Map<String, Map<Integer, List<MyObject>>> mapMap, MyObject next) ->
                mapMap.computeIfAbsent(next.getPersonalId(), k -> new HashMap<>())
                    .computeIfAbsent(next.getRowNumber(), k -> new ArrayList<>())
                    .add(next),
            (left, right) -> right.forEach((k, v) -> left.merge(k, v,
                (oldV, newV) -> {
                    newV.forEach((k1, v1) -> oldV.merge(k1, v1,
                        (listOld, listNew) -> {
                            listOld.addAll(listNew);
                            return listOld;
                        }));
                    return oldV;
                }))
        );
    
    byIdAndRow.forEach((k, v) -> { // printing the map
        System.out.println(k);
        v.forEach((k1, v1) -> System.out.println(k1 + " -> " + v1));
    });
}

Output:

null
null -> [MyObject{'null', null, 'desc8'}]
id2
1 -> [MyObject{'id2', 1, 'desc5'}, MyObject{'id2', 1, 'desc6'}, MyObject{'id2', 1, 'desc7'}]
id1
1 -> [MyObject{'id1', 1, 'desc1'}, MyObject{'id1', 1, 'desc2'}]
2 -> [MyObject{'id1', 2, 'desc3'}, MyObject{'id1', 2, 'desc4'}]

A link to Online Demo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM