Type erasure not working in Java Map class

前端 未结 2 1463
别那么骄傲
别那么骄傲 2021-01-03 05:22

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.

相关标签:
2条回答
  • 2021-01-03 05:58

    There is extra information inside the bytecode that is used decode the generic information.

    0 讨论(0)
  • 2021-01-03 06:04

    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:

    1. All generic parameters (type variables) declared by the type, and their bounds;
    2. The full generic signature of the type's base class;
    3. The full generic signature of the interfaces implemented by the type.

    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:

    1. There are two generic parameters, K and V, both of which extend java.lang.Object.
    2. The base class is java.util.AbstractMap<K, V>. To clarify, K and V here refer to the parameters defined by HashMap (not AbstractMap).
    3. The class implements 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:

    1. All generic parameters (type variables) declared by the method, and their bounds;
    2. The full generic signature of the method's parameter types;
    3. The full generic signature of the method's return type.

    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):

    1. The method descriptor is (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;.
    2. The generic 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.

    0 讨论(0)
提交回复
热议问题