Reputation: 8363
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()));
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
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
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.
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
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