0

I have seen this question, but it does not seem to answer both cases below and does not reference any further documentation, which I'd love to read.

I have the following fragment of code:

    public static void main(String[] args) {
        // compile-time OK
        // run-time ClassCastException
        Child noWarnings = (Child) getParent();

        // vs...

        // compile-time failure
        Child compileTimeFailure = new Parent();
    }
    
    private static Parent getParent() {
        return new Parent();
    }

    private static class Parent { }

    private static class Child extends Parent { }

The code on line 4 of the snippet produces no warnings, but results in a ClassCastException. Why is the compiler unable to typecheck this?

This gets even weirder if we add a layer of indirection:

    public static void main(String[] args) {
        // compile-time OK
        // run-time ClassCastException
        Boxed noWarnings = new Boxed((Child) getParent());

        // vs...
        
        // compile-time OK, but warning is emitted correctly
        // run-time ClassCastException
        Boxed classCastWarning = new Boxed((Child) new Parent());
    }

    private static Parent getParent() {
        return new Parent();
    }

    private static class Boxed {
        public Boxed(Child data) {

        }
    }

    private static class Parent { }

    private static class Child extends Parent { }

It seems un-intuitive that the compiler does not allow the first example, but allows the second.

Any light shed on either of these case would be most welcome.

Paul Benn
  • 1,911
  • 11
  • 26

3 Answers3

1

Why is the compiler unable to typecheck this?

Some Parent instances are instance of Child, because all Childs are Parents. The compiler doesn't know if the instance that will be returned is a Child or not; but it could be. So it allows it.

In fact, as Stephen C points out, JLS does not allow the compiler to call this a compilation error.

Specifically, the language spec says:

If the compile-time type of the operand cannot be converted by casting conversion (§5.5) to the target type specified by the cast operator, then a compile-time error occurs.

(It also describes another situation in which compile-time errors occur in casting expressions; but these don't apply here because there isn't one or more AdditionalBound terms).

Parent can be converted to Child by a narrowing reference conversion, so a compile-time error doesn't occur.

Boxed classCastWarning = new Boxed((Child) new Parent());

This is obviously going to fail. new Parent() isn't an instance of Child, it's an instance of Parent.

But the compiler doesn't actually consider anything about new Parent() in the context of the cast beyond the fact it's an expression of type Parent, which it has to allow you to cast to a Child.

But that's not to say that tooling such as an IDE isn't allowed to give you a warning (or even an error, if you have it so configured): it's clear via static analysis that new Parent() is going to be an instance of Parent exactly (because that's all new Parent() can be), so casting that to a subclass is going to fail. Your tooling can tell you things that aren't specifically covered in the spec.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • 1
    Formally ... the JLS does not *allow* the compiler to call this a compilation error. Even in the case that is obviously going to fail. (But it could give you a warning.) – Stephen C Sep 21 '21 at 15:44
  • I'm afraid I'm still not clear on this. how is this different to calling the method `getParent()`? What I'm looking for is the place where the JLS explicitly says that the case `new Boxed((Child) getParent());` should not produce a warning - would be really good if you have a link @StephenC! – Paul Benn Sep 21 '21 at 15:49
  • @PaulBenn when you say "produce a warning", do you really mean a warning, or do you mean a compile-time error? If you do mean a warning, what is giving the warning? The compiler, or your IDE? – Andy Turner Sep 21 '21 at 15:50
  • @PaulBenn - https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.16 – Stephen C Sep 21 '21 at 15:54
  • @AndyTurner I meant both really - running `javac -Werror` on this doesn't produce a warning at all, but my point was basically "why not"?. Thanks for pointing out that this particular case is an IDE inspection :) – Paul Benn Sep 21 '21 at 16:01
  • Note the JLS is very clear about situations when compile time errors should occur. It doesn't state categorically that compile time errors may not occur in other cases, but a compiler that "helpfully" produced compilation errors in cases that other compilers didn't would be a bane to portability. (Warnings are OK ... because you can ignore warnings.) – Stephen C Sep 21 '21 at 16:02
  • @StephenC Thanks for the link, very helpful. I am still a little surprised that code like this does not result in a compile-time warning (as opposed to an error), in either of the cases I outlined above, but just in IDE-specific warnings. do you know what the reasoning was here? it's not clear from the spec, to me – Paul Benn Sep 21 '21 at 16:05
  • @PaulBenn the developers of the spec only have so much time. If the spec doesn't require it, they don't need to do it. And since you're testing your code, you'll find out soon enough that there's a problem there, right? More seriously, the only case where it is provable that the cast will fail is the `(Child) new Parent()` case - anywhere else, you have to get into serious flow analysis to determine that it's not going to fail. I can imagine it's just not worth the effort. – Andy Turner Sep 21 '21 at 16:09
  • There are three reasons. 1) Writing the JLS rules that would make this a compilation error is a lot harder than you think. Especially when you try to distinguish the various cases where the return type cannot be determined precisely. (And the rules would be hard to understand. (Note that you are already having problems with the current simple rules.)) 2) They would place an extra burden on the compiler writer. 3) It would present sugnificant backwards compatibility problems ... given that the core Java type checking rules were fixed back in the 1990's. – Stephen C Sep 21 '21 at 16:11
  • @AndyTurner the narrowing reference conversion link is helpful! does this mean the above is a **checked** type conversion? this implies, according to [docs](https://docs.oracle.com/javase/specs/jls/se11/html/jls-5.html#jls-5.1.6.2), that the JVM is able to "fully validate type correctness". I am just wondering what definition of type correctness applies here, is this something you can help with? Perhaps the developers decided in this particular case that a warning wasn't worth it due to the possibility of perfectly valid code containing this type of cast? – Paul Benn Sep 21 '21 at 16:13
  • Yes. Type correctness means ... according to the JLS specification of correctness. (There are some caveats with generic types too, but these are not sufficient to allow you to break runtime type safety. You'll get an exception. You can only **really** break runtime type safety using `Unsafe` or native code.) – Stephen C Sep 21 '21 at 16:14
  • It's not necessarily a checked type conversion; but in the case it's an unchecked type conversion, you're getting an unchecked cast warning anyway. – Andy Turner Sep 21 '21 at 16:14
  • one last thing @StephenC, regarding your reason #1 above which I think makes the strongest case. I understand that it would be impossible to make this an error. But I still don't fully grasp why it doesn't make sense to make it a warning. Making it a warning would cause no backwards-compat issues. It seems like a "trust the developer" move, out of line with the more common Java approach of making everything as safe as possible. – Paul Benn Sep 21 '21 at 16:22
  • 1
    @PaulBenn that's a question for the Java designers. I'm going to re-assert that there are greater priorities. – Andy Turner Sep 21 '21 at 16:26
1

Apart from existing answer, I would like to stress the reason why it's not easy to compute this exception during compile time.

The below example is also from the excerpt from https://docs.oracle.com/javase/specs/jls/se11/html/jls-5.html#jls-5.5 with minor additions from me.

class Point { int x, y; }

interface Colorable { void setColor(int color); }

class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}
final class EndPoint extends Point {}

class Test {
    public static void main(String[] args) {
        Point p = new Point();
        ColoredPoint cp = new ColoredPoint();
        Colorable c;
        // The following may cause errors at run time because
        // we cannot be sure they will succeed; this possibility
        // is suggested by the casts:

        cp = (ColoredPoint)p; // p might not reference an
                              // object which is a ColoredPoint
                              // or a subclass of ColoredPoint

        // this wont throw casts exception as references to object is valid
        Point refPoint = new ColoredPoint();
        cp = (ColoredPoint)refPoint; // perfectly valid

        c = (Colorable)p;      // p might not be Colorable

        // The following are incorrect at compile time because
        // they can never succeed as explained in the text:
        Long l = (Long)p;            // compile-time error #1
        EndPoint e = new EndPoint();
        c = (Colorable)e;            // compile-time error #2
    }
}

This example is self-explanatory on its own most cases, but you can give further read to the documentation above.

Karthik R
  • 5,523
  • 2
  • 18
  • 30
0

ClassCastExceptions never occur at compile time. All exceptions occur at runtime. You can check the settings of your IDE to get hints while coding.

McPringle
  • 1,939
  • 2
  • 16
  • 19
  • ah yes, I knew this but forgot to reflect it in the title. I've now edited that to make it clear. cheers – Paul Benn Sep 21 '21 at 15:46