Reputation: 6593
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
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.
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);
Upvotes: 1
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
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
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