53

If I try to cast a String to a java.util.Date, the Java compiler catches the error. So why doesn't the compiler flag the following as an error?

List<String> strList = new ArrayList<>();                                                                      
Date d = (Date) strList;

Of course, the JVM throws a ClassCastException at runtime, but the compiler doesn't flag it.

The behavior is the same with javac 1.8.0_212 and 11.0.2.

Mike Woinoski
  • 3,123
  • 1
  • 16
  • 15
  • 2
    Nothing special about `List` here. `Date d = (Date) new Object();` – Elliott Frisch Mar 21 '20 at 23:27
  • 1
    I have been playing with an arduino lately. I'd love a compiler that didn't happily accept any cast and then just did them with totally unpredictable results. String to integer? Sure thing! Double to integer? Yes sir! String to boolean? At least that one mostly becomes false... – Stian Mar 22 '20 at 08:48
  • @ElliottFrisch: There's an obvious inheritance relationship between Date and Object, but there's no relationship between Date and List. So I expected the compiler to flag this cast, the same way it would flag a cast from String to Date. But as Zabuza explains in their excellent answer, List is an interface, so the cast would be legal if `strList` was an instance of a class that implements List. – Mike Woinoski Mar 22 '20 at 13:16
  • This is a frequently recurring question, and I'm sure I've seen multiple duplicates of it. It is basically the reverse-version of the strongly related: https://stackoverflow.com/questions/21812289/java-casting-an-object-to-an-interface-which-is-not-implemented?noredirect=1&lq=1 – Hulk Mar 23 '20 at 09:26
  • Does this answer your question? [Why does it compile when casting to an unrelated interface?](https://stackoverflow.com/questions/19824941/why-does-it-compile-when-casting-to-an-unrelated-interface) – Hulk Mar 23 '20 at 09:27
  • 1
    @StianYttervik -fpermissive is what's doing that. Turn on compiler warnings. – bobsburner Mar 23 '20 at 09:28

3 Answers3

89

The cast is technically possible. It cannot easily be proven by javac that it is not so in your case and the JLS actually defines this as a valid Java program, so flagging an error would be incorrect.

This is because List is an interface. So you could have a subclass of a Date that actually implements List disguised as List here - and then casting it to Date would be perfectly ok. For example:

public class SneakyListDate extends Date implements List<Foo> {
    ...
}

And then:

List<Foo> list = new SneakyListDate();
Date date = (Date) list; // This one is valid, compiles and runs just fine

Detecting such a scenario might not always be possible, as it would require runtime information if the instance is coming from, for example, a method instead. And even if, it would require much more effort for the compiler. The compiler only prevents casts that are absolutely impossible due to there being no way for the class-tree to match at all. Which is not the case here, as seen.

Note that the JLS requires your code to be a valid Java program. In 5.1.6.1. Allowed Narrowing Reference Conversion it says:

A narrowing reference conversion exists from reference type S to reference type T if all of the following are true:

  • [...]
  • One of the following cases applies:
    • [...]
    • S is an interface type, T is a class type, and T does not name a final class.

So even if the compiler could figure out that your case is actually provably impossible, it is not allowed to flag an error because the JLS defines it as valid Java program.

It would only be allowed to show a warning.

Zabuzard
  • 25,064
  • 8
  • 58
  • 82
  • 16
    And worth noticing, that the reason it catches the case with String, is that String is final, so the compiler knows that no class can extend it. – MTilsted Mar 22 '20 at 18:04
  • 5
    Actually, I don't think it's the "finalness" of String that makes `myDate = (Date) myString` fail. Using the JLS terminology, the statement attempts to convert from `S` (the `String`) to `T` (the `Date`). Here, `S` is not an interface type, so the JLS condition quoted above does not apply. As an example, try casting a Calendar to a Date and you'll get a compiler error even though neither class is final. – Mike Woinoski Mar 22 '20 at 21:39
  • He was most likely refering to casting a `List` to `String`, as opposed to casting `List` to `Date`. Former doesnt compile because `String` doesnt implement `List` and its impossible to further extend it to introduce `List`, because the class is `final`. – Zabuzard Mar 22 '20 at 22:02
  • 1
    I don't know whether or not to be disappointed the compiler can't do enough static analysis to prove that strList can only ever be of type ArrayList. – Joshua Mar 22 '20 at 23:46
  • @Joshua: That was my thought, too. It's not clear from this answer whether a Java compiler is forbidden from doing that, or if it's merely an unimplemented feature in the current OpenJDK `javac`. As Stephen C's more satisfying answer points out, it could become valid after the fact if the definition of the other class changes, but `ArrayList` and `Date`are both part of the Java language spec so a compiler would have to special-case to know that `ArrayList` will never be a `Date`. – Peter Cordes Mar 23 '20 at 00:54
  • @PeterCordes: In which case any such code should be wrapped in an if (instanceof) check. (I never considered special-casing it based on the compiler knowing the language definition.) – Joshua Mar 23 '20 at 03:37
  • 3
    The compiler is not forbidden from checking. But it is forbidden from calling it an error. That would make the compiler non-compliant. (See my answer ...) – Stephen C Mar 23 '20 at 05:38
  • '... the compiler would need to invest much more effort in that check, possibly even requiring runtime information' is meaningless. The compiler doesn't have runtime information, no matter how much effort it invests. This paragraph should be removed. Cannot imagine why this comment was deleted. – user207421 Mar 23 '20 at 09:43
  • @user207421 rephrased a bit. Feel free to edit yourself. – Zabuzard Mar 23 '20 at 11:46
  • 3
    To add a little bit lingo, the compiler would need to prove that the type `Date & List` is *uninhabitable*, it isn't enough to prove that it is *uninhabited* currently (it could be in the future). – Polygnome Mar 23 '20 at 14:31
16

Let us consider a generalization of your example:

List<String> strList = someMethod();       
Date d = (Date) strList;

These are the main reasons why Date d = (Date) strList; is not a compilation error.

  • The intuitive reason is that the compiler does not (in general) know the precise type of the object returned by that method call. It is possible that in addition to being a class that implements List, it is also a subclass of Date.

  • The technical reason is that the Java Language Specification "allows" the narrowing reference conversion that corresponds to this type cast. According to JLS 5.1.6.1:

    "A narrowing reference conversion exists from reference type S to reference type T if all of the following are true:"

    ...

    5) "S is an interface type, T is a class type, and T does not name a final class."

    ...

    In a different place, JLS also says that an exception may be thrown at runtime ...

    Note that the JLS 5.1.6.1 determination is based solely on the declared types of the variables involved rather than the actual runtime types. In the general case, the compiler does not and cannot know the actual runtime types.


So, why can't the Java compiler work out that the cast won't work?

  • In my example, the someMethod call could return objects with a variety of types. Even if the compiler was able to analyze the method body and determine the precise set of types that could be returned, there is nothing to stop someone changing it to return different types ... after compiling the code that calls it. This is the basic reason why JLS 5.1.6.1 says what it says.

  • In your example, a smart compiler could figure out that the cast can never succeed. And it is permitted to emit a compile-time warning to point out the problem.

So why isn't a smart compiler permitted to say this is an error anyway?

  • Because the JLS says that this is a valid program. Period. Any compiler that called this an error would not be Java compliant.

  • Also, any compiler that rejects Java programs that the JLS and other compilers say is valid, is an impediment to the portability of Java source code.

Hulk
  • 6,399
  • 1
  • 30
  • 52
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 4
    Upvote for the fact that *after compiling the calling class the called function implementation may change*, so even if it is provable at compile time, with the current implementation of the callee, that the cast is impossible, this may not be so *at later run times* when the callee has changed or been replaced. – Peter - Reinstate Monica Mar 22 '20 at 21:41
  • 2
    Upvote for highlighting the portability problem that would be introduced if a compiler tries to be too smart. – Mike Woinoski Mar 22 '20 at 21:45
2

5.5.1. Reference Type Casting:

Given a compile-time reference type S (source) and a compile-time reference type T (target), a casting conversion exists from S to T if no compile-time errors occur due to the following rules.

[...]

If S is an interface type:

  • [...]

  • If T is a class or interface type that is not final, then if there exists a supertype X of T, and a supertype Y of S, such that both X and Y are provably distinct parameterized types, and that the erasures of X and Y are the same, a compile-time error occurs.

    Otherwise, the cast is always legal at compile time (because even if T does not implement S, a subclass of T might).

List<String> is S and Date is T in your case.

Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90