4
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public class Foo<T> {

    public List<Integer> aGenericList;
    
    public T item;
    
    public Foo() {
        aGenericList = new ArrayList<>();
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Foo foo = new Foo<String>();
        System.out.println(foo.aGenericList.getClass());

        Field testField = Foo.class.getField("aGenericList");
        Type genericType1 = testField.getGenericType();
        System.out.println(genericType1.getTypeName());
    }
}

The result is:

class java.util.ArrayList
java.util.List<java.lang.Integer>

which means with the reflection approach, it is possible to get the erased type information.

Now my questions are:

  1. Is this behavior formally defined in the JLS/JVMS spec (if it is, where?), or up to different vendors implementing the language?
  2. Is it possible to apply the reflection approach to the local variable foo to get something like Foo<java.lang.String>?
wlnirvana
  • 1,811
  • 20
  • 36
  • 1
    Did you mean to declare `aGenericList` as `List` instead of `List`? – user2357112 Jan 10 '21 at 08:12
  • @user2357112supportsMonica I did mean `List`, but what you suggested is indeed interesting. I'll probably post another question about that case later on. – wlnirvana Jan 10 '21 at 08:19
  • It seems hard to distinguish between compile and run time here. For example, what about the local variable `Foo foo = new Foo();`? What if we have `List extends Number> aLocalGenericList = new ArrayList();` as a local variable? Is it possible to get the `String` or `Number` or `Integer` info at runtime? – wlnirvana Jan 10 '21 at 08:47
  • It occurs to me that the question in your title "Why is this generic type information for member field not erased in Java?" is not actually one of the two questions you ask in the body of your question. – M. Justin Jul 17 '23 at 04:59

3 Answers3

4

Your Questions

1. Is this behavior formally defined in the JLS/JVMS spec (if it is, where?), or up to different vendors implementing the language?

The Java Language Specification appears to specifically not describe reflection:

Consequently, this specification does not describe reflection in any detail.

But instead leaves the full behavior of reflection to be documented by the API (i.e. in the Javadoc).

However, the Java Virtual Machine Specification does explain that generic information must be emitted by a compiler:

4.7.9. The Signature Attribute

The Signature attribute is a fixed-length attribute in the attributes table of a ClassFile, field_info, or method_info structure (§4.1, §4.5, §4.6). A Signature attribute records a signature (§4.7.9.1) for a class, interface, constructor, method, or field whose declaration in the Java programming language uses type variables or parameterized types. See The Java Language Specification, Java SE 15 Edition for details about these constructs.

[...]

4.7.9.1. Signatures

Signatures encode declarations written in the Java programming language that use types outside the type system of the Java Virtual Machine. They support reflection and debugging, as well as compilation when only class files are available.

A Java compiler must emit a signature for any class, interface, constructor, method, or field whose declaration uses type variables or parameterized types. Specifically, a Java compiler must emit:

  • A class signature for any class or interface declaration which is either generic, or has a parameterized type as a superclass or superinterface, or both.

  • A method signature for any method or constructor declaration which is either generic, or has a type variable or parameterized type as the return type or a formal parameter type, or has a type variable in a throws clause, or any combination thereof.

    If the throws clause of a method or constructor declaration does not involve type variables, then a compiler may treat the declaration as having no throws clause for the purpose of emitting a method signature.

  • A field signature for any field, formal parameter, or local variable declaration whose type uses a type variable or a parameterized type.

[...]

2. Is it possible to apply the reflection approach to the local variable foo to get something like Foo<java.lang.String>?

No, because local variables are not reflectively accessible. At least not directly by the Java Language. But let's say they were. You have:

Foo foo = new Foo<String>();

What would be reflected is the left-hand-side. That is a raw type, so all you would know is that the type of foo is Foo. You would not be able to tell that the instance created by the right-hand-side was parameterized with String.


Some Clarification (Hopefully)

When we say "generics are erased at run-time" we don't mean in this context. The statically defined type of reflectively accessible constructs, such as fields, is saved in the byte-code. For example, the following:

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;

public class Main {

  private static List<? extends Number> list = new ArrayList<Integer>();

  public static void main(String[] args) throws Exception {
    Field field = Main.class.getDeclaredField("list");

    // Due to List being a generic type the returned Type is actually
    // an instance of java.lang.reflect.ParameterizedType
    Type genericType = field.getGenericType();
    System.out.println("Generic Type  = " + genericType);

    // The raw type can be gotten from the ParameterizedType. Here the
    // returned Type will actually be an instance of java.lang.Class
    Type rawType = ((ParameterizedType) genericType).getRawType();
    System.out.println("Raw Type      = " + rawType);

    // The ParameterizedType gives us access to the actual type
    // arguments declared. Also, since a bounded wildcard was used
    // the returned Type is actually an instance of
    // java.lang.reflect.WildcardType
    Type typeArgument = ((ParameterizedType) genericType).getActualTypeArguments()[0];
    System.out.println("Type Argument = " + typeArgument);

    // We know in this case that there is a single upper bound. Here
    // the returned Type will actually be an instance of java.lang.Class
    Type upperBound = ((WildcardType) typeArgument).getUpperBounds()[0];
    System.out.println("Upper Bound   = " + upperBound);
  }
}

Will output:

Generic Type  = java.util.List<? extends java.lang.Number>
Raw Type      = interface java.util.List
Type Argument = ? extends java.lang.Number
Upper Bound   = class java.lang.Number

All that information is there in the source code. Note that we are reflectively looking at the list field. We are not looking at an instance (i.e. a run-time object) referenced by said field. Knowing the generic type of the field is really no different than knowing that the field's name is list.

What we don't know is that the ArrayList was parameterized with Integer. Changing the above to:

import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;

public class Main {

  private static List<? extends Number> list = new ArrayList<Integer>();

  public static void main(String[] args) {
    Class<?> clazz = list.getClass();
    System.out.println("Class          = " + clazz);

    TypeVariable<?> typeParameter = clazz.getTypeParameters()[0];
    System.out.println("Type Parameter = " + typeParameter);
  }
}

Outputs:

Class          = class java.util.ArrayList
Type Parameter = E

We can see we know that the instance referenced by list is an instance of java.util.ArrayList. But from there all we can determine is that the ArrayList class is generic and has a single type parameter E. We have no way of determining that the list field was assigned an ArrayList with a type argument of Integer. In other words, the ArrayList instance itself does not know what type of elements it's declared to contain—that information has been erased.

To put it another way, the list field's type is known at run-time but the ArrayList instance (i.e the object created at run-time) only knows it's an ArrayList.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Key takeaway: erasure only applies to runtime instance, not statically declared field. Thank you very much for the detailed answer. – wlnirvana Jan 10 '21 at 11:44
  • @wlnirvana you can always find out even that type, [but you need a trick](https://stackoverflow.com/questions/64873444/generic-class-type-parameter-detail-at-runtime/64934396#64934396) – Eugene Jan 10 '21 at 19:32
  • @Eugene Well, yes, but that "trick" is essentially making the class non-generic. Still cool though. – Slaw Jan 11 '21 at 00:16
1

The type of List<Integer> is a compile time constant. When this happens, the compiler bakes in the type.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
1

No, you can't find the erased type information, still (unless via a trick if you want to get that Foo<java.lang.String>). I don't know why, but this seems much easier to answer. All you have to do is read the documentation of getGenericType:

a Type object that represents the declared type for the field represented by this Field object.

Not the actual type, but the declared type.

If you de-compile the code (javap -v -p -c), you will see two important fields under aGenericList:

public java.util.List<java.lang.Integer> aGenericList;
descriptor: Ljava/util/List;
flags: (0x0001) ACC_PUBLIC
Signature: #17                          // Ljava/util/List<Ljava/lang/Integer;>;

Signature and descriptor. The second one is what it is used at call sites, the first one makes sure that generics are actually used correctly by the compiler. Let's say such an example:

static Map<Integer, String> map = new HashMap<>();

public static void add(Integer x, String y) {
   map.put(x, y);
}

public String get(Integer x) {
   return map.get(x);
}

If you decompile get, you will see (among other things):

4: invokeinterface #19,  2 // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
9: checkcast     #23      // class java/lang/String

how does the compiler know to insert that checkcast? Because the full generic information is retained in the Signature field.

Eugene
  • 117,005
  • 15
  • 201
  • 306