0

I'm facing a contradiction in how generics subtyping works. From my understanding, generics are nonvariant, so there should not be any subtyping relationship between the two.

Box<?> n =  new Box<>(null); 
Box<Object> error = (Box<T>) n; //error

Fails.

I understand that n of compile time type Box<?> can be type-casted to Box<T> as a subtyping relationship of Box<T> <: Box<?> exists. However since generics are invariant, i cannot assign a compile time type of Box<T> to Box<Object>.

With that in mind here comes the contradiction:

Box<T> g = new Box<>(t);
Box<Integer> fine = (Box<Integer>) g; //ok, but why?

It is strange because in the first example, we have shown that we cannot assign different generics together due to it being invariant. But in the 2nd example, I am able to do a type casting of compile type Box<T> to Box<Integer> .

though the 1 is an assignment and 1 is a typecasting operation, i would assume they work the same way. Is there a good explanation for this ?

Thanks in advance.

Savior
  • 3,225
  • 4
  • 24
  • 48
neowenshun
  • 860
  • 1
  • 7
  • 21

2 Answers2

2

To be clear, this is very much an issue with the assignment, not with the casting.

In your first snippet

Box<?> n =  new Box<>(null); 
Box<Object> error = (Box<T>) n; //error 

The error should be something like

Type mismatch: cannot convert from Box<T> to Box<Object>

As you've stated, this is trying to tell you that a Box<T> is not a subtype of Box<Object>, so the expression on the right hand side of the assignment operator can't be assigned to error. This is essentially the same issue described in

In your second snippet

Box<T> g = new Box<>(t);
Box<Integer> fine = (Box<Integer>) g; //ok, but why?

In terms of the assignment, the compiler doesn't see anything wrong. You're assigning an expression of type Box<Integer> to a variable of type Box<Integer>.

though the 1 is an assignment and 1 is a typecasting operation, i would assume they work the same way. Is there a good explanation for this ?

Yes. The Java Language Specification states

Casting contexts allow the operand of a cast expression (§15.16) to be converted to the type explicitly named by the cast operator. Compared to assignment contexts and invocation contexts, casting contexts allow the use of more of the conversions defined in §5.1, and allow more combinations of those conversions.

The JLS goes on to explain which conversion are allowed (see also 5.1.6 Narrowing Reference Conversion, 4.10 Subtyping), but the short of it is that we have a narrowing reference conversion that is unchecked, that is not allowed in an assignment context. You likely also saw the compiler's 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 (§4.12.2). To flag this to the programmer, an unchecked narrowing reference conversion causes a compile-time unchecked warning, unless suppressed by @SuppressWarnings (§9.6.4.5).

That is why you see these behaviors. You could've introduced an extra cast in your first snippet to make it compile

Box<?> n = new Box<>();
Box<Object> error = (Box<Object>)(Box<T>) n; // error

This is more representative of what you're doing in your second snippet (you would have received a similar warning).

Savior
  • 3,225
  • 4
  • 24
  • 48
1

This: (SomeType) someExpr is the casting operator. It's a bit of a tricky beast, as the cast operator is used for 3 completely different operations. Guns and Grandmas over here. If SomeType is primitive, it converts things. If SomeType is a plain jane type, something like String x = (String) obj, it is a type assertion: It doesn't just make the compiler adjust the type of the 'AST node', it also injects some bytecode that will do nothing if obj is indeed a String, but will cause a ClassCastException to be thrown if it is not.

Then there is the 'generics type coercion', the third thing casts do. It's what you are doing in these examples - g is already of type Box, so the only part you're actually adding is the generics bit.

This does nothing at runtime. No code is injected. You're telling the compiler: "Trust me, I know this is true, I understand there will be absolutely no checking of any sort". If you mess up and it's not true, some code that contains absolutely no casts whatsoever will nevertheless end up throwing a ClassCastException:

List<String> list = new ArrayList<String>();
list.add("Hello");
List<Integer> hmm = (List<Integer>) (List) list;
System.out.println(list.get(0));

The 4th line will throw a ClassCastEx - even though the 'cast' appears to be on the 3rd line.

These 'generics type coercion' things have only one purpose: To let you work with legacy code; legacy code where the type safety of generics cannot be applied, simply because the code doesn't have any generics. It's been, what, 25 years since generics were introduced (with java 1.5), but the language had to cope with the fact that java 1.1 through java 1.4 were quite popular (in fact, java was arguably the most popular language on the planet at that point in time), so the generics feature is designed to cope with 'legacy' (read: lacks generics info) code.

If you find that you need generics type coercion to make your code work, take a step back, because you really shouldn't be doing that, except for extremely simplistic cases, and almost always in the form of casting straight to a type var (e.g. return (T) arr[i];, which is in the get(int idx) impl of ArrayList, for example). This doesn't do anything either (it would be impossible for that line to produce a ClassCastException; no code is injected at runtime, it's purely a figment of the compiler's imagination), but it lets you e.g. use an array to represent a generics-based data structure, exactly what ArrayList is trying to do.

In other words, yes, you are right, generics is supposed to be invariant so it feels weird that you can cast one type to another, seemingly entirely incompatible type - but that's sort of the point: To let you work around missing / miswritten generics in libraries.


So, why does the first snippet not work? Because you're telling the compiler: Please treat this expression of type Box<?> as type Box<T>. If T isn't defined here, this doesn't work at all (compiler would have no idea what T is). If it IS defined, then this still does not work - T is not an Object. This is just basic java generics invariance rules: You have an expression that you told the compiler: Just treat it as a Box<T> and don't complain, please - and are trying to assign that to a Box<Object> which is not a valid assignment.

In the second snippet, you're telling the compiler: I know, I know, that g is Box and not compatible, but will you shut up and just take my word for it, that you can treat it as a Box<Integer> and just carry on? So, the compiler does, and sees an assignment of a thing that you told the compiler is a Box<Integer> to a variable of type Box<Integer> which is fine, so java will let you do this, though it will emit a warning to whine about the fact that you think you know better than the compiler does. It doesn't like it when you do that.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • I've read your answer a couple times, but I can't find an explanation for the differing behavior in their 2 snippets of code. You explain unrelated casts and go into Java's generics history, but why can you cast in their first snippet and not in their second? – Savior Feb 28 '21 at 15:58
  • @Savior I'd think that is obvious, but I have edited the answer to elaborate on what the purpose and JLS intent of generics type coercions imply for the 2 snippets. – rzwitserloot Feb 28 '21 at 16:21
  • _Please treat this expression of type Box> as type Box. [...] this still does not work_ That's wrong. The casting part of that line works fine. It's the assignment that doesn't work. You mention that later with _is not a valid assignment_, but **why**? – Savior Feb 28 '21 at 16:42
  • I think you're now just looking for nits to pick. – rzwitserloot Feb 28 '21 at 18:19