I decompiled the Map class using javap. The class definition still shows the presence of generic types K and V. This should have been erased by the concept of type erasure.
There is extra information inside the bytecode that is used decode the generic information.
If generic signature information were completely erased, it would not be possible to consume generic types or methods unless you also had the source code. Think about it: in order to use generics effectively, the compiler must know that a type or method is generic, and it must know the number, position, and bounds of the generic parameters.
To that end, javac
emits what's called a Signature
attribute on types and methods which are themselves generic, or whose signatures contain type variables or instantiations of other generic types.
For a generic type like Map<K, V>
, the class definition will emitted with a Signature
attribute describing:
For the Map
interface, the Signature
value looks like this:
<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;
You can see this attribute in javap -v
at the very end of the output, on the line following the closing }
. To see what a more complete generic signature looks like, take a look at the HashMap
class, which has a generic base class and implements multiple interfaces:
<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/util/AbstractMap<TK;TV;>;Ljava/util/Map<TK;TV;>;Ljava/lang/Cloneable;Ljava/io/Serializable
From this signature, the compiler knows the following about type HashMap
:
K
and V
, both of which extend java.lang.Object
.java.util.AbstractMap<K, V>
. To clarify, K
and V
here refer to the parameters defined by HashMap
(not AbstractMap
).java.util.Map<K, V>
, java.lang.Cloneable
, and java.io.Serializable
.Methods may also have Signature
attributes, but in the case of methods, the signature describes:
However, a method's Signature
is considered extra metadata; you will never see one referenced directly in bytecode. Instead, you will see references to the method descriptor, which is similar to a signature that has had generic erasure applied recursively. Unlike Signature
attributes, method descriptors are mandatory. javap -v
is kind enough to show you both. For example, given the HashMap
method public V put(K, V)
:
(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
.Signature
is (TK;TV;)TV;
.The Signature
tells the compiler and your IDE the full generic signature of the method, enabling enforcement of type safety. The descriptor is how the method is actually referenced in the bytecode at a call site. For example, given the expression map.put(0, "zero")
where map
is a Map<Integer, String
>, the instruction sequence would be something like:
aload (some variable holding a Map)
iconst_0
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
ldc "zero"
invokeinterface java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
Note how there is no generic information retained. Limited type safety is enforced at runtime by the insertion of checkcast
instructions, which perform runtime casts. For example, a call to map.get(0)
on a Map<Integer, String>
would include an instruction sequence similar to:
aload (some variable holding a Map)
iconst_0
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
invokeinterface java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
checkcast Ljava/lang/String;
Thus, even though the Map
type is fully erased at the call site, the emitted bytecode ensures that any value retrieved from a Map<Integer, String>
is actually a String
, and not some other Object
.
It's important to stress that, like most metadata in a classfile, Signature
attributes are completely optional. And while javac
will emit them when necessary, it is possible for them to be stripped out by post processors like bytecode optimizers and obfuscators. This would, of course, make it impossible to consume generics in the manner intended. If, for example, you were to strip out the Signature
attributes in java/util/Map.class
, you could only consume Map
as a non-generic class equivalent to Map<Object, Object>
, and you would have to handle type checking yourself.