Jai
Jai

Reputation: 8363

Java: Find common item on nested list using Stream API

Let's say I have a List<List<Animal>> animals. This nested list represents a list of places where each place contains a list of animals.

I need to find out a list of animal types that appears in at least two different places. I know I can do normal loops and do that. Is there any way this can be done via Stream API?

Example:

List<List<Animal>> animals = new ArrayList<>();
animals.add(Arrays.asList(new Dog(), new Cat()));
animals.add(Arrays.asList(new Dog(), new Bird()));
animals.add(Arrays.asList(new Bird()));

Expected (equivalent of):

List<Class<? extends Animal>> animalTypes = Arrays.asList(Dog.class, Bird.class);

As for attempt, I only managed to convert the inner list to a set of classes:

animals.stream().map(place -> place.stream().map(animal -> animal.getClass()).collect(Collectors.toSet()));

Update

The code to do this without Stream API:

final List<List<Animal>> animals = new ArrayList<>();
animals.add(Arrays.asList(new Dog(), new Cat()));
animals.add(Arrays.asList(new Dog(), new Bird()));
animals.add(Arrays.asList(new Bird()));

final Map<Class<? extends Animal>, Integer> count = new HashMap<>();

for (final List<Animal> place : animals) {
    final Set<Class<? extends Animal>> uniqueTypes = new HashSet<>();

    for (final Animal animal : place) {
        uniqueTypes.add(animal.getClass());
    }

    for (final Class<? extends Animal> type : uniqueTypes) {
        if (!count.containsKey(type))
        {
            count.put(type, 1);
        }
        else
        {
            count.put(type, count.get(type).intValue() + 1);
        }
    }
}

final List<Class<? extends Animal>> typesAppearingAtLeastAtTwoPlaces = new ArrayList<>();

for (final Class<? extends Animal> type : count.keySet()) {
    if (count.get(type).intValue() >= 2) {
        typesAppearingAtLeastAtTwoPlaces.add(type);
    }
}

System.out.println(typesAppearingAtLeastAtTwoPlaces);

Output:

[class Test$Dog, class Test$Bird]

Upvotes: 1

Views: 701

Answers (3)

123-xyz
123-xyz

Reputation: 637

I think you can also try StreamEx. It gives you the chance to write more concise codes with better readability:

StreamEx.of(animals)
    .flatMap(e -> e.stream().map(Animal::getClass).distinct())
    .distinct(2).toList();

Upvotes: 2

Hearen
Hearen

Reputation: 7828

First and foremost, probably you should use flatMap instead of map in your attempt.

animals.stream().map(place -> place.stream().map(animal -> animal.getClass()).collect(Collectors.toSet()));

Second, actually we can do that using an external ConcurrentHashMap which will enable us to use parallel when needed.

    ConcurrentHashMap<Class, AtomicLong> theCounterMap = new ConcurrentHashMap<>();
    animals.stream().flatMap(list -> list.stream().map(animal -> animal.getClass()).distinct())
        .forEach(clazz -> theCounterMap.computeIfAbsent(clazz, k -> new AtomicLong()).getAndIncrement());
    List<Class> classList = theCounterMap.entrySet().stream()
            .filter(entry -> entry.getValue().get() > 1)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());

But if you need to track the source list (as the two different places) then you need to further modify the solution above.

Updated

Based on @shmosel's advice, you can directly use a simpler method to achieve the same as follows:

    Map<Class, Long> theCounterMap = animals.stream().flatMap(list -> list.stream().map(animal -> animal.getClass()).distinct())
        .collect(Collectors.groupingBy(e -> e, Collectors.counting()));
    List<Class> classList = theCounterMap.entrySet().stream()
            .filter(entry -> entry.getValue() > 1)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());

Upvotes: 0

Misha
Misha

Reputation: 28133

First, count all the animals and then select the ones that occur more than once:

import static java.util.stream.Collectors.*;
.....

Map<Class<? extends Animal>, Long> animalCounts = animals.stream()
        .flatMap(
                lst -> lst.stream()
                    .map(a -> a.getClass())
                    .distinct()   // in case several of the same animal are in the same place
        )
        .collect(groupingBy(x -> x, counting()));

List<Class<? extends Animal>> animalTypes = animalCounts.entrySet().stream()
        .filter(e -> e.getValue() > 1)
        .map(Map.Entry::getKey)
        .collect(toList());

Upvotes: 5

Related Questions