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. Why does that not happen ?
./javap -verbose java.util.Map
Classfile jar:file:/opt/jdk1.8.0_101/jre/lib/rt.jar!/java/util/Map.class
Last modified 22 Jun, 2016; size 4127 bytes
MD5 checksum 238f89b3e2ff9bebe07aa22b0a3493a9
Compiled from "Map.java"
public interface java.util.Map<K extends java.lang.Object, V extends java.lang.Object>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
Java For Testers Type erasure is a process in which compiler replaces a generic parameter with actual class or bridge method. In type erasure, compiler ensures that no extra classes are created and there is no runtime overhead.
- Erasure is a type of alteration in document. It can be classified as chemical erasure and physical erasure.
What is the following method converted to after type erasure? public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) { // ... } Answer: public static int findFirstGreaterThan(Comparable[] at, Comparable elem) { // ... }
Type-erasure simply means "erasing" a specific type to a more abstract type in order to do something with the abstract type (like having an array of that abstract type).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With