JDK-8299416 : javac complains about "local variables referenced from an inner class must be final or effectively final" although variable is only assigned once
  • Type: Bug
  • Component: tools
  • Sub-Component: javac
  • Affected Version: 20,21,22,23,24
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2022-12-29
  • Updated: 2025-04-07
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 26
26Unresolved
Related Reports
Blocks :  
Relates :  
Relates :  
Description
When we updated the preview builds on Apache Lucene's nightly runs from Java 20-ea+17 to Java 20-ea+29, one of our test classes no longer compiled: https://github.com/apache/lucene/blob/c180c5cdabed7a64783515ec3086a3f7965b8cc6/lucene/replicator/src/test/org/apache/lucene/replicator/nrt/TestStressNRTReplication.java

I extracted the essence of this class (one method) into a new file and replaced some external packages/classes by dummy values or java.lang.Object. So please ignore if the code is "senseful", it should just show the problem, it won't run. It is only there to compile.

When compiling the attached CompileFailure.java with Java 20-ea+29, it fails like the following:

$ javac CompileFailure.java
CompileFailure.java:269: error: local variables referenced from an inner class must be final or effectively final
                if (childLog != null) {
                    ^
CompileFailure.java:271: error: local variables referenced from an inner class must be final or effectively final
                    childLog.write("process done; exitValue=" + exitValue + "\n");
                    ^
CompileFailure.java:272: error: local variables referenced from an inner class must be final or effectively final
                    childLog.close();
                    ^
CompileFailure.java:282: error: local variables referenced from an inner class must be final or effectively final
                  if (childLog != null) {
                      ^
CompileFailure.java:289: error: local variables referenced from an inner class must be final or effectively final
                            + childLog
                              ^
5 errors

When looking at the code, "childLog" is only assigned once with an if/else statement and never changed later. Previous versions (Java 8, 11, 17, and also Java 20-ea+17) compile that file sucessfully. It can be easily tested with above command line.

To my understanding, the code is valid and the variable *is* effectively final.

As workaround you can declare the variable as final and then compilation passes.

This is a major regression, as the code looks not too complicated and patterns like this may happen more often in productive code out there (declaring variable without final and assign a value with if/else and later use it in anonymous class). I have to say, that it was not easily to reproduce with a simple 10-liner (only the if/else and the Runnable), but still this looks like a big problem.

In Apache Lucene we fixed this by adding a final before the variable: https://github.com/apache/lucene/commit/e32b95e3161cbc2abf68d6e3bb3f609ebaa5b6ea

The problem seems to be caused by JDK-8294461, which was added in build 22. That is the only commit in javac that touched the code around "effectively final". The code mentoned here does not have the described problem (as far as I understand).
Comments
JDK-8294461 has been reverted, and the code should compile again, so I don't think this qualifies as P2 anymore. Keeping this bug open for now to track any outcomes of JDK-8299861.
11-01-2023

Hi Jan, thanks for update, this looks fine. I also like JDK-8299861, it is precisely describing the problem. Uwe
10-01-2023

As an update, there's a plan to evaluate the specification wording, I've filled JDK-8299861 for that. For JDK 20, the proposal is to revert the change in javac for effectively final, filled as JDK-8299849.
10-01-2023

bq. Yes, having a constant true of false expression is the key here. The definition of effectively final per JLS is unchanged, only javac is implementing the check more properly now. OK, I misunderstood that because there was a CSR and I interpreted it to change the spec. The problem here is caused by how the definition differs from common understanding, as it is very formal. It is a bit strange and hard to explain even in university courses what's wrong if you see the above example codes with true/false/non-constant. I played around and indeed depending on the if statement it compiles or does not compile, which makes it a frustrating experience: C:\Users\Uwe Schindler\Desktop\Bug>type EffectivellyFinal.java public class EffectivellyFinal { public void test() { int i; if (false) { i = 0; } else { i = 1; } new Object() { void t() { System.err.println(i); } }; } } C:\Users\Uwe Schindler\Desktop\Bug>javac EffectivellyFinal.java EffectivellyFinal.java:11: Fehler: Von Innere Klasse referenzierte lokale Variablen müssen final oder effektiv final sein System.err.println(i); ^ 1 Fehler C:\Users\Uwe Schindler\Desktop\Bug>type EffectivellyFinal.java public class EffectivellyFinal { public void test() { int i; if (true) { i = 0; } else { i = 1; } new Object() { void t() { System.err.println(i); } }; } } C:\Users\Uwe Schindler\Desktop\Bug>javac EffectivellyFinal.java EffectivellyFinal.java:11: Fehler: Von Innere Klasse referenzierte lokale Variablen müssen final oder effektiv final sein System.err.println(i); ^ 1 Fehler C:\Users\Uwe Schindler\Desktop\Bug>type EffectivellyFinal.java public class EffectivellyFinal { public void test(boolean x) { int i; if (x) { i = 0; } else { i = 1; } new Object() { void t() { System.err.println(i); } }; } } C:\Users\Uwe Schindler\Desktop\Bug>javac EffectivellyFinal.java (compiles fine) C:\Users\Uwe Schindler\Desktop\Bug> The related issue's CSR states: "The patch was ran through a corpus of Java source code, and there was only one project out of ~45000 that failed to compile due to this change." => which project was that? P.S.: The last sentence was purely my personal opinion and was not meat to change anything. It is already long time ago, but I always had my problems with "effectively final" (I personally declare everything as final and tend to use unmodifiable object instances).
02-01-2023

Yes, having a constant true of false expression is the key here. The definition of effectively final per JLS is unchanged, only javac is implementing the check more properly now. As I said, we could think of making the behavior dependent on source level (although, frankly, I'd first like to see some data on how common the problem is). I am personally not sure (yet, at least) why the spec is written in a way it is written, but unless and until it is changed, it specifies the way javac should behave. Discussion on whether Java should or should not have effectively final seems purely theoretical to me - Java has effectively final for a long time.
02-01-2023

Basically, I think this issue is caused by the fact that you changed the definition of "effectively final" in JDK-8296148, but this had the side effect that this happened. IMHO, should the compiler not use the "--release" or "-source" parameter to use correct semantics? Actually the defintiion of "effective final" is very theoretical, and just because you explained why this happens here (it seems to only only happen for "if (false)"), to me it makes clear that the spec is wrong or has a bug. There should be no difference between true/false/non-constant. If I would have written the spec for "effectively final", i would just have added the following sentence to the spec: "A variable is effectively final if the compiler would not produce a compile error when the keyword "final" would have been added as modifier." (plain simple and that's how "users" understand it). If it would have been also implemented like that: "make the compiler add final and try if it would trigger error" those errors would not have been happened from beginning (including the one in JDK-8296148). My personal opinion: Effective final was a bad idea from beginning; instead forcing user to add "final" around Java 7/8 would have been much better. The current lazyness is not good for a hard typed language.
02-01-2023

Just to confirm my understanding from your analysis: The important thing here is the constant expression in the "if (false)" statement? In our code this is also "false" because the test code has it hardcoded to be SEPARATE_CHILD_OUTPUT = false. This explains why I was not able to reproduce the compile failure when I wrote a simple code pattern seen everywhere with an if statement based on a condition e.g. passed in a method parameter, e.g., "if (something == 123)" (with something is some method parameter or similar). I did not notice that the if expression was constant (stupid as I was, in Lucene's code I did not see the @SuppressWarnings("null") by eclipse, which shows you asap that childLog can never be null and that should have made me cautious!). On top, we have disabled the "dead code" warning, too. So this means the bug is not so important, because only code with a constant if expression will fail compilation, although it is very hard to understand for somebody looking at the code. Maybe set priority of this issue to medium and think of a better error message or relaxing the "effectively final". Why did this issue appear after JDK-8294461 was applied? Because the change there was about counter variables in for statements!
02-01-2023

So, this is quite tricky. First, a simplified testcase: --- public class EffectivellyFinal { public void test() { int i; if (false) { i = 0; } else { i = 1; } new Object() { void t() { System.err.println(i); } }; } } --- One important element here is that the conditions/requirements for "final" and "effectively final" differ slightly. In particular, for (assignment to) final, the condition is (JLS 16): <quote> For every assignment to a blank final variable, the variable must be definitely unassigned before the assignment, or a compile-time error occurs. </quote> For effectively final, a variable is effectively final if (JLS 4.12.4.), when assigned, it is definitely unassigned *and* not definitely assigned: <quote> Whenever it occurs as the left hand side in an assignment expression, it is definitely unassigned and not definitely assigned before the assignment; that is, it is definitely unassigned and not definitely assigned after the right hand side of the assignment expression (§16 (Definite Assignment)). </quote> So, if at the point of "i = 0;" would "i" be both definitely unassigned and definitely assigned, it would not be effectively final. Now, can it happen that a variable is definitely unassigned and definitely assigned at the same time? Lets first look at JLS 16.2.7: <quote> The following rules apply to a statement if (e) S ... V is [un]assigned before S iff V is [un]assigned after e when true. </quote> I.e. variables are definitely assigned and unassigned before the then statement, as they were after the condition, when the condition evaluates to true. (Note here two sets of definitely assigned/unassigned are kept for conditions - one when the condition evaluated to true and one when it evaluated to false.) So, can a variable be both definitely assigned and unassigned (at the same time) when true after a condition? Turns out it can (JLS 16.1.1.): <quote> V is [un]assigned after any constant expression whose value is false when true. </quote> In this particular case, the condition is a constant expression of value "false". Which means all variables are both definitely assigned and unassigned when true after the condition. JLS says this about it: <quote> Because a constant expression whose value is true never has the value false, and a constant expression whose value is false never has the value true, the first two rules are vacuously satisfied. They are helpful in analyzing expressions involving the operators && (§16.1.2), || (§16.1.3), ! (§16.1.4), and ? : (§16.1.5). </quote> So, at the point of the "i = 0" assignment, "i" is both definitely assigned and unassigned at the same time, and hence it is not effectively final, although it is OK for it to be final (because assignments to (blank) final variables don't verify definite assignment, only definite unassignment). So, technically javac is right in rejecting this code. I suppose there may be some thought on the importance of the difference between final and effectively final, and possibly on limiting the change from JDK-8294461 based on source levels.
02-01-2023