Annotation-Based Null Analysis in Eclipse
This guide will walk you through Eclipse's annotation-based null analysis. Though it might cause chaos in large code bases, it will keep things cleaner in the long run.
Join the DZone community and get the full member experience.
Join For FreeThe NullPointerException is always an unexpected event and points toward a programming error. This article will help to avoid these kinds of errors by learning about how Eclipse can help to eliminate these problems — by using the "Eclipse annotation-based null analysis." For the sake of simplicity, I will just be calling it "null analysis."
Besides null analysis, Eclipse offers the flow analysis, which is always running in the background. It shows obvious null access locations.
But this approach is very limited because Eclipse cannot know in general if a method might return null or not.
This exact missing information is what we can add with annotations, and that's what we will talk about in this article.
Null analysis is a formal verification. We augment the source code with statements if something “might be null” or “must never be null.” And null analysis checks for consistency, while the flow analysis can now work in a much more complete manner.
@NonNull or @Nullable, That Is the Question
All used annotations are imported from org.eclipse.jdt.annotation.
@NonNull and @Nullable can be applied to local variables, fields, method parameters, and return types, as well as type parameters in generics.
If something is annotated with @Nullable, this means, this “something” might hold the null reference. If there is setting for the defaults, everything without an annotation is seen as @Nullable or maybe @Nullable. But this standard behavior can be changed. Later, I show how.
@Nullable
private Listener listener;
If something is annotated with @NonNull, it means that the value must never be null.
A method parameter can only be passed if it's proven to be non-null. And the method implementation can rely on that without doing null checks.
A return value must be non-null. The null analysis verifies the method implementation is holding this. A caller can rely on it without doing null checks.
Fields must be assigned with non-null values at least at the end of the constructor.
@NonNull
private Listener listener = new Listener(){ … };
If a field is not initialized directly or in the constructor, an error is issued.
class A {
@NonNull
private Listener listener; // error
}
@NonNullByDefault
If the null analysis is going to be used in a project, in my opinion, it makes sense to put everything by default to be @NonNull. This can be done with a third annotation, @NonNullByDefault. It can be applied to methods, classes, or the whole package.
To annotate a package, there must be a package-info.java in that package. It might look like this:
@org.eclipse.jdt.annotation.NonNullByDefault
package name.of.pack;
The Rest of the World?
To succeed in the transition to the outside world, the null analysis must be integrated smoothly.
class A {
@NonNull
private String str = System.lineSeparator();
}
Without additional information, the null analysis has to flag this line at least as a warning. Because it is not known if the method-returned value from System.lineSeparator() is @NonNull.
By reading the documentation and the source code of System.lineSeparator(), it is clear that the return value is always @NonNull. Hence, we want to define this once so the null analysis can access this information in future. But we cannot modify the external source code.
“Eclipse External Annotation” (EEA) offers a mechanism to add this information for external libraries. Later, we will see how to use it.
Preparation
To use null analysis, the compiler settings and the EEA (Eclipse External Annotation) path must be configured.
First the compiler settings:
Go to the project settings ⇒ Java Compiler ⇒ Warnings/Errors ⇒
Null analysis: Check “Enable annotation-based null analysis” (a pop-up will do the next two for you)
Null analysis: “Null pointer access”: Error
Null analysis: “Potential null pointer access”: Error
For the “Eclipse External Annotations,” a folder holding this information must be set. In my project, we use the location castle.util/eea.
Project Properties ⇒ Java Build Path ⇒ Libraries ⇒
JRE ⇒ External Annotations: /castle.util/eea
Plugin Dependencies ⇒ External Annotations: /castle.util/eea
Migration
If you work with an existing code base, the sheer amount of shown issues might be very high. I had about 5,000 findings. Most likely you cannot fix them all at once.
Because of that, I suggest you have the versioned project settings unchanged and work only locally with null analysis activated. When all errors are solved, then the null analysis can be activated for all team members.
Applying Annotations
The following are valid for annotations in general in Java. Some of these were surprising for me, as I haven’t encountered them before.
Sequence of Modifiers
@Override
@Nullable
public static Listener getListener(){
// ...
}
@Override
public static @Nullable Listener getListener(){ // not conformant
Both examples are technically identical, but when you use SonarLint, it verifies you keep the sequence of modifiers as suggested in the Java language specification.
This “correct” sequence is a bit un-intuitive because here, the location of @Nullable seems to apply for the method and not the return value.
Arrays
Arrays are two things: the array object itself and its elements. And for both annotation can be applied individually.
This makes the array element @Nullable:
@Nullable
private Listener[] listeners;
private @Nullable Listener[] listeners2; // same as before
@Nullable
private Listener[] getListeners(){ … }
Now, only the array, while the elements are not annotated:
private Listener @Nullable[] listeners = null; // OK
And last, the array and its elements:
@Nullable
private Listener @Nullable[] listeners = null;
Qualified Names
When using explicit qualifiers, containing the package or the outer type name, the annotation is in between.
private name.of.pack.@Nullable Listener listener;
for( Map.@NonNull Entry<String, String> : map.entrySet() ){ … }
Dealing With External Code
Let's use this example:
The method has the implicit @NonNull for the return type because we have activated @NonNullByDefault on the package. Hence, the null analysis demands the return value of sb.toString() to be @NonNull.
But the JRE StringBuilder class has no annotations. And Eclipse cannot know if this is a problem or not. Hence, the warning.
To solve the situation, we make a judgment and declare whether StringBuilder.toString() returns @Nullable or @NonNull.
The precondition is that we have the source code of StringBuilder visible in the project. When using the JDK, this is normally the case.
Now we can use “external annotations:”
- Navigate to the method’s source (Cursor onto the “toString()”, then F3).
- Place cursor onto the return type.
- Press Ctrl-1 and make the choice.
If not yet existing, this creates a file in the previously configured EEA location. I configured “/castle.util/eea” so it creates the files “/castle.util/eea/java/lang/StringBuilder.eea”.
By examining the documentation and optionally also the source code of the StringBuilder.toString() method, I can make a decision on the return type being @NonNull or @Nullable.
If I choose @Nullable, as a consequence, the warning in my code would turn into an error. Because now Eclipse knows that this is a problem.
If I choose @NonNull, the warning in my code goes away.
The stored information in the EEA file looks like this:
class java/lang/StringBuilder
toString
()Ljava/lang/String;
()L1java/lang/String;
It identifies the method with its signature and gives the alternative with the null annotation change. A 1 indicates @NonNull, and a 0 indicates @Nullable. Here, you can read it as "the toString method with no argument and a return type of @NonNull String."
Override External Methods
The following example overrides an “equals” method.
By having the @NonNullByDefault, the Object parameter is seen as @NonNull. But this conflicts with the super method because it does not have the @NonNull annotation.
To solve the problem, the signatures must be made compatible with their null annotations.
One possibility is to change the external class (java.lang.Object here). This is possible with EEA, but it would be wrong in this example. The parameter of the Object.equals() method is allowed to be null, according to the documentation.
Hence the local class must be adapted:
In other cases, changing the base class with EEA might be the right decision.
Check Fields for Null
@Nullable
private Path outputPath;
void action(){
if( outputPath == null ){
outputPath = getDefault();
}
// Potential null pointer access:
// this expression has a '@Nullable' type
outputPath.toString();
}
Eclipse shows an error, but the field is checked for being null. At first glance, this is surprising. But the problem is that running multithreaded code could have set this field to null in the time in between.
The safe solution is to read the field into a local variable and, if needed, write it back.
void action(){
Path path = outputPath;
if( path == null ){
path = getDefault();
outputPath = path;
}
path.toString();
}
Now this method is shown in Eclipse without errors.
Utilities
In org.eclipse.jdt.annotation.Checks, you can find some useful methods when working with the null analysis.
Avoid @Nullable fields
For sure, everything would be easier if we could avoid @Nullable completely. But often, other things have a shorter lifetime, or the initialization is not finished in the constructor.
If there are many references to @Nullable fields, we need a lot of null checks. And this is to make the tool (Eclipse null analysis) happy. This is frustrating and makes the code less readable.
In simple cases, we can find a good default value. A String field might be initialized with the empty string. But when this is done, you must be careful. You need to check if the existing code does a null check and runs a different behavior onto this decision. If this is found, the existing code must first be changed. Otherwise, you introduce a logic error.
The Java 8 Optional class and the “Null Object Pattern” can be used to convert @Nullable fields into @NonNull. But most often, this does not solve the problem. Instead, it only moves or hides the problem. With this, you might even work against the null analysis, as the static checks are now runtime checks.
In my opinion, it is best to redesign the classes to have as few @Nullable fields as possible.
As an example, I want to show a refactoring of a JFace dialog to have only one single @Nullable field left.
Here the starting scenario:
class MyDialog extends TitleAreaDialog {
@Nullable Button enableCheckBox;
public void createDialogArea( Composite parent ){
// ...
enableCheckBox = new Button( parent, SWT.CHECK );
// ...
}
}
The example shows a typical JFace dialog. Here, the problem is that the initialization of the widgets (Button, Text, Label, etc.) is not done in the constructor. Instead, JFace (TitleAreaDialog) uses the strategy pattern and, therefore, the application dialog (MyDialog) must override abstract methods that will be called by the base class. In MyDialog, the method createDialogArea is called by TitleAreaDialog while the dialog is opening.
In this createDialogArea method, all the widgets are created. Unfortunately, MyDialog cannot pre-create its widgets in the constructor itself because the SWT widgets expect to receive their parent composite reference in their constructors. And this parent composite is only given as a parameter to the method createDialogArea().
When the class is written with this pattern, this leads to all fields referencing widgets or other UI components to be @Nullable.
One solution can be to create an inner class representing the “initialized” state. It is instantiated in the createDialogArea() and can initialize all fields in the constructor as @NonNull.
class MyDialog extends TitleAreaDialog {
@Nullable Gui gui;
public void createDialogArea( Composite parent ){
gui = new Gui( parent );
}
class Gui {
Button enableCheckBox;
Gui( Composite parent ){
// ...
enableCheckBox = new Button( parent, SWT.CHECK );
// ...
}
}
}
Now the class is separated into two parts: the outer class derived from TitleAreaDialog and the inner class with the @NonNull UI fields.
Listeners and other UI code in the inner class can now work without null checks. If other methods must be overridden from TitleAreaDialog, they must do the null check once for the inner class instance and can then delegate to it.
Conclusion
The null analysis creates, on an existing code base, a significant amount of work and leads to source code with many null checks. But this will lead to better design in future because the analysis makes your null safety deficits more visible.
Published at DZone with permission of Frank Benoit. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments