Home > Enterprise >  Java Stream.map() on Subclasses with Wildcards Not Working
Java Stream.map() on Subclasses with Wildcards Not Working

Time:05-17

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

  •  Tags:  
  • java
  • Related