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?
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 agroupingByorpartitioningBy. For example, given a stream ofEmployee, 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 emptySet. If a streamfilter()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())));
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With