views:

460

answers:

6

I was trying to come up with obscure test cases for an alternative open-source JVM I am helping with (Avian) when I came across an interesting bit of code, and I was surprised that it didn't compile:

public class Test {
    public static int test1() {
        int a;
        try {
            a = 1;
            return a; // this is fine
        } finally {
            return a; // uninitialized value error here
        }
    }
    public static void main(String[] args) {
        int a = test1();
    }
}

The most obvious code path (the only one that I see) is to execute a = 1, "attempt" to return a (the first time), then execute the finally, which actually returns a. However, javac complains that "a" might not have been initialized:

    Test.java:8: variable a might not have been initialized  
        return a;  
               ^  

The only thing I can think of that might cause / allow a different code path is if an obscure runtime exception were to occur after the start of the try but before the value 1 is assigned to a - something akin to an OutOfMemoryError or a StackOverflowException, but I can't think of any case where these could possibly occur at this place in the code.

Can anyone more familiar with the specifics of the Java standard shed some light on this? Is this just a case where the compiler is being conservative - and therefore refusing to compile what would otherwise be valid code - or is something stranger going on here?

A: 

The compiler is just being conservative here.

Itay
Actually, the compiler is REQUIRED to be conservative here; see @msaeed's answer.
Stephen C
+3  A: 

I believe it's just due to the semantics of a try-catch-finally relationship. From the Java Language Specification:

If execution of the try block completes normally, then the finally block is executed...

If execution of the try block completes abruptly because of a throw of a value V...

If execution of the try block completes abruptly for any other reason R, then the finally block is executed...

The last case seems to be the most relevant here. It seems that the finally block should be able to be correctly executed if the the try block completes abruptly for ANY reason. Obviously if the try block ended before the assignment the finally block would not be valid. Though, as you said, this is not particularly likely.

Falaina
+2  A: 

It's very likely javac is required to make the blanket assumption that an exception could occur at any point in the try block, even during the assignment, and that therefore the finally might return an uninitialized variable. In theory it could do a detailed analysis and discover that in all paths through the try block 'a' would always be initialized successfully, but that's a lot of work for almost no gain.

Now if someone just can point out the relevant section in the Java Language Specification ...

Jim Ferrans
This behavior is specified by the language specification. The compiler has to issue that error and is not permitted to suppress it using any additional analysis.
notnoop
@msaeed: Thanks, that's exactly what I would suspect.
Jim Ferrans
+8  A: 

It may seem counter intuitive that an exception could occur on the a=1 line, but a JVM error could occur. Thus, leaving the variable a uninitialized. So, the compiler error makes complete sense. This is that obscure runtime error that you mentioned. However, I would argue that an OutOfMemoryError is far from obscure and should be at least thought about by developers. Furthermore, remember that the state that sets up the OutOfMemoryError could happen in another thread and the one action that pushes the amount of heap memory used past the limit is the assignment of the variable a.

Anyways, since you are looking at compiler design, I'm also assuming that you already know how silly it is to return values in a finally block.

Elijah
+1 This is a good point.
Jim Ferrans
I was doing my best to exercise the JSR (Jump SubRoutine) jitting code in Avian (for this, I had to use ECJ, because sun javac only rarely generates JSRs anymore), and the test case I condensed this example from was significantly more complicated. I am quite aware of the pointlessness of such code - but if it can be compiled, Avian must correctly run it.
Joshua Warner
+3  A: 

The Java Language Specification requires that a variable is assigned before it is used. The JLS defines specific rules for that known as "Definite Assignment" rules. All Java compilers need to adhere to them.

JLS 16.2.15:

V is definitely assigned before the finally block iff V is definitely assigned before the try statement.

In another words, when considering the finally statement, the try and catch block statements within a try-catch-finally statement assignments are not considered.

Needless to say, that specification is being very conservative here, but they would rather have the specification be simple while a bit limited (believe the rules are already complicated) than be lenient but hard to understand and reason about.

Compilers have to follow these Definite Assignment rules, so all compilers issue the same errors. Compilers aren't permitted to perform any extra analysis than the JLS specifies to suppress any error.

notnoop
+1 @msaeed, good find!
Jim Ferrans
+1  A: 

I guess Java Compiler assumes the worst case - there is no guarantee that anything in the try block is even executed due to some reason. So its complaint is valid. The variable might not have been initialized.