Home > Software design >  partition a stream and do something different with each part
partition a stream and do something different with each part

Time:10-28

I have a List of objectIds and a map of <id,object>. I need to go through my list of objIds, and for each one - if it's in the map, add it to a new list of objects, and if it's not in the list, add the id to a list of missing ids.

I can easily do this using a simple for each loop:

    List<String> myIdList = Arrays.asList("a", "b", "c");
    Map<String,Object> myObjectMap = new HashMap<>();
    
    List<Object> objectList = new ArrayList<>();
    List<String> missingObjIds = new ArrayList<>();
    for(String id : myIdList) {
        Object obj = myObjectMap.get(id);
        if(obj == null) {
            missingObjIds.add(id);
        } else {
            objectList.add(obj);
        }
    }

I would like to do this in a less verbose way using the Stream API, but I'm not sure how. I tried partitioning the stream, but that got me either a map with 2 lists of ids (which would necessitate going through the list again to retrieve the mapped objects) or a list of objects with nulls for all the missing objects and no way to get the missing ids.

I'm sure there has to be a way to do it. Any ideas?

CodePudding user response:

It's not very efficient, but you can do it.

List<String> myIdList = Arrays.asList("a", "b", "c");
Map<String,Object> myObjectMap = new HashMap<>();
myObjectMap.put("b", "B");

Map<Boolean, List<String>> partitioned = myIdList.stream()
    .collect(Collectors.partitioningBy(myObjectMap::containsKey));

List<Object> objectList = partitioned.get(true).stream()
    .map(myObjectMap::get).collect(Collectors.toList());
List<String> missingObjIds = partitioned.get(false);

System.out.println("objectList="   objectList);
System.out.println("missingObjIds="   missingObjIds);

output:

objectList=[B]
missingObjIds=[a, c]

CodePudding user response:

This can be done in a single stream-statement and without side-effects by using a custom Collector, or make use of the three-args version of collect(). I'll go with the latter option.

For that, we would need to define a custom accumulation type, which should be represented by a mutable object capable of consuming stream elements and producing the final result.

Since we need to obtain two different lists, the result of the stream execution should the accumulation type itself exposing these two lists.

So, in a nut-shell, the custom type, let's call it Partitioner, which we would use to perform mutable reduction should wrap two list. It would be more versatile if we make it generic and add a Function and Predicate, which would be involved in partitioning the data to its properties.

And for convenience, we can implement Consumer interface.

That's how the stream might be implemented:

public static void main(String[] args) {
    List<String> myIdList = Arrays.asList("a", "b", "c");
    Map<String, Object> myObjectMap = Map.of("a", "Alice");
    
    Partitioner<String, Object> resulttttt = myIdList.stream()
        .collect(
            () -> new Partitioner<>(myObjectMap::containsKey, myObjectMap::get),
            Partitioner::accept,
            Partitioner::merge
        );
    
    List<Object> objectList = ;
    List<String> missingObjIds = result.getKeys();

    System.out.println(result.getItems());
    System.out.println(result.getKeys());
}

Output:

[Alice] // Values assated with the Keys that match the IDs from the list
[b, c]  // missing IDs

And that's how the custom accumulation type might look like:

public class Partitioner<K, R> implements Consumer<K> {
    
    private List<R> items = new ArrayList<>(); // objectList
    private List<K> keys = new ArrayList<>();  // missingObjIds
    private Predicate<K> shouldStoreItem;
    private Function<K, R> mapper;
    
    public Partitioner(Predicate<K> shouldStoreItem, Function<K, R> mapper) {
        this.shouldStoreItem = shouldStoreItem;
        this.mapper = mapper;
    }
    
    @Override
    public void accept(K k) {
        if (shouldStoreItem.test(k)) items.add(mapper.apply(k));
        else keys.add(k);
    }
    
    public Partitioner<K, R> merge(Partitioner<K, R> other) {
        items.addAll(other.items);
        keys.addAll(other.keys);
        return this;
    }
    
    // getters
}

CodePudding user response:

If you really want to use a stream here is one way.

  • this creates a an entry based on the presence of the id.
  • it then groups those independent entries by the keys "Missing" and "Present"

The Data

 List<String> myIdList = Arrays.asList("a", "b", "c");
 Map<String, Object> myObjectMap = new HashMap<>();
 myObjectMap.put("c", "C");

The process

Map<String, List<Object>> map = myIdList.stream()
         .<Entry<String, Object>>mapMulti((id, consumer) -> {
             Entry<String, Object> entry;
             Object obj = myObjectMap.get(id);
             if (obj == null) {
                 entry = new SimpleEntry<>("Missing", id);
             } else {
                 entry = new SimpleEntry<>("Present", obj);
             }
             consumer.accept(entry);
         }).collect(Collectors.groupingBy(Entry::getKey, Collectors
                 .mapping(Entry::getValue, Collectors.toList())));

System.out.println(map.get("Present"));
System.out.println(map.get("Missing"));

prints

[C]
[a, b]

But I would you just stick with what you have. It makes sense and works.

  • Related