Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type erasure not working in Java Map class

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:
like image 991
Suraj Chakraborty Avatar asked Sep 27 '17 10:09

Suraj Chakraborty


People also ask

How does type erasure works in Java?

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.

What are the two 2 types of Erasure?

- Erasure is a type of alteration in document. It can be classified as chemical erasure and physical erasure.

What is the following class converted to after type 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) { // ... }

What is type erasure and when would you use it?

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


1 Answers

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.

like image 69
Mike Strobel Avatar answered Oct 17 '22 07:10

Mike Strobel