4

I stumbled upon this interesting cast/generics problem today:

public class A {

    Map<Class<? extends B>, List<Set<B>>> mapListSet = new HashMap<>();
    Map<Class<? extends B>, Set<B>> mapSet = new HashMap<>();

    public <T extends B> List<Set<T>> foo(Class<T> clazz) {
        List<Set<T>> listSet = (List<Set<T>>) mapListSet.get(clazz);

        return listSet;
    }

    public <T extends B> Set<T> bar(Class<T> clazz) {
        Set<T> set = (Set<T>) mapSet.get(clazz);

        return set;
    }
}

class B {

}

I can compile the method "bar" with only one warning from the IDE, whereas the IDE completely refuses to compile method "foo". This is a simplified example taken from my recently written code, does anyone know if I can make it more elegant rather than just doing this?

    public <T extends B> List<Set<T>> foo(Class<T> clazz) {
        List<Set<T>> listSet = (List) mapListSet.get(clazz);

        return listSet;
    }

Any help is hugely appreciated, I have a feeling this code smells really bad and I would love to improve on it.

Bartolini
  • 53
  • 6
  • 1
    Right. This code is never going to work, because your map is keyed with sets of classes, not classes. – Louis Wasserman Dec 25 '22 at 20:19
  • Oh I'm sorry, I oversimplified my example. Going to fix it right away. – Bartolini Dec 25 '22 at 20:23
  • Now it should be as intended. – Bartolini Dec 25 '22 at 20:30
  • 2
    You have created `mapListSet` to have values of `List>`, but in foo you are trying to assign that to `List>`. It’s important to realise that T being a subclass of B does NOT mean that eg List will be a subclass of List – racraman Dec 25 '22 at 21:47
  • 1
    Hmm ok I see, but how come I can do a similar thing in bar, and why is it still possible to force the compiler to trust the cast to raw List? – Bartolini Dec 25 '22 at 22:08
  • 2
    I am more confused about why `Set set = (Set) mapSet.get(clazz);` in `bar` method only gave you *warning* instead of compilation error. – Pshemo Dec 25 '22 at 22:18
  • 2
    If ever the words "force the compiler" come out of your fingers, you should take a step back and ask whether you *really* want to do that. What you are trying to do is inherently type-unsafe. Maybe you do it anyway, but you should at least consider your alternatives first. – John Bollinger Dec 25 '22 at 22:20
  • @Pshemo I was as buffled, because both problems seemed quite similar in essence, hence my confusion why the one worked. – Bartolini Dec 25 '22 at 22:22
  • @JohnBollinger you are right, it seemed to me to be a bad solution. I was only wondering why it was possible at all in the first place. – Bartolini Dec 25 '22 at 22:23
  • @Bartolini: Just out of curiosity: What is it that your actual code is doing? – Lii Dec 25 '22 at 22:35
  • @Lii I'm writing an entity component system. I basically have `components` (class B) which are part of an `entity` and a `scene` which is a container for entities. The entity stores the components using the map from my question. This way I can use the function `bar` to quickly retrieve a component of a certain type from an entity. – Bartolini Dec 25 '22 at 22:43
  • @Lii it seemed to me to be very convenient to call `getComponent(RandomComponent.class)` on an entity and have it return the already casted component ready to use. – Bartolini Dec 25 '22 at 22:45
  • 1
    @Bartolini: Your solution is similar to Guava's [`ClassToInstanceMap`](https://guava.dev/releases/19.0/api/docs/com/google/common/collect/ClassToInstanceMap.html), but with multiple values for each key class. – Lii Dec 26 '22 at 08:40
  • 1
    @Bartolini If you are interested I posted question about warning vs error in similar situation. Maybe someone will help solving that mystery: [Why when casting List> to List> causes compilation error, but casting List to List causes warning?](https://stackoverflow.com/q/74923840) – Pshemo Dec 26 '22 at 23:05

2 Answers2

3

My other answer shows how the cast you ask for can be done.

But! On a second though I think there is a better solution: Change the field types to this:

class A {

    Map<Class<? extends B>, List<? extends Set<? extends B>>> mapListSet = new HashMap<>();
    Map<Class<? extends B>, Set<? extends B>> mapSet = new HashMap<>();
    ....

In that way you don't need the cast-through-extra-type trick, it works without it.

And, more importantly: The field types express the actual type of the elements.

Your current types express that the elements are Set<B>. But they're not! They are not sets of type B, they are sets of some unknown subtype to B. And that is exactly what Set<? extends B> expresses.

Lii
  • 11,553
  • 8
  • 64
  • 88
2

To do what you want to do you have to circumvent the type system a little.

You have to trick the compiler by going through an extra type:

List<Set<T>> listSet = (List<Set<T>>) (Object) mapListSet.get(clazz); // Compiles

The problem is, as racraman notes, that T extends B does not imply that List<Set<T>> extends List<Set<B>>, and since the compiler doesn't see how the cast could ever work it generates an error. It is the same reason as you can't cast, for example String to `Number. Since then have no subtype relationship the compiler things the cast jsut can't be right.

It looks like you keep track of them member types of each set at runtime, using a Class as key. It than situation it might be justified to use the above trick.

Lii
  • 11,553
  • 8
  • 64
  • 88
  • This could just be `List> listSet = (List) mapListSet.get(clazz);` which is already part of the question. Double-casting doesn't have any advantages – knittl Dec 26 '22 at 11:22
  • @knittl The (very slight) disadvantage of your solution is that it uses raw types. So you get a raw type warnings, in addition to the unchecked cast warning. I think the double cast version is more "honest", because you only cheat in one way instead of two. – Lii Dec 26 '22 at 19:08
  • yes, it uses raw types. But it will fail for `(List) mapListSet` (casting a map to a list). Casting to Object always succeeds and cannot be checked by the compiler. There is no solution without disadvantages here :( – knittl Dec 26 '22 at 19:53