简体   繁体   中英

Merging objects from two list with unique id using Java 8

class Human {
    Long humanId;
    String name;
    Long age;
}


class SuperHuman {
    Long superHumanId;
    Long humanId;
    String name;
    Long age;
}

I've two lists. List of humans and List of superHumans. I want to create a single list out of the two making sure that if a human is superhuman, it only appears once in the list using java 8. Is there a neat way to do it? UPDATE: These are different classes ie neither extends the other. I want the final list to be of superhumans. If a human already is superhuman, we ignore that human object. If a human is not a superhuman, we convert the human object into the super human object. Ideally I would want to sort them by their age at the end so that I get a list of superhuman sorted by date in descending order.

Based on your updated question:

List<Human> humans = ... // init 
List<SuperHuman> superHumans = ... // init

Set<Long> superHumanIds = superHumans.stream()
    .map(SuperHuman::getHumanId)
    .collect(toSet());

humans.stream()
    .filter(human -> superHumanIds.contains(human.getHumanId()))
    .map(this::convert)
    .forEach(superHumans::add);

superHumans.sort(Comparator.comparing(SuperHuman::getAge));

Assuming this class has another method with the following signature:

private Superhuman convert(Human human) {
    // mapping logic
}

You do have other suggestions about how your code should be re-factored to make it better, but in case you can't do that, there is a way - via a custom Collector that is not that complicated.

A custom collector also gives you the advantage of actually deciding which entry you want to keep - the one that is already collected or the one that is coming or latest in encounter order wins. It would require some code changes - but it's doable in case you might need it.

 private static <T> Collector<Human, ?, List<Human>> noDupCollector(List<SuperHuman> superHumans) {
    class Acc {

        ArrayList<Long> superIds = superHumans.stream()
                .map(SuperHuman::getHumanId)
                .collect(Collectors.toCollection(ArrayList::new));

        ArrayList<Long> seen = new ArrayList<>();

        ArrayList<Human> noDup = new ArrayList<>();

        void add(Human elem) {
            if (superIds.contains(elem.getHumanId())) {

                if (!seen.contains(elem.getHumanId())) {
                    noDup.add(elem);
                    seen.add(elem.getHumanId());
                }

            } else {
                noDup.add(elem);
            }
        }

        Acc merge(Acc right) {
            noDup.addAll(right.noDup);
            return this;
        }

        public List<Human> finisher() {
            return noDup;
        }

    }
    return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
}

Supposing you have these entries:

List<SuperHuman> superHumans = Arrays.asList(
            new SuperHuman(1L, 1L, "Superman"));
    //
    List<Human> humans = Arrays.asList(
            new Human(1L, "Bob"),
            new Human(1L, "Tylor"),
            new Human(2L, "John"));

Doing this:

List<Human> noDup = humans.stream()
            .collect(noDupCollector(superHumans));

System.out.println(noDup); // [Bob, Tylor]

Try this.

List<Object> result = Stream.concat(
    humans.stream()
        .filter(h -> !superHumans.stream()
            .anyMatch(s -> h.humanId == s.humanId)),
    superHumans.stream())
    .collect(Collectors.toList());

Suppose neither of the classes inherit the other one. I can imagine you have two lists:

Human alan = new Human(1, "Alan");
Human bertha = new Human(2, "Bertha");
Human carl = new Human(3, "Carl");
Human david = new Human(4, "David");
SuperHuman carlS = new SuperHuman(1, 3, "Carl");
SuperHuman eliseS = new SuperHuman(2, 0, "Elise");

List<Human> humans = new ArrayList<>(Arrays.asList(alan, bertha, carl, david));
List<SuperHuman> superHumans = new ArrayList<>(Arrays.asList(carlS, eliseS));

We see that Carl is listed as a human, and also as a superhuman. Two instances of the very same Carl exist.

List<Object> newList = humans.stream()
    .filter((Human h) -> {
        return !superHumans.stream()
            .anyMatch(s -> s.getHumanId() == h.getHumanId());
    })
    .collect(Collectors.toList());
newList.addAll(superHumans);

This code tries to filter the list of humans, excluding all entries whose humanId exist in the list of superhumans. At last, all superhumans are added.


This design has several problems:

  • Intuitively, the classes are related, but your code says otherwise. The fact that you are trying to merge, suggests the types are related.
  • The classes have overlapping properties ( humanId and name ) which as well suggest that the classes are related.

The assumption that the classes are related, is definitely not unfounded.

As other commenters suggested, you should redesign the classes:

class Human {
    long id; // The name 'humanId' is redundant, just 'id' is fine
    String name;
}

class SuperHuman extends Human {
    long superHumanId; // I'm not sure why you want this...
}
Human alan = new Human(1, "Alan");
Human bertha = new Human(2, "Bertha");
Human carl = new SuperHuman(3, "Carl");
Human david = new Human(4, "David");
Human elise = new SuperHuman(5, "Elise");

List<Human> people = Arrays.asList(alan, bertha, carl, david, elise);

Then you have one single instance for each person. Would you ever get all superhumans from the list, just use this:

List<Human> superHumans = people.stream()
    .filter((Human t) -> t instanceof SuperHuman)
    .collect(Collectors.toList());

superHumans.forEach(System.out::println); // Carl, Elise

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