0

I have a Map<K, V> and want to cast it to a Map<K, U> where V extends U. Example: I want to go from Map<String, String> to Map<String, Object>. This problem exists with all kinds of collections.

This question already has an answer with a "hacky" solution and one that explains why casting generic types is generally a bad idea. But let's revisit the example from the answer from Voo:

List<String> m = Something;
m.add("Looks good.");
m.add("42");
List<Object> s = (List<Object>)m; // (1) does not compile
List<Object> t = (List)m; // (2) compiles with warnings
Object myObject = s.get(1);

A few questions about this example:

  • Why does this (1) not compile? Is a way to make it compile other than the hack (2)?
  • What could possibly go wrong when casting a List<String> to a List<Object> since all Strings are objects?

My concrete problem:

I have got a Map<Integer, Map<Vector2i, MyClass> > map = ... that contains a map for each level. The map on level 0 is of type Map<Vector2i, MySubClass> special = .... I want now special to be part of map for the key 0 so that accesses to the map for key 0 can be treated as a normal Map<Vector2i, MyClass> object.

The situation is read-only, meaning that writing to map and special happens separatedly.

piegames
  • 975
  • 12
  • 31

1 Answers1

0

What could possibly go wrong when casting a List<String> to a List<Object> since all Strings are objects?

Look at it from this perspective: If it's a List<Object>, everything can be added to it. So, this can go wrong, for example:

List<Object> t = (List)m; // (2) compiles with warnings
t.add(1235); // it's a List<Object>, so we can add an Integer

for (String s : m) { // m only contains Strings, right?!
    System.out.println(s.length());
}

And you'll hit a ClassCastException once s is not a String as expected.

[Addendum]

Why does this (1) not compile? Is a way to make it compile other than the hack (2)?

I'm not sure, what your actual problem is, so I'm just guessing. But the first time I was faced with a similar issue, was something like this:

/* Just example; functionality does not matter, just that it accepts List<Number> */
public static void printNumbers(List<Number> numbers) {
    numbers.forEach(System.out::println);
}

Above's utility method takes a List<Number> and just prints each of them. Now you typically have List<Integer>, List<Double>, etc. in your code. But the following code would node compile:

List<Integer> integers = Arrays.asList(1, 2, 3);
printNumbers(integers);

Now, instead of doing wild casting (and in case you have control over the imaginary utility method of course), do the following:

public static void printNumbers(List<? extends Number> numbers) {
    numbers.forEach(System.out::println);
}

Now the method accepts Lists of any kind of Numbers. But you cannot add anything to the list (beside null) inside the method, because the actual type is not known at runtime.

This explanation is probably a little casual. If you want to get more in detail, search for the "PECS rule" ("producer extends, consumer super"). Same would apply for any generic types, such as Maps well, of course.

qqilihq
  • 10,794
  • 7
  • 48
  • 89
  • I see. I was focused on the 'reading' part and totally forgot that when writing it is still possible to mess things up. – piegames Jul 31 '17 at 19:27
  • One other thing to add is the notion of 'type erasure', whereby generics are a compilation time notion. The cast can be done at run time by doing `List s = (List) List.class.cast(m);`, but then it is a Pandora's box. For some extra insight, see: https://stackoverflow.com/questions/14524751/cast-object-to-generic-type-for-returning – Andre M Jul 31 '17 at 19:47
  • The ´? extends´ solution resolves my problem, thank you. – piegames Jul 31 '17 at 19:48