This nested list processing is not only ugly, but also inefficient. You are always better off storing the identifiers of one list in Set , allowing an efficient search, and then process another list using Set . Thus, you do not perform the operations list1.size() times list2.size() , but list1.size() plus list2.size() , which is a significant difference for larger lists. Then, since both operations are essentially the same, it is worth abstracting them into a method:
public static <A,B,R,ID> List<R> extract( List<A> l1, List<B> l2, Function<A,ID> aID, Function<B,ID> bID, Function<A,R> r) { Set<ID> b=l2.stream().map(bID).collect(Collectors.toSet()); return l1.stream().filter(a -> !b.contains(aID.apply(a))) .map(r).collect(Collectors.toList()); }
This method can be used as
List<NamedDTO> added = extract(listTwo, listOne, NamedDTO::getId, SomeObject::getId, Function.identity()); List<NamedDTO> removed = extract(listOne, listTwo, SomeObject::getId, NamedDTO::getId, so -> new NamedDTO(so.getId(), so.getName()));
Since replacing two lists requires the helper method to be independent of element types, it expects a function to access the id property, which can be set using method references. Then you need a function that describes the result element, which is an identical function in one case (just getting NamedDTO ) and a lambda expression that NamedDTO from SomeObject in another.
The operation itself is performed in the same straightforward manner as described above, iterating over one list, matching with the identifier and collecting in Set , and then iterating over the other list, saving only the elements whose identifier is not in the set, compare the result type and take it to List .