Search code examples
javagenericscastingjlsparameterization

Rule for Cast operator validity when Interface held type is converted to a final class Type according to JLS


Consider the following clause of JLS 8 §5.5.1

Blockquote If S is an interface type:

...

If T is a class type that is final, then:

...

– Otherwise, S is either a parameterized type that is an invocation of some generic type declaration G, or a raw type corresponding to a generic type declaration G. Then there must exist a supertype X of T, such that X is an invocation of G, or a compile-time error occurs.

Here is the code example I tried to frame based on my understanding:

class Scratch {
  public static void main(String[] args) {
    S_intrf S = new S_clss();
    T_clss T = (T_clss) S;
  }
}

interface G<T> {}

interface S_intrf extends G<String> {}
class S_clss implements S_intrf {}

interface X extends G<String> {}
final class T_clss implements X {}

The diagram shows what I tried to implement in the example: enter image description here

  • Here the interface G<> is the generic super type that is being used to generate other types based on the clause:
  • Not sure why I am getting compilation error at that section?
  • In the S -> T conversion - my target type is Parameterized type and final type which is complying all the rules that JLS mentions for validity of the cast operation.

Solution

  • You've overcomplicated matters. Let's first ditch the generics:

    class Scratch {
      public static void main(String[] args) {
        S_intrf S = new S_clss();
        T_clss T = (T_clss) S;
      }
    }
    
    interface G {}
    
    interface S_intrf extends G {}
    class S_clss implements S_intrf {}
    
    interface X extends G {}
    final class T_clss implements X {}
    

    and try to compile that, which produces the exact same error. Hence the generics don't matter and just muddy the waters. They do not explain this error.

    As you can see from your own diagram you made, there is an exclusionary principle at work:

    • There is some unknown type; this is the actual type of the object that expression S resolves to.
    • We do know a few things about this unknown type; we know it has to be an object (because it's a reference variable), and that object has S_intrf in its type hierarchy. After all, if that wasn't the case, we could never get here; the JVM does not let you assign a reference to an object that isn't to variable S, given that it is declared as being of type S_intrf. This is an absolute guarantee: You can't make javac compile code so that this doesn't hold, and if you try to use bytecode manipulation shenanigans, the verifier will refuse to load the class.

    Given that knowledge, we can surmise one more thing: This unknown type, whatever it might be, cannot possibly be T_clss or any subtype of it. We know this because T_clss is final (therefore, no subtype of it can exist), and, it does not itself implement S_intrf.

    Hence, the cast will necessarily fail, it can't not. And in the Java Lang Spec, there's a rule that says the compiler must reject such a situation with a compile time error.

    We can 'prove' this by reading the JLS, but I'll leave that as an exercise to the reader. A more pragmatic way to show this truly is the answer, is to simply remove the final. Now, it does compile! If you then run it, you get, as expected, a ClassCastException.

    Without final, one could write e.g. class Example extends T_clss implements S_intrf {} and now we have a type such that an instance of this could both [A] be referred to by variable S, and [B] can be cast to T_clss.

    By the rules of JLS, the fact that a code analyser can trivially figure out that variable S in fact refers to an instance of S_clss() is not taken into consideration. The rules of the analysis that leads to 'oh, the analyser says this code will always fail, therefore refuse to compile it' is set in stone, spelled out by the JLS, and a compiler must adhere to it exactly.


    The §5.5.1 clause is most likely referring to the fact that you can use the cast operation to make assertions about the stuff in the generics part; you can for example do:

    List<Object> foo = ...;
    List<String> bar = (List<String>) foo;
    

    This compiles, with a warning, as the java compiler is not going to generate any code or give you any guarantees that the list in fact only contains strings. §5.5.1 is talking about casts where the non-generics part is useless (e.g. casting an expression of type List to List, which is obviously pointless), but the generics part is relevant - e.g. the above snippet.