0

This is disallowed, which I believe is due to type erasure. (T is erased and cannot be accessed in runtime to read its class like T.class).

class Cup<T> {
    private T t;
    public T[] getArray(int size) {
        Class<T> cls = T.class;
        return (T[]) Array.newInstance(cls, size);
    }
}

But how come this compiles? I thought the type conversion of (T)val would happen at runtime, thus the JVM would know nothing about if T is String or anything else. So javac should prevent the program from being compiled at all. would error out.

public class Cup<T> {
    public T get() {
        Integer val = 1;
        T result = (T)val;
        return result;
    }

    public static void main(String[] args) {
        Cup<String> cup = new Cup<>();
        System.out.println(cup.get());
    }
}

Did I miss anything on compile time vs runtime? Why this design choice? What is the intuition behind that?

wlnirvana
  • 1,811
  • 20
  • 36
  • It compiles because you a performing an unchecked cast. If you run the program, you'll see that it results in a `ClassCastException` because an `Integer` cannot be casted to a `String`. – Jacob G. Feb 05 '21 at 05:17
  • It compiles... but with a warning. Doesn't it? That warning tells you there's potential for trouble. Read through [What is unchecked cast and how do I check it?](https://stackoverflow.com/questions/2693180/what-is-unchecked-cast-and-how-do-i-check-it/2693301) – ernest_k Feb 05 '21 at 05:17
  • @JacobG. Yeah I noticed the warning, but my point is how come the erasured `String` type info is "reified" i the second example during the cast, but not in the first example Previously I thought the second would fail the compilation. – wlnirvana Feb 05 '21 at 05:20
  • You can not guarantee the type of `T.class` at runtime, it can be anything. But when you create `Cup`, it is known at compile-time and it won't change at runtime. – Aniket Sahrawat Feb 05 '21 at 05:27
  • Can you explain what you understand about why the first code snippet is disallowed, and exactly why you don't think the second code snippet should be allowed? There should be a misunderstanding somewhere. – Sweeper Feb 05 '21 at 05:29
  • @AniketSahrawat `Cup` is known at compile time, but the conversion of `(T)val` is at runtime I suppose? Then how come the JVM knows `T` is now `String` instead of `Object`? – wlnirvana Feb 05 '21 at 05:33
  • No, the compiler typecast it at compile time. There is no information available at runtime. If you have a bytecode decoder then you can verify it. Intellij Ultimate has a bytecode decoder, not sure about the community version. – Aniket Sahrawat Feb 05 '21 at 05:35
  • @Sweeper Edited my post. I think there might be something wrong in my understanding of compile time vs runtime with regard to type conversion. – wlnirvana Feb 05 '21 at 05:37
  • @AniketSahrawat Would you mind elaborating on "typecast it at compile time". Are you referring to `(T)val` being performed at compile time, or `Cup` becoming `Cup`? – wlnirvana Feb 05 '21 at 05:40
  • Both of them are done at compile time. `(T) val` becomes `(String) val` and `Cup` becomes `Cup`. – Aniket Sahrawat Feb 05 '21 at 05:43
  • @AniketSahrawat So if I have many instances of the generics, say `Cup`, `Cup`, etc. There will be many different versions of typecast code generated during compile time for `(T) val`? – wlnirvana Feb 05 '21 at 05:45
  • Yes, exactly. I would recommend you download a bytecode decoder to understand it completely. – Aniket Sahrawat Feb 05 '21 at 05:47
  • @AniketSahrawat Sure. By the way, would you mind reorg your comment into an answer so that I can accept it? Also I would appreciate if you can link to the spec where this semantic is officially specified, and explain the intuition about how the type system works here. (Sorry if I'm asking for too much). – wlnirvana Feb 05 '21 at 05:50
  • http://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.6 would be a nice starting point. You can accept the answer by sweeper, it has a lot of information and I would consider that you accepted my answer if you accept sweeper's :) – Aniket Sahrawat Feb 05 '21 at 06:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228333/discussion-between-wlnirvana-and-aniket-sahrawat). – wlnirvana Feb 06 '21 at 02:32

2 Answers2

1

This is called an unchecked narrowing reference conversion by the Java Lanaguage Spec. Allowing these casts is important for building some generic code. For example, they are used widely in implementing ArrayList, where the elements are stored in an Object[] and so static type checking is lost as items are added to and retrieved from the underlying array. However, because the compiler cannot perform the check statically nor at runtime, use of these conversions results in an unchecked warning:

If a narrowing reference conversion is unchecked, then the Java Virtual Machine will not be able to fully validate its type correctness, possibly leading to heap pollution. To flag this to the programmer, an unchecked narrowing reference conversion causes a compile-time unchecked warning, unless suppressed by @SuppressWarnings

These conversions exist solely in source code; they are not compiled into the bytecode and have no effect on the runtime. They are required solely to force the programmer to declare that they know (better than the compiler) that the conversion is safe.

Mark Peters
  • 80,126
  • 17
  • 159
  • 190
  • To sum up, the java language designers could have chosen checked cast here, in which case compilation would fail due to `T` being lost at runtime, just like in my first example. However, they opted for "unchecked narrowing reference" in this case, for the benefit like how `(E) elementData[index]` is used in `ArrayList` implementation. Is my understanding correct? – wlnirvana Feb 06 '21 at 02:31
1

You are correct that "the JVM would know nothing about if T is String or anything else", and as a result nothing will happen at runtime.

Normally, the runtime evaluation of a (reference type) cast would involve checking whether the object that you are casting actually is of the type that you are casting to. For example:

Object o = new Object();
String s = (String)o;

At runtime, a check is performed on o, and it will be found that o is actually of type Object and not String, and as such, a ClassCastException will be thrown.

On the other hand, if you are casting to a type parameter T, the runtime has no idea what T is, and so does not do any checking, hence this is an "unchecked cast", as the warning says.

So if val isn't actually of type T, no exceptions will be thrown:

Cup<String> c = new Cup<>();
c.get();

Even though I called get in the above code, and the line with the cast is executed, there will be no exceptions, because there is no runtime check. The runtime thinks that get returns an Object. It is only when the runtime knows what type to cast, does the cast happen, and the exception get thrown:

Cup<String> c = new Cup<>();
String s = c.get(); // this throws an exception

The compiler inserts the cast in the second line like so String s = (String)c.get();

As you can see, it doesn't really matter that the runtime doesn't know what T is at the line where the cast is, because you don't need the cast there anyway. Consider the type-erased version of your code:

public class Cup {
    public Object get() {
        Integer val = 1;
        Object result = val;
        return result;
    }
    public static void main(String[] args) {
        Cup cup = new Cup();
        System.out.println(cup.get());
    }
}

You'll notice that this is perfectly fine code that will compile!

(T)val here is mostly here to make the compiler happy, to convince the compiler that val is indeed of type T.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Interesting. I found that while `c.get();` runs without exception as you stated, `c.get().getClass();` actually has the `checkcast ` instruction generated and throws an exception at runtime. But note that no `String` is explicitly written by the programmer in this statement! This means the javac type system is actually capable of deducing that `c.get()` is returning `String` rather than `Object`, but the language designers deliberately chose not to statically check the cast when only `c.get()` is called. – wlnirvana Feb 06 '21 at 03:34
  • And I feel the more intuitive (i.e. for people without deep knowledge of how casting and type erasure work) explanation for this exception lies in neither `c.get().getClass()` nor `Cup c = new Cup<>();`, but in `T result = (T)val;` being allowed. That is, the cast check should not be delayed to `c.get().getClass()` or even `c.get()`, but should have been handled in `(T) val` by failing this line at compile time. – wlnirvana Feb 06 '21 at 03:42
  • @wlnirvana Your first comment is correct. What the compiler _can't_ do is to insert a check at the `(T)val` line, because the runtime doesn't know what `T` is. Regarding your second comment, the compiler _could_ be designed to not allow this kind of unchecked casts, but then it would make a lot of code very hard/impossible to write. I'm not one of the language designers, but I think they possibly weighed the costs and benefits of this, and concluded that it's better to allow this. – Sweeper Feb 06 '21 at 03:45
  • @wlnirvana Think of it this way. You are the designer and you want that the code written in java 4 should be compatible/compilable with/in java 14. How would you design it? Stress on it and you will find your answers without anybody's help. – Aniket Sahrawat Feb 06 '21 at 07:05