Effectively Final
A compiler plugin to enforce function parameters are either explicitly or effectively final.
It’s best practice to not assign to a parameter in the body of a function. You can find lots of opinions about this but here’s a good summary on Stack Overflow.
One issue is that it’s a bit tedious to have to write the modifier final
before each parameter declaration and with the addition of Java 8’s “effecitvely final” concept it seems reasonable to not require final
and enforce via the compiler. This is less clutter in your code.
Explicitly final:
public class ExplicitlyFinalExample {
public void sendData(final String alpha, final String beta,
final int gamma, final List<String> omega) {
System.out.printf("sending %s, %s, %s, %s\n",
alpha, beta, gamma, omega);
}
}
Effectively final:
public class EffectivelyFinalExample {
public void sendData(String alpha, String beta,
int gamma, List<String> omega) {
System.out.printf("sending %s, %s, %s, %s\n",
alpha, beta, gamma, omega);
}
}
In both snippets above, the method params are final. They are either final because they are explicitly declared as being final or they are effectively final because they are never assigned to.
However, it’s easy for the programmer to break this contract in the effectively final example by assigning to one of the method params in the body of the method. As long as this method parameter isn’t used in an Anonymous Class or within a lambda expression, it’s still valid.
This is something we can detect with a Java Compiler Plugin and fail the build
Reporting an error when breaking the effectively final contract:
public class BreakingEffectivelyFinalExample {
public void sendData(String alpha, String beta,
int gamma, List<String> omega) {
if (gamma < 100) {
gamma = 100; // <|--- plugin will generate a compilation error on this line
}
System.out.printf("sending %s, %s, %s, %s\n",
alpha, beta, gamma, omega);
}
}
Compilation Error
BreakingEffectivelyFinalExample.java:[5,13] error: EFFECTIVELY_FINAL: Assignment to param in ``gamma = 100
Plugin Details
There’s a working copy of this plugin in github and published to Maven Central.
Java 8 introduced the ability to write plugins for the Java Compiler. When properly configured, your instance of Plugin will load that allows you to visit the compilation unit and in this case perform a little extra validation.
The plugin consists of the following components:
Plugin Implementation
You need to write a class that implements the Plugin interface. The plugin proper doesn’t do anything other than advertise its name and when initialized add a TaskListener.
TaskListener
The TaskListener receives callbacks to indicate when a task is starting or stopping. In our case we only care about the finished event of the analyze stage.
Scanner / Visitor
The TreePathScanner allows you to traverse the compilation unit and visit each of the nodes in the AST. In our case, we want to visit each of the methods to see if the parameters are assigned to in violation of the effectively final rule.
If we’re visiting a method, we want to record any of its non-final params and if we have one or more non-final param then we want to traverse into the body and hit every assignment.
We’re relying on javac to report any real errors and thus our logic here can be simpler.
For example, when visiting a method, we can ignore any context passed into us since any params in scope from enclosing methods must already be explicitly or effectively final.
Consider the following:
void foo(int a, int b) {
MyClass mc = new MyClass() {
void differentMethod(int c, int d) {
a = c + d; // <--- javac will catch for us
}
}
}
Therefore, when we encounter a method like we have here, we can disregard the param given to us and traverse into the body of the method with all the non-final params from just this method.
It’s tempting to not traverse if all the params are final, or it has no params but there could be something in the body like an inner class that has an assignment to one of its own params.
ServiceLoader Configuration
The compiler is loaded via the java.util.ServiceLoader
pattern, so you need to have the fully qualified name of your plugin in META-INF/services/com.sun.source.util.Plugin
.
Maven Config Example
The maven compiler plugin supports arguments to javac. The plugin’s name needs to be passed as an arg as shown below.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<compilerArgs>
<arg>-Xplugin:EffectivelyFinal</arg>
</compilerArgs>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
</configuration>
<dependencies>
<dependency>
<groupId>com.massfords</groupId>
<artifactId>effectively-final</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</plugin>