You are here: Home ยป Some background on Generics: dumping a Map

Some background on Generics: dumping a Map

Posted on Wednesday, December 17 2014 at 11:16 (Age: 5 yrs) | Category: Java

In Java, dumping a Map to see what key/value mappings it contains is quite easy. Lets assume we have a factory method which returns a map, containing some String to Long mappings:

Map<String, Long> map = createMap();

The toString() method of the Collection classes usually provide a useful implementation, so that we can simply use

System.err.println(map);

to dump the map. If we want to have more control over the output format, e.g. one line per map entry, we can use a simple loop to iterate over the entries:

Set<Entry<String, Long>> entries = map.entrySet();
for (Entry<String, Long> entry : entries) {
    String key = entry.getKey();
    Long value = entry.getValue();
    System.err.printf("%s=%s\n", key, value);
}

When running this code with the map returned from the createMap() method, we get

Exception in thread "main" java.lang.ClassCastException: com.example.Key cannot be cast to java.lang.String
	at com.example.MapSample.run(MapSample.java:25)
	at com.example.MapSample.main(MapSample.java:54)

Ehm... wait a moment ... why do we get this exception in the line which executes String key = entry.getKey();? We did not get any compile time errors or even warnings, and entry.getKey() is declared to return a String due to the definition of entry as Entry<String, Long>. Also, the Map is declared as Map<String, Long> - so each key element in the map must be a String, right?

No.

Lets examine how the map is created in the createMap() factory method:

private Map<String, Long> createMap() {
    Map result = new HashMap();
    result.put("one",  1L);
    result.put(new Key(), 3L);
    result.put("two",  2L);
    result.put("five",  5L);
    result.put("four",  4L);
    return result;
}

Obviously, we are allowed to use different types than String as a key, an instance of the Key class in this case. This is possible since Map and HashMap are used as raw types here, instead of parameterized types. The Java compiler will issue some warnings, but will still compile the method as such (and even the warnings can be switched off with an annotation like @SuppressWarnings({ "unchecked", "rawtypes" })).

Remember that Generics (paremeterized types) are syntactic sugar only - internally, during runtime, everything is raw types, with the necessary casts automatically applied where necessary. These casts succeed when the parameterized types are used throughout the application. If this was properly done in the createMap() method, the compiler would not let us use Key as a key value. However, due to the usage of Map as raw type, we can. And then, during runtime, the casts mentioned before will fail horribly. In the example above, entry.getKey() still returns an Object reference, but due to the type parameters the compiler automatically casts the result to a String. You can verify this in the byte code which contains a corresponding checkcast instruction after entry.getKey() has been invoked.

The general rule is: do not use raw types. Especially with large legacy code bases, there are sometimes occasions where Collections are still created without the proper usage of type parameters (but later returned through parameterized types). Thus, the learning is: Even if you get a reference to a properly parameterized Collection type, do not be surprised if there are other types stored in it when you iterate over the Collection values or keys.

Back to our dumping method, we can simply solve the issue by not forcing any implicit casts:

Set<Entry<String, Long>> entries = map.entrySet();
for (Entry<String, Long> entry : entries) {
    Object key = entry.getKey();
    Object value = entry.getValue();
    System.err.printf("%s=%s\n", key, value);
}

When run, this will produce the expected output:

four=4
one=1
com.example.Key@15db9742=3
two=2
five=5

No comments

Be the first to post a comment!