user3127896
user3127896

Reputation: 6593

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, e.g 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.

Upvotes: 0

Views: 1184

Answers (4)

Youcef LAIDANI
Youcef LAIDANI

Reputation: 60046

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

Upvotes: 1

Ianislav Trendafilov
Ianislav Trendafilov

Reputation: 56

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

Upvotes: 0

Holger
Holger

Reputation: 298539

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, e.g. 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());

Upvotes: 2

Joop Eggen
Joop Eggen

Reputation: 109613

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.

Upvotes: 1

Related Questions