Wednesday, August 19, 2015

"Variable is accessed within inner class. Needs to be declared final."

Problem:

"Variable is accessed within inner class. Needs to be declared final." or a similar error occurs.

Solution:

Declare the variable final, or make it an instance variable.

Explanation:

Java doesn't want developers to change local variables from within an inner class or an anonymous inner class.

Inner Classes and Local Variables

Any variable defined in a method and accessed by an anonymous inner class must be final. Or, as Oracle says:
"An anonymous class cannot access local variables in its enclosing scope that are not declared as final or effectively final"
Note: "effectively final" is something new introduced in Java SE 8. It is defined as a variable or parameter that is not declared as final, whose value is never changed after it is initialized.

But why make it so inner classes can't modify variables belonging to their outer scope?
The reason is that the inner class "captures" the variable. To understand why this matters, we need to understand the implications of how captured variables work. If you are familiar with closures, that is exactly what's going on here.
The inner class is a closure. It copies the variable from it's enclosing scope to a new variable, and brings just that copy inside the inner class. Anything it does to that copy is independent from the variable in the enclosing scope. So if the variable changes in the inner class, and then it is used later in the enclosing scope, the changes made in the inner class did not persist in the enclosing scope.

Basically, what happens in the inner class stays in the inner class.
Below is a proper scenario.

 
public class RadiusStuff { //This is the outer class
    public void start(Stage stage) { //This method is the enclosing context
    Button submit = new Button("Submit");
    final string radius = "10";
    submit.setOnAction(new EventHandler<ActionEvent>() {
        //This is the inner class, specifically an anonymous class.
        @Override
        public void handle(ActionEvent e) {
            submit.setText(radius);
        }
    });
  }
}

This functionality is by design.

Java wants the developer to use the final keyword on any variables that are going to be modified in the inner class. This prevents us from thinking the things we change in the inner class will persist in the enclosing scope.
In a way, adding the final keyword does not change the behavior of the code. Think about it. With or without the final keyword, any changes to the variable in the inner class won't persist, so why make any changes at all? Java forces developers to use final in this scenario just to emphasize that we shouldn't be modifying a local variable in an inner class.

But what if, in the closure, we are only reading from the variable and not writing to it? Does it still need to be declared final?
If you are using Java 7 or below, the answer is "yes".
In Java 8, if we are only accessing but not changing the variable, the variable is "effectively final" and does not need to be declared final.

The problem with this is that if we ever want to change the variable we won't be able to. In the below code I show an example of this case by assigning a new value to our "radius" variable, but it will not work.
 
public class RadiusStuff { //This is the outer class
    public void start(Stage stage) { //This method is the enclosing context
    Button submit = new Button("Submit");
    final string radius = "10";
    submit.setOnAction(new EventHandler<ActionEvent>() {
        //This is the inner class, specifically an anonymous class.
        @Override
        public void handle(ActionEvent e) {
            submit.setText(radius);
        }
    });
    radius = "15"; //This will throw compile-time error.
  }
}

Instance Variables

I mentioned under the "Solution" section that we could also just make the variable an instance variable. Does this really work? Why?
Referring back to Oracle's documentation we see that our inner class "cannot access local variables in its enclosing scope that are not declared as final..."
So why would an instance variable be an exception? Note the words "local variable". This only applies to variables declared within the method that the inner class is in, also known as the "enclosing scope".
So does this mean an anonymous inner class can change an instance variable, and have those changes persist outside of the inner class?
Yes.

 
public class RadiusStuff { //This is the outer class
    string radius = "10"; //Instance variable, no need to be final.

    public void start(Stage stage) { //This method is the enclosing context
    Button submit = new Button("Submit");
    
    submit.setOnAction(new EventHandler<ActionEvent>() {
        //This is the inner class, specifically an anonymous class.
        @Override
        public void handle(ActionEvent e) {
            submit.setText(radius);
        }
    });
    string radius = "15"; //This is allowed now.
  }
}

Other notes:

  • Variables defined in interfaces are implicitly final, even if they don't have the final keyword.
  • For variables that reference objects, the properties of the object can be modified, even if the variable is final. However, you cannot change which object the variable refers to.
  • You can find excellent information on closures here: C# in depth
  • The behavior mentioned in my post is similar between Java and C#, but there are some differences, so be careful. Perhaps I'll discuss those in a future post.