3

Suppose I have this Java 8 code:

public class Foo {
    private long id;
    public getId() {
        return id;
    }

    //--snip--
}


//Somewhere else...

List<Foo> listA = getListA();
List<Foo> listB = getListB();

List<Foo> uniqueFoos = ???;

In List<Foo> uniqueFoos I want to add all elements of listA and listB so all Foos have unique IDs. I.e. if there is already a Foo in uniqueFoos that has a particular ID don't add another Foo with the same ID but skip it instead.

Of course there is plain old iteration, but I think there should be something more elegant (probably involving streams, but not mandatory), but I can't quite figure it out...

I can think of good solutions involving an override of the equals() method to basically return id == other.id; and using a Set or distinct(). Unfortunately I can't override equals() because object equality must not change.

What is a clear and efficient way to achieve this?

Hatch
  • 595
  • 2
  • 9
  • 18
  • `List uniqueFoos = Stream.concat(listA.stream(), listB.stream()) .collect(Collectors.collectingAndThen(Collectors.toMap(Foo::getId, Function.identity(), (a,b) -> a, LinkedHashMap::new), m -> new ArrayList<>(m.values())));` – Holger Apr 20 '18 at 15:12
  • 1
    that can be debatable, but code apparent simplicity is not elegance to me. A simple iteration is easily identifiable, understandable and doesn't perform unwanted extra actions, even if code is longer. I find them elegant. And you don't loose time for this on SO – Kaddath Apr 20 '18 at 15:13
  • 1
    Possible duplicate of [java 8 how to get distinct list on more than one property](https://stackoverflow.com/questions/42817884/java-8-how-to-get-distinct-list-on-more-than-one-property) – Pramod Apr 20 '18 at 15:17

3 Answers3

6

You can do it with Collectors.toMap:

Collection<Foo> uniqueFoos = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toMap(
        Foo::getId,
        f -> f,
        (oldFoo, newFoo) -> oldFoo))
    .values();

If you need a List instead of a Collection, simply do:

List<Foo> listUniqueFoos = new ArrayList<>(uniqueFoos);

If you also need to preserve encounter order of elements, you can use the overloaded version of Collectors.toMap that accepts a Supplier for the returned map:

Collection<Foo> uniqueFoos = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toMap(
        Foo::getId,
        f -> f,
        (oldFoo, newFoo) -> oldFoo,
        LinkedHashMap::new))
    .values();

I think it's worth adding a non-stream variant:

Map<Long, Foo> map = new LinkedHashMap<>();
listA.forEach(f -> map.merge(g.getId(), f, (oldFoo, newFoo) -> oldFoo));
listB.forEach(f -> map.merge(g.getId(), f, (oldFoo, newFoo) -> oldFoo));

Collection<Foo> uniqueFoos = map.values();

This could be refactored into a generic method to not repeat code:

static <T, K> Collection<T> uniqueBy(Function<T, K> groupBy, List<T>... lists) {
    Map<K, T> map = new LinkedHashMap<>();
    for (List<T> l : lists) {
        l.forEach(e -> map.merge(groupBy.apply(e), e, (o, n) -> o));
    }
    return map.values();
}

Which you can use as follows:

Collection<Foo> uniqueFoos = uniqueBy(Foo::getId, listA, listB);

This approach uses the Map.merge method.

fps
  • 33,623
  • 8
  • 55
  • 110
2

Something like this will do.

 List<Foo> uniqueFoos = Stream.concat(listA.stream(), listB.stream())
                              .filter(distinctByKey(Foo::getId))
                              .collect(Collectors.toList());


 public <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
  }
pvpkiran
  • 25,582
  • 8
  • 87
  • 134
1

You could write this one. This skips the second and next elements that have the same id thanks to filter() and the use of a Set that stores the encountered ids :

    Set<Long> ids = new HashSet<>();
    List<Foo> uniqueFoos = Stream.concat(getListA().stream(), getListB().stream())
                                 .filter(f -> ids.add(f.getId()))
                                 .collect(Collectors.toList());

It is not a full stream solution but it is rather straight and readable.

davidxxx
  • 125,838
  • 23
  • 214
  • 215