Why require local variables to be final when acces

2019-05-15 05:48发布

问题:

We all know you can't do things like this:

int a = 7;
new Runnable() {
     public void run() {
         System.out.println(a);
     }
}.run();
...

...without making a final. I get the technical reason why and it's because local variables live on the stack and you can't safely make a copy unless you know it won't change.

What I struggle to see however is why the compiler doesn't have an implementation hack so that it when it sees the above situation it compiles down to something like:

int[] a = {7};
new Runnable() {
    public void run() {
        System.out.println(a[0]);
    }
}.run();
...

Then we're in the position where it's safe to access a from an anonymous inner class and indeed change it if we wish. Of course, it might only do this hack when we actually change a. As far as I could see this would be a relatively simple thing to put in, would work for all types and would allow a to be changed from whatever context. Of course, the above proposal could be changed to use synthetic wrapper classes for multiple values or another approach that's a bit more efficient, but the idea is the same. I guess there's a small performance hit, but I doubt it'd be excessive especially with the potential for more optimisations under the hood. Aside from perhaps certain reflective calls that rely on synthetic fields being a certain way breaking, I can't see many disadvantages, but I've never heard it seriously proposed! Is there a reason why?

回答1:

When the anonymous inner class is constructed, the values of all the variables used within it are copied. So if the inner class then tried to change the value of the variable, that wouldn't be visible. For example, suppose this were valid:

int a = 7;
Runnable r = new Runnable() {
    public void run() {
        a = 5;
    }
};
r.run();
System.out.println(a);

You might expect it to print 5 (which indeed it would in C#). But because only a copy has been taken, it would actually print 7... if it were allowed, with no bigger changes.

Of course, Java could have been changed to really capture the variable instead of its value (as C# was for anonymous functions). That requires automatically creating an extra class to store the "local" variables, and make both the method and the anonymous inner class share an instance of that extra class. That would have made anonymous inner classes more powerful, but arguably harder to understand. C# decided to go for the power-but-complexity route; Java went for the restrictive-but-simple approach.

(Using an array instead of a custom class is valid for a single variable, but becomes more wasteful when there are multiple variables involved - you don't really want to have to create a wrapper object for every variable if you can help it.)

Note that there are significant complexities involved in the capture-the-variable approach, at least using the C# rules. For example:

List<Runnable> runnables = new ArrayList<Runnable>();
int outer = 0;
for (int i = 0; i < 10; i++) {
    int inner = 0;
    runnables.add(new Runnable() {
        public void run() {
            outer++;
            inner++;
        }
    });
}

How many "inner" variables are created? One for each instance of the loop, or one overall? Basically, scopes make life tricky for this sort of thing. Feasible, but tricky.



回答2:

Another problem you have (and can have in groovy which doesn't have this restriction) is that non final variables can change (otherwise you wouldn't have a problem making it final)

int a = 1; 
// use a in another thread, or potentially run it later
a = 2; 

Should the thread always see a = 1 or can it sometimes see a = 2.

The only way to write predictable anonymous classes is if the local variables don't change. The compiler could be smarter and detect when a variable can no longer change and that would simplify some odd cases IMHO. But the simplest solution is to require the local variable to be final.

BTW: An alternative which my IDE has an auto-fix for is to use an array of one instead.

final int[] a = { 1 };
//  use a[0] in an anonymous class.
a[0] = 2;

While ugly, it does make it unlikely you will accidentally write an anonymous class where the value could change.



回答3:

Given that anonymous inner classes can outlive the scope in which they were created, I don't see how you're 'hack' would be easy to implement. The inner class still needs to maintain copies of all the variables it has closed around, and as such they cannot refer to variables that have been created on the stack.