简体   繁体   中英

Why Java couldn't figure out some obvious illegal casts when type parameters are involved?

Consider the following example:

public class Example {
    public static <T> void f(T obj) {
        Integer i = (Integer) obj; // runtime error
    }
    public static void main(String[] args) {
        f("hello");
    }
}

Is there any reason why Java cannot figure out that cast in line 3 is illegal in compile time? Surely, because of the type erasure, the signature of the function in runtime will be f(Object obj) , but it seems to me that in the compile-time it's enough information to catch the error.

Compare this with the case:

List<String> ls = new ArrayList<>();
ls.add(42); // compile-time error
ls.add("foo");
Integer i = ls.get(0); // compile-time error

where a type parameter is involved but the error succesfully detected in the compile-time.

If the answer is "the compiler is just not smart enough", then is there any reason (for backward compatibility?) why it can't be made smarter?

Explanation

Java, based on its current rule set (see the JLS) has to treat the method content and its call-site separately .


Method content

The cast

(Integer) obj

has to be allowed at compile-time since T could be an Integer . After all, a call like

f(4)

should succeed and be allowed.

Java is not allowed to take in the call-site of the method into consideration. Also, this would imply that Java would have to scan all call-sites but this is impossible since that would also include possible future call-sites that have not been written yet or are included later on, in case you are writing a library.


Call-site

The call-site also has to be legal since Java is not allowed to take the method-content into consideration.

The signature demands T ( extends Object ) and String fullfills that. So it is allowed to be called like that.

If Java would also check the content, imagine you would hide the cast 3 levels depper in some other method calls. Then Java not only has to check f s code but also the code of those methods and possibly all their if statements to check if the line with the bad cast is even reached. It is NP-hard to prove that with 100% certanity at compile-time, hence it is also not part of the rule set.


Why?

While we saw that such situations are not always easy to detect and that actually proving it for all possible situations might even be impossible (NP-hard), Java designers could certainly have added some weaker rules that cover the dangerous situations partially.

Also, there are actually some similar situations in which Java will help you out with weaker rules. For example a cast like

House a = new House();
Dog b = (Dog) a;

is forbidden because Java can prove easily that the types are completely unrelated. But as soon as the setup becomes more complex, with types coming from other methods, generics, Java can not easily check it anymore.

All in all, you will have to ask a Java Language Designer for the precise reasons in the decision making. It is what it is.


Static code analysis

What you have here is typically the job of a static code analyzer (like most IDEs already include). They will actually scan through your code, all usages and so on and try to figure out if your current code flow has possible issues.

It is important to note that this also includes a lot of false-positives, as we just learned that not all of such usages might actually be wrong, some dangerous setups might be intended.


Appendix: Comments

Based on the discussion in the comments, let me stress out the fact that your particular example is indeed simple to prove to be wrong . So the call-site could easily be forbidden in this particular case (any static code analyzer will happilly throw a warning at you for this code).

However, we can do very simple modifications to the code already that demonstrates why it is so difficult to actually prove errors when connecting the call-site with the content of the method.

So the tldr is that almost all real code situations require much more efforts for a tool to prove with 100% that the call is incorrect. Also, it is much more difficult to program this and it can not always be ensured that there are no false-positives. Which is why such stuff is typically not done by a compiler but rather by static code analyzers.

The two common examples for this are method nesting and code branching.

Nesting

Imagine you hide the cast (Integer) obj a level depper, in another method:

public static void main(String[] args) {
    f("hello");
}

public static <T> void f(T obj) {
    g(obj);
}

public static <T> void g(T obj) {
    Integer i = (Integer) obj;
}

In order to prove this, Java would now have to connect the call-site from main to the content in f , to the call-site in g . If you add more levels of nesting this quickly gets out of control and needs a recursive deep analysis before anything can be proven.

Branching

Another very common but difficult situation for the compiler is if you hide the bad cast in a branched code flow:

public static void main(String[] args) {
    f("hello");
}

public static <T> void f(T obj) {
    if (isMonday()) {
        Integer a = (Integer) obj;
    } else {
        String b = (String) obj;
    }
}

Now, Java would need knowledge about what isMonday() returns at compile-time, which is simply impossible.

But if Java flags this, it would be bad. Because, what if we ensure externally that we only launch the program mondays? It should work then.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM