I have two classes ChildA
and ChildB
which are subclasses of Parent
.
I am given a list of Parent
and want to use Stream to map them to a Map<? extends Parent, List<? extends Parent>>
The issue is it seems the .map
part is removing the subclass. Here is some sample code illustrating my problem:
items.stream()
.map(item -> item.isA() ? item.getA() : item.getB()) //<--- issue here, returns List<Parent> instead of List<? extends Parent>
.collect(Collectors.groupingBy(i -> i.getClass()));
With the above, the output is: Map<? extends Class, List<Parent>>
but I want Map<?extends Class, List<? extends Parent>>
so I could then do something like:
map.get(A.class)
-> List<A>
So I can easily assign two variables:
List<A> a = map.get(a.class);
List<b> b = map.get(b.class);
Where as right now I would have to do something like:
map.get(A.class).stream().map(A::cast).collect(Collectors.toList());
Is there a way to get my desired result?
EDIT:
Here is a minimum reproducible example: https://replit.com/@rgurn/MinExample?v=1#Main.java
Note it was not my design choice to make the container class Item
and I cannot change it.
CodePudding user response:
OK, here is what I came up with. Do it the way you were but you will need to rebuild the lists by casting.
List<Item> items = List.of(new Item("a", new A()),
new Item("b", new B()));
Map<Class<?>, List<Parent>> map = items
.stream()
.map(item -> item.isA() ? item.getA() : item.getB())
.collect(Collectors.groupingBy(p->p.getClass()));
List<A> list = new ArrayList<>();
for (Parent p :map.get(A.class)) {
list.add(A.class.cast(p));
}
This doesn't scale well but you can always cast the contents of the List using the map Key. You should not get any class cast exceptions since you are separating them in the map according to class.
CodePudding user response:
After the sad realization I had a misunderstanding of List
and couldn't do what I initially tried, I ended up going with a Pair
based approach like @M. Prokhorov suggested:
Pair<ArrayList<A>, ArrayList<B>> collection = items.stream()
.map(i -> i.isA() ? i.getA() : i.getB())
.collect(() -> Pair.of(new ArrayList<A>(), new ArrayList<B>()), (p, u) -> {
if (u instanceof A) {
p.getFirst().add((A) u);
} else {
p.getSecond().add((B) u);
}
}, (p1, p2) -> {
p1.getFirst().addAll(p2.getFirst());
p1.getSecond().addAll(p2.getSecond());
});
List<A> aList = collection.getFirst();
List<B> bList = collection.getSecond();
Overall I'm not too happy about it. Something just feels off, maybe I should just KISS. If anyone has a better method of doing this I'd be happy to see it.
CodePudding user response:
Although I still don't think you need it, this a clean implementation of Item.class:
public class Item {
private final Parent item;
public Item(Parent item) {
this.item = item;
}
public Parent get() {
return item;
}
public boolean isA() {
return item instanceof A;
}
public boolean isB() {
return item instanceof B;
}
public A getA() {
return (A) item;
}
public B getB() {
return (B) item;
}
public String getType() {
return getClass().getSimpleName();
}
}
Your .map
-call does nothing except for mapping an Item
to a Parent
(whether or not item is A, it just adds the attribute item
back in).
But you cannot collect your Stream
as the desired Map
anyway (there is no such Map
in java), so I made one for you:
public class ClassMap<V> {
private final HashMap<Class<? extends V>, List<? extends V>> map;
public ClassMap() {
this.map = new HashMap<>();
}
public ClassMap(@NonNull List<V> input) {
this();
for (V v : input) add(v);
}
public <W extends V> void add(@NonNull W item) {
Class<W> clazz = (Class<W>) item.getClass();
List<W> list = (List<W>) map.get(clazz);
if (list == null) map.put(clazz, list = new ArrayList<>());
list.add(item);
}
public <W extends V> List<W> get(@NonNull Class<W> clazz) {
return (List<W>) map.get(clazz);
}
public int size() {
return map.size();
}
}
You can create one like this: ClassMap<Parent> map = new ClassMap<>(items.stream().map(Item::get).collect(Collectors.toList()));
.