Search code examples
javajava-streamhashsetcollectors

Unable to apply Merge function to convert to get Set on a Stream output


Here is a sample source data structure to simplify the problem case data for my scenario. This Map has duplicate descriptions for different codes.

Map<Integer, String> descriptionsByCode = new HashMap();
descriptionsByCode.put(1, "description 1");
descriptionsByCode.put(2, "description 2");
descriptionsByCode.put(4, "description 3");
descriptionsByCode.put(5, "description 4");
descriptionsByCode.put(6, "description 5");
descriptionsByCode.put(7, "description 5");
descriptionsByCode.put(8, "description 5");
descriptionsByCode.put(9, "description 2");
descriptionsByCode.put(10, "description 2");
descriptionsByCode.put(11, "description 3");
descriptionsByCode.put(12, "description 4");
descriptionsByCode.put(13, "description 1");
descriptionsByCode.put(14, "description 6");
descriptionsByCode.put(15, "description 8");

I also have a complex business object, but for simplicity purpose lets assume there is an object with only 2 field.

@Data
@AllArgsConstructor
class StatusCounts {
    private String description;
    private Map<Integer, Integer> someIemCountByStatusCode; //defaults to zero for our example
}

If, I want to have a map of StatusCount object By Description. I can have it easily. Below is a working code, added here to concentrate on the main problem mentioned after this

     Map<String, StatusCounts> statusCountsByDescription
            = descriptionsByCode
            .entrySet()
            .stream()
            .collect(Collectors.toMap(
                    e -> e.getValue(),  //key function
                    e -> new StatusCounts(e.getValue(),   //value function 
                            Collections.singletonMap(e.getKey(), 0)),
                    (e, v) ->  // merge function for collision 
                    { // is there a better way to write below code ? 
                        Map m = new HashMap();
                        m.putAll(e.getItemCountByStatusCode());
                        m.putAll(v.getItemCountByStatusCode());
                        e.setItemCountByStatusCode(m);
                        return e;
                    }
            ));

Above code is not clean, but working well. What I actually want is, a Set instead of Map

I am unable to find a way, in reduce or collect functions, to provide a handling for collision detection (like merge function does in case of Collectors.toMap).

The combiner function in both reduce or collect, doesn't work for collision.
Collectors.toSet doesn't even accept any parameter. So I though I can write collision detection handling in accumulator in case of reduce or in or Biconsumer in case of collect. So I tried below using collect & BiConsumer But this one is uncompilable same is happening with reduce

Set<StatusCounts> statusCounts
            = descriptionsByCode
            .entrySet()
            .stream()
            .collect(() -> new HashSet<StatusCounts>(),
                    (set, entry) -> {
                        Optional<StatusCounts> statusCount
                                = findStatusCountWithSameDescription(set, entry.getValue());

                        if (statusCount.isPresent()) {
                            statusCount.get()
                                        .getItemCountByStatusCode()
                                        .put(entry.getKey(), 0);
                        } else {
                            set.add(new StatusCounts(
                                    entry.getValue(),
                                    Collections.singletonMap(entry.getKey(), 0)));
                        }
                        return set;
                    }
            );



private static Optional<StatusCounts> findStatusCountWithSameDescription
       (HashSet<StatusCounts> set, String description) {
        return set.stream()
                  .filter(e -> e.getDescription()
                                .equalsIgnoreCase(description))
                  .findFirst();
    }

This is the compilation error

Cannot resolve method 'collect(<lambda expression>, <lambda expression>)'

Solution

  • You could have used Collectors.groupingBy to solve this:

    Map<String, Set<Integer>> map 
        = descMap.entrySet()
                 .stream()
                 .collect(Collectors.groupingBy(Entry::getValue, 
                                                Collectors.mapping(Entry::getKey, Collectors.toSet())));                                       
    

    Here, a map would be generated with the description as the key. Collectors.mapping maps each entry to the status code and then combines them into a Set

    I think that a Set<Integer> instead of Map<Integer, Integer> would have been sufficient within the StatusCounts class. This is because from the code, it seems like you just want to know the different codes under a description. In that case you could try the below:

    return map.entrySet()
              .stream()
              .map(entry -> new StatusCounts(entry.getKey(), entry.getValue()))
              .collect(Collectors.toSet());
    

    UPDATE

    If you are not allowed to change the existing StatusCounts class, then you could try the below:

    Map<String, Map<Integer, Integer>> tempMap
        = descMap.entrySet()
                 .stream()
                 .collect(Collectors.groupingBy(Entry::getValue, 
                                                Collectors.mapping(Entry::getKey, Collectors.toMap(Function.identity(), v -> 0))));
    
    // You may combine the below code with above instead of using a tempMap.
    // I have separated it for simplicity
    Set<StatusCounts> result 
        = tempMap.entrySet()
                 .stream()
                 .map(entry -> new StatusCounts(entry.getKey(), entry.getValue()))
                 .collect(Collectors.toSet());