Search code examples
javajava-streamcollectors

Java collect with grouping and mapping for `set`, but need an empty set if all values are `null`


In my Java 11 application, I want to get Product Updates from a repository. One Product Update has a updateId and a list of productIds to update.

  • If there are no product numbers that should be updated for update with updateId = X, I still want to write to another table that I've processed the update X; updateStatusRepository.setStatusProcessing(updateId) and updateStatusRepository.setStatusProcessed(updateId) should still be called for this updateId.

  • If there are product updates present, they should be processed in the ProductProcessingService.

For now, the groupingBy and mapping give me a Set with a null entry instead of an empty set, which is why I later remove all null Product IDs.

List<ProductUpdate> productUpdateList = updateStatusRepository.getProductUpdates();
Map<String, Set<String>> productUpdateMap = productUpdateList
          .stream()
          .collect(
              Collectors.groupingBy(
                  ProductUpdate::getUpdateId,
                  Collectors.mapping(ProductUpdate::getProductNo, Collectors.toSet())));

productUpdateMap.forEach(
          (updateId, productIds) -> {
        try {
          updateStatusRepository.setStatusProcessing(updateId);
          productIds.remove(null);
          if(!productIds.isEmpty()) {
            productProcessingService.performProcessing(Lists.newArrayList(productIds));
          }
          updateStatusRepository.setStatusProcessed(updateId);
        } catch (Exception e) {
              //
        }
});

I'd prefer if it were possible to use mapping in such a way that it delivers an empty Set directly if all values are null.

Is there a way to do this elegantly?


Solution

  • You could use Collectors.filtering:

    Map<String, Set<String>> productUpdateMap = productUpdateList
          .stream()
          .collect(Collectors.groupingBy(
                   ProductUpdate::getVersionId,
                   Collectors.mapping(ProductUpdate::getProductNo, 
                                      Collectors.filtering(Objects::nonNull, 
                                                           Collectors.toSet()))));
    

    I think Collectors.filtering fits your exact use case: it will filter out null product numbers, leaving an empty set if all product numbers happen to be null.


    EDIT: Note that in this case, using Collectors.filtering as a downstream collector is not the same as using Stream.filter before collecting. In the latter case, if we filtered out elements with a null product number before collecting, we might end up with a map without entries for some version id, i.e. in case all product numbers are null for one specific version id.

    From Collectors.filtering docs:

    API Note:

    The filtering() collectors are most useful when used in a multi-level reduction, such as downstream of a groupingBy or partitioningBy. For example, given a stream of Employee, to accumulate the employees in each department that have a salary above a certain threshold:

    Map<Department, Set<Employee>> wellPaidEmployeesByDepartment
      = employees.stream().collect(
        groupingBy(Employee::getDepartment,
                   filtering(e -> e.getSalary() > 2000,
                             toSet())));
    

    A filtering collector differs from a stream's filter() operation. In this example, suppose there are no employees whose salary is above the threshold in some department. Using a filtering collector as shown above would result in a mapping from that department to an empty Set. If a stream filter() operation were done instead, there would be no mapping for that department at all.


    EDIT 2: I think it's worth mentioning the alternative proposed by @Holger in the comments:

    Map<String, Set<String>> productUpdateMap = productUpdateList
          .stream()
          .collect(Collectors.groupingBy(
                   ProductUpdate::getVersionId, 
                   Collectors.flatMapping(pu -> Stream.ofNullable(pu.getProductNo()), 
                                          Collectors.toSet())));