简体   繁体   中英

Combine two collections with predicate using Stream API

I have two collections of type Entity .

class Entity {
    long id;
    // other fields
}

Collection<Entity> first = ...
Collection<Entity> second = ...

I need to combine them in a such way that all elements in first should be replaced by elements from second if their ids are equal.

So, eg if first contains [{"id": 1}, {"id": 2}, {"id": 3}] and second contains [{"id": 3}, {"id": 4}, {"id": 5}]

the result should be like this: [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}] where {"id": 3} from second replaced {"id": 3} from first.

Is there a way how to implement it in a Java 8 way, using Stream API.

While it is possible to do the entire operation with one stream operation, the result would not be very efficient due to the repeated iteration over the collections. It's strongly recommended to use two operations here

Map<Long,Entity> secondById = second.stream()
    .collect(Collectors.toMap(Entity::getId, Function.identity()));

Collection<Entity> result = first.stream()
    .map(e -> secondById.getOrDefault(e.getId(), e))
    .collect(Collectors.toList());

Note that I took your task description “elements in first should be replaced by elements from second if their ids are equal” literally, eg it will not insert elements from the second not having a matching element in first. If first has a defined encounter order, it will be retained.

Note that if first is a mutable list, you could replace the elements in-place:

List<Entity> first = ...
Collection<Entity> second = ...

Map<Long,Entity> secondById = second.stream()
    .collect(Collectors.toMap(Entity::getId, Function.identity()));

first.replaceAll(e -> secondById.getOrDefault(e.getId(), e));

Changing it to not just replace elements of first , but add all elements of second is not so hard:

Map<Long,Entity> secondById = second.stream()
    .collect(Collectors.toMap(Entity::getId, Function.identity()));

Collection<Entity> result = Stream.concat(
    first.stream().filter(e -> !secondById.containsKey(e.getId())),
    second.stream()
  ).collect(Collectors.toList());

Maybe some old-fashioned, simple Collection methods can help you :

first.removeAll(second);
first.addAll(second);

Note you have to implement both hashCode and equals to use id as a parameter in your Entity class.

Ideone demo


Edit

As you cannot change equals , you can use Collection.removeIf to replicate the effect of removeAll :

second.forEach(entity2 -> first.removeIf(entity1 -> entity1.getId() == entity2.getId()));
first.addAll(second);

Ideone demo 2

The stream form of first.removeAll(second).addAll(second) so to say:

Stream<Entity> result = Stream.concat(
    first.stream().filter(entity -> !second.contains(entity)),
    second.stream());

This assumes that the equal entities in second differ from first by a non-id field value, not relevant for equality.

You can use Map.merge(..) to chose the newer record. In the example bellow it is functionally equivalent to Map.put(..), but allows you to compare other Entity attributes, such as a creation timestamp or other.

// Extract Entity::getId as a Map key, and keep the Entity object as value
Map<Long, Entity> result = first.stream()
               .collect(Collectors.toMap(Entity::getId, Function.identity()));

// Merge second collection into the result map, overwriting existing records  
second.forEach(entity -> result.merge(entity.getId(), entity, (oldVal, newVal) -> newVal));

// Print result
result.values().forEach(System.out::println);

If you want to use Stream API only, then you can concat the two Collections into a single Stream using Stream.concat(...) and then write a custom Collector

Map<Long, Entity> result2 = Stream.concat(first.stream(), second.stream())
        .collect(LinkedHashMap::new, // the placeholder type
                (map, entity) -> map.merge(entity.getId(), entity, (oldVal, newVal) -> newVal),
                (map1, map2) -> {}); // parallel processing will reorder the concat stream so ignore

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