Issue
I have an interface Animal
and several implementations of it:
interface Animal {}
class Cat implements Animal {}
class Dog implements Animal {}
class Hamster implements Animal {}
class Participant {
private Animal animal;
private List<String> names;
}
I need to group Animal
objects based on the class type (Cat
/Dog
/Hamster
) and merge names
lists using streams.
For example:
From originalList
I need to get the resultList
:
List<Participant> originalList = List.of(
new Participant(new Cat(), List.of("Leo")),
new Participant(new Cat(), List.of("Simba")),
new Participant(new Dog(), List.of("Charlie", "Ralph")),
new Participant(new Dog(), List.of("Max")),
new Participant(new Hamster(), List.of("Snowy"))
);
List<Participant> resultList = List.of(
new Participant(new Cat(), List.of("Leo", "Simba")),
new Participant(new Dog(), List.of("Charlie", "Ralph", "Max")),
new Participant(new Hamster(), List.of("Snowy"))
);
I was only able to cast the list to a map, but I don't know how to further cast that to a list
Map<Class<? extends Animal>, List<Participant>> result = list.stream()
.collect(Collectors.groupingBy(it -> it.getAnimal().getClass()));
Solution
Well, if you really want you can access no-args constructor by invoking getConstructor()
on the instance of Class<T>
but it might throw NoSuchMethodException
which is checked, and then invoke newInstance()
which in turn throws a bunch of exceptions, dealing with them inside the lambda isn't a great idea, hence we need to extract this logic into a separate method.
And that's how we can obtain instances of Cat
, Dog
and Hamster
which are needed to create resulting Participant
objects.
The method responsible for creating a new instance of Animal
subtypes by the means of reflection might be written like this:
public static <T extends Animal> Animal createAnimal(Class<T> animalClass) {
try {
return animalClass.getConstructor().newInstance();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
throw new IllegalArgumentException("Unable create instance of " + animalClass.getSimpleName());
}
Which looks scary, but in case if you want to see the full reflection-based solution it's here.
Alternative Approach - defining an auxiliary Record
There's a better way, than using reflection.
As @Holger has pointed out in the comments, we can define a record (or a class) which would wrap an instance of Animal
and override equals/hashCode
contract based on the class of animal.
record Key(Animal animal) {
@Override
public int hashCode() {
return animal.getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof Key k && k.animal.getClass() == animal.getClass();
}
}
And then we can use this record to group participants by animal type inside the groupingBy()
.
List<Participant> originalList = List.of(
new Participant(new Cat(), List.of("Leo")),
new Participant(new Cat(), List.of("Simba")),
new Participant(new Dog(), List.of("Charlie", "Ralph")),
new Participant(new Dog(), List.of("Max")),
new Participant(new Hamster(), List.of("Snowy"))
);
List<Participant> resultList = originalList.stream()
.collect(Collectors.groupingBy(
participant -> new Key(participant.getAnimal()),
Collectors.flatMapping(participant -> participant.getNames().stream(),
Collectors.toList())
))
.entrySet().stream()
.map(entry -> new Participant(entry.getKey().animal(), entry.getValue()))
.collect(Collectors.toList());
resultList.forEach(System.out::println);
Output:
Participant{animal=CAT, names=[Leo, Simba]}
Participant{animal=DOG, names=[Charlie, Ralph, Max]}
Participant{animal=HAMSTER, names=[Snowy]}
Alternative Approach - changing the overall Class-Design
Since your classes Cat
, Dog
and Hamster
completely luck attributes, you can substitute all as well as interface Animal
with an enum (and even you've provided a simplified code and these classes do have attributes, using dummy objects created via no-args constructor which are not really meant to represent particular instances of Animal
doesn't look very clean, so you can have them both - your classes and the enum, and they will serve different purposes).
Animal { CAT, DOG, HAMSTER }
And things would become more simpler:
List<Participant> originalList = List.of(
new Participant(Animal.CAT, List.of("Leo")),
new Participant(Animal.CAT, List.of("Simba")),
new Participant(Animal.DOG, List.of("Charlie", "Ralph")),
new Participant(Animal.DOG, List.of("Max")),
new Participant(Animal.HAMSTER, List.of("Snowy"))
);
List<Participant> resultList = originalList.stream()
.collect(Collectors.groupingBy(Participant::getAnimal,
Collectors.flatMapping(participant -> participant.getNames().stream(),
Collectors.toList())
))
.entrySet().stream()
.map(entry -> new Participant(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
resultList.forEach(System.out::println);
Output:
Participant{animal=CAT, names=[Leo, Simba]}
Participant{animal=DOG, names=[Charlie, Ralph, Max]}
Participant{animal=HAMSTER, names=[Snowy]}
Answered By - Alexander Ivanchenko
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.