1

I was reading about Generics from ThinkingInJava and found this code snippet

public class Erased<T> {
  private final int SIZE = 100;
  public void f(Object arg) {
    if(arg instanceof T) {}          // Error
    T var = new T();                 // Error
    T[] array = new T[SIZE];         // Error
    T[] array = (T)new Object[SIZE]; // Unchecked warning
  }
}

I understand the concept of erasure and I know that at runtime, there is no type for T and it is actually considered an Object (or whatever the upper bound was)

However, why is it that this piece of code works

public class ClassTypeCapture<T> {
      Class<T> kind;
      public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
      }
      public boolean f(Object arg) {
        return kind.isInstance(arg);
      }
}

Shouldn't we apply the same argument here as well that because of erasure we don't know the type of T at runtime so we can't write anything like this? Or am I missing something here?

alina
  • 291
  • 2
  • 9
  • We **know** the type of T at runtime, as T is erased and replaced with the type – Steyrix Jan 29 '20 at 15:50
  • 1
    There is nothing here that shows that `instanceof` is disallowed. Just an invalid cast of an object array to `T`. – user207421 Jan 29 '20 at 15:52
  • Also, how would you create a `ClassTypeCapture` for a generic `T`? – Andy Turner Jan 29 '20 at 15:53
  • You might want to look here: https://stackoverflow.com/questions/8692214/when-to-use-class-isinstance-when-to-use-instanceof-operator – Niklas Keck Jan 29 '20 at 15:59
  • `T` is erased at runtime; the java.lang.Class object is not. The Class object knows what its type is at runtime, regardless of generics. – VGR Jan 29 '20 at 17:40

3 Answers3

4

In your example, T is indeed erased. But as you pass kind, which is the class object of the given type, it can be perfectly used for the said check.

Look what happens when you use this class:

ClassTypeCapture<String> capture = new ClassTypeCapture<>(String.class);

Here, the class object of String is passed to the given constructor, which creates a new object out of it.

During class erasure, the information that T is String is lost, but you still have

ClassTypeCapture capture = new ClassTypeCapture(String.class);

so this String.class is retained and known to the object.

glglgl
  • 89,107
  • 13
  • 149
  • 217
3

The difference is that you do have a reference in the second snippet to an instance of java.lang.Class; you don't have that in the first.

Let's look at that first snippet: There is only one instance of Erased as a class. Unlike, say, C templates which look a bit like generics, where a fresh new class is generated for each new type you put in the generics, in java there's just the one Erased class. Therefore, all we know about T is what you see: It is a type variable. Its name is T. Its lower bound is 'java.lang.Object'. That's where it ends. There is no hidden field of type Class<T> hiding in there.

Now let's look at the second:

Sure, the same rule seems to apply at first, but within the context of where you run the kind.isInstance invocation, there's a variable on the stack: kind. This can be anything - with some fancy casting and ignoring of warnings you can make a new ClassTypeCapture<String>() instance, passing in Integer.class. This will compile and even run, and then likely result in all sorts of exceptions.

The compiler, just by doing some compile time lint-style checks, will really try to tell you that if you try to write such code that you shouldn't do that, but that's all that is happening here. As far as the JVM is concerned, the String in new ClassTypeCapture<String>(Integer.class) and the Integer are not related at all, except for that one compile-time check that says: The generics aren't matching up here, so I shall generate an error. Here is an example of breaking it:

ClassTypeCapture /* raw */ a = new ClassTypeCapture<Integer>(String.class);
ClassTypeCapture<Integer> b = a;

this runs, and compiles. And b's (which is the same as a's - same reference) 'kind' field is referencing String.class. The behaviour of this object's f() method is very strange.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
2

we don't know the type of T at runtime

You're missing the point of generics: generics allow the compiler to "sanity check" the types, to make sure they're consistent.

It's tempting to read ClassTypeCapture<T> as "a ClassTypeCapture for type T", but it's not: it's a ClassTypeCapture, with a hint to the compiler to check that all of the method invocations/field accesses/return values involving that reference are consistent with the type T.

To make this more concrete (let's do it with List, that's easier):

List<String> list = new ArrayList<>();
list.add("hello");
String e = list.get(0);

the <String> is an instruction to the compiler to do this:

List list = new ArrayList();
list.add("hello");               // Make sure this argument is a `String`
String e = (String) list.get(0); // Insert a cast here to ensure we get a String out.

At runtime, the "T-ness" isn't known any more, but the ClassTypeCapture (or Object, or String, or whatever) still is. You can ask an Object if it's an instance of an Object, String, ClassTypeCapture, or whatever.

You just can't ask it if it was a ClassTypeCapture<String> at compile time, because that <String> is just a compiler hint, not part of the type.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • I somehow understand your point, but I'm still confused. You say that we don't know about the "T-ness" right? My question is, if "arg instanceof T" doesn't sound right because we don't know the T-ness at runtime, then how come "kind.isInstance(arg)" sounds right when kind is also in fact, Class and we don't know the T-ness at runtime? My point is, in both these cases, T becomes "Object" so it's like saying, "arg instanceof Object" or "Object.class.isInstance(arg)" :/ – alina Jan 29 '20 at 16:21