The Checker Framework Manual:

Custom pluggable types for Java https://checkerframework.org/ Version 3.6.1 (2 Sep 2020)

For the impatient: Section 1.3 describes how to install and use pluggable type-checkers.

This manual is also available in PDF.

Contents

Chapter 1 Introduction

The Checker Framework enhances Java’s type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs.

A “checker” is a tool that warns you about certain errors or gives you a guarantee that those errors do not occur. The Checker Framework comes with checkers for specific types of errors:

These checkers are easy to use and are invoked as arguments to javac.

The Checker Framework also enables you to write new checkers of your own; see Chapters 24 and 32.

1.1 How to read this manual

If you wish to get started using some particular type system from the list above, then the most effective way to read this manual is:

Read all of the introductory material (Chapters 1–2).

Read just one of the descriptions of a particular type system and its checker (Chapters 3–25).

Skim the advanced material that will enable you to make more effective use of a type system (Chapters 26–36), so that you will know what is available and can find it later. Skip Chapter 32 on creating a new checker.

1.2 How it works: Pluggable types

Java’s built-in type-checker finds and prevents many errors — but it doesn’t find and prevent enough errors. The Checker Framework lets you define new type systems and run them as a plug-in to the javac compiler. Your code stays completely backward-compatible: your code compiles with any Java compiler, it runs on any JVM, and your coworkers don’t have to use the enhanced type system if they don’t want to. You can check part of your program, or the whole thing. Type inference tools exist to help you annotate your code; see Section 30.2.

Most programmers will use type systems created by other people, such as those listed at the start of the introduction (Chapter 1). Some people, called “type system designers”, create new type systems (Chapter 32). The Checker Framework is useful both to programmers who wish to write error-free code, and to type system designers who wish to evaluate and deploy their type systems.

This document uses the terms “checker” and “type-checking compiler plugin” as synonyms.

1.3 Installation

This section describes how to install the Checker Framework.

If you use a build system that automatically downloads dependencies, such as Gradle or Maven, no installation is necessary ; just see Chapter 34.

; just see Chapter 34. If you wish to try the Checker Framework without installing it, use the Checker Framework Live Demo webpage.

This section describes how to install the Checker Framework from its distribution. The Checker Framework release contains everything that you need, both to run checkers and to write your own checkers.

Alternately, you can build the latest development version from source (Section 36.3).

Requirement: You must have JDK 8 or JDK 11 installed. (The Checker Framework should work with any JDK 8–12, but we only test with JDK 8 and JDK 11.)

The installation process is simple! It has two required steps and one optional step.

Download the Checker Framework distribution: https://checkerframework.org/checker-framework-3.6.1.zip Unzip it to create a checker-framework-3.6.1 directory. Configure your IDE, build system, or command shell to include the Checker Framework on the classpath. Choose the appropriate section of Chapter 34.

That’s all there is to it! Now you are ready to start using the checkers.

We recommend that you work through the Checker Framework tutorial, which walks you through how to use the Checker Framework on the command line (Nullness, Regex, and Tainting Checkers). There is also a Nullness Checker tutorial by David Bürgin; the setup instructions are out of date, but you can read through the steps.

Section 1.4 walks you through a simple example. More detailed instructions for using a checker appear in Chapter 2.

1.4 Example use: detecting a null pointer bug

This section gives a very simple example of running the Checker Framework. There is also a tutorial that gives more extensive instructions for using the Checker Framework on the command line, and a Nullness Checker tutorial by David Bürgin.

Let’s consider this very simple Java class. The local variable ref ’s type is annotated as @NonNull , indicating that ref must be a reference to a non-null object. Save the file as GetStarted.java . import org.checkerframework.checker.nullness.qual.*; public class GetStarted { void sample() { @NonNull Object ref = new Object(); } } Run the Nullness Checker on the class. You can do that from the command line or from an IDE: From the command line, run this command: javac -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java where javac is set as in Section 34.5. To compile within your IDE, you must have customized it to use the Checker Framework compiler and to pass the extra arguments (see Chapter 34). The compilation should complete without any errors. Let’s introduce an error now. Modify ref ’s assignment to: @NonNull Object ref = null ; Run the Nullness Checker again, just as before. This run should emit the following error: GetStarted.java:5: incompatible types. found : @Nullable <nulltype> required: @NonNull Object @NonNull Object ref = null; ^ 1 error

The type qualifiers (e.g., @NonNull) are permitted anywhere that you can write a type, including generics and casts; see Section 2.1. Here are some examples:

@Interned String intern() { ... } // return value int compareTo( @NonNull String other) { ... } // parameter @NonNull List< @Interned String> messages; // non-null list of interned Strings

Chapter 2 Using a checker

A pluggable type-checker enables you to detect certain bugs in your code, or to prove that they are not present. The verification happens at compile time.

Finding bugs, or verifying their absence, with a checker is a two-step process, whose steps are described in Sections 2.1 and 2.2.

The programmer writes annotations, such as @NonNull and @Interned , that specify additional information about Java types. (Or, the programmer uses an inference tool to automatically insert annotations in his code: see Section 3.3.7.) It is possible to annotate only part of your code: see Section 29.1. The checker reports whether the program contains any erroneous code — that is, code that is inconsistent with the annotations.

This chapter is structured as follows:

Section 2.1: How to write annotations

Section 2.2: How to run a checker

Section 2.3: What the checker guarantees

Section 2.4: Tips about writing annotations

Additional topics that apply to all checkers are covered later in the manual:

Chapter 27: Advanced type system features

Chapter 28: Suppressing warnings

Chapter 29: Handling legacy code

Chapter 31: Annotating libraries

Chapter 32: How to create a new checker

Chapter 34: Integration with external tools

There is a tutorial that walks you through using the Checker Framework on the command line. There is also a Nullness Checker tutorial by David Bürgin (last updated in April 2016).

2.1 Where to write type annotations

You may write a type annotation immediately before any use of a type, including in generics and casts. Because array levels are types and receivers have types, you can also write type annotations on them. Here are a few examples of type annotations:

@Interned String intern() { ... } // return value int compareTo( @NonNull String other) { ... } // parameter String toString( @Tainted MyClass this) { ... } // receiver ("this" parameter) @NonNull List< @Interned String> messages; // generics: non-null list of interned Strings @Interned String @NonNull [] messages; // arrays: non-null array of interned Strings myDate = ( @Initialized Date) beingConstructed; // cast

You only need to write type annotations on method signatures, fields, and some type arguments. Most annotations within method bodies are inferred for you; for more details, see Section 27.7.

The Java Language Specification also defines declaration annotations, such as @Deprecated and @Override, which apply to a class, method, or field but do not apply to the method’s return type or the field’s type. They should be written on their own line in the source code.

2.2 Running a checker

To run a checker, run the compiler javac as usual, but either pass the -processor plugin_class command-line option, or use auto-discovery as described in Section 2.2.3. A concrete example of using -processor to run the Nullness Checker is:

javac -processor nullness MyFile.java

where javac is as specified in Section 34.5.

You can also run a checker from within your favorite IDE or build system. See Chapter 34 for details about build tools such as Ant (Section 34.3), Buck (Section 34.4), Gradle (Section 34.8), and Maven (Section 34.12); IDEs such as IntelliJ IDEA (Section 34.9), Eclipse (Section 34.7), NetBeans (Section 34.13), and tIDE (Section 34.14); and about customizing other IDEs and build tools.

The checker is run on only the Java files that javac compiles. This includes all Java files specified on the command line and those created by another annotation processor. It may also include other of your Java files, if they are more recent than the corresponding .class file. Even when the checker does not analyze a class (say, the class was already compiled, or source code is not available), it does check the uses of those classes in the source code being compiled. Type-checking works modularly and intraprocedurally: when verifying a method, it examines only the signature (including annotations) of other methods, not their implementations.

After you compile your code while running a checker, the resulting .class and .jar files can be used for pluggable type-checking of client code.

If you compile code without the -processor command-line option, no checking of the type annotations is performed. Furthermore, only explicitly-written annotations are written to the .class file; defaulted annotations are not, and this will interfere with type-checking of clients that use your code. Therefore, you create .class files that will be distributed or compiled against, you should run the type-checkers for all the annotations that you have written.

2.2.1 Using annotated libraries

When your code uses a library that is not currently being compiled, the Checker Framework looks up the library’s annotations in its class files.

Some projects are already distributed with type annotations by their maintainers, so you do not need to do anything special. An example is all the libraries in https://github.com/plume-lib/. Over time, this should become more common.

For some other libraries, the Checker Framework developers have provided an annotated version of the library. The annotated libraries appear in the org.checkerframework.annotatedlib group in the Central Repository. The annotated library has identical behavior to the upstream, unannotated version; the source code is identical other than added annotations. (Some of the annotated libraries are bcel, commons-csv, commons-io, guava, and java-getopt. If the library you are interested in does not appear in the Central Repository, you can contribute by annotating it, which will help you and all other Checker Framework users; see Chapter 31.)

To use an annotated library:

If your project stores .jar files locally, then download the .jar file from the Central Repository.

files locally, then download the file from the Central Repository. If your project manages dependencies using a tool such as Gradle or Maven, then update your buildfile to use the org.checkerframework.annotatedlib group. For example, in build.gradle , change api group: 'org.apache.bcel', name: 'bcel', version: '6.3.1' api group: 'commons-io', name: 'commans-io', version: '2.6' to api group: 'org.checkerframework.annotatedlib', name: 'bcel', version: '6.3.1' api group: 'org.checkerframework.annotatedlib', name: 'commons-io', version: '2.6.0.1' Usually use the same version number. (Sometimes you will use a slightly larger number, if the Checker Framework developers have improved the type annotations since the last release by the upstream maintainers.) If a newer version of the upstream library is available but that version is not available in org.checkerframework.annotatedlib, then open an issue requesting that the org.checkerframework.annotatedlib version be updated.

There is one special case. If an .astub file is shipped with the Checker Framework in checker/resources/, then you can use -Astubs=checker.jar/stubfilename.astub. The “checker.jar” should be literal — don’t provide a path. (This special syntax only works for “checker.jar”.)

2.2.2 Summary of command-line options

You can pass command-line arguments to a checker via javac’s standard -A option (“A” stands for “annotation”). All of the distributed checkers support the following command-line options. Each checker may support additional command-line options; see the checker’s documentation.

To pass an option to only a particular checker, prefix the option with the canonical or simple name of a checker, followed by an underscore “_”. Such an option will apply only to a checker with that name or any subclass of that checker. For example, you can use

-ANullnessChecker_lint=redundantNullComparison -Aorg.checkerframework.checker.guieffect.GuiEffectChecker_lint=debugSpew

to pass different lint options to the Nullness and GUI Effect Checkers. A downside is that, in this example, each of the checkers will issue one “The following options were not recognized by any processor” warning.

Unsound checking: ignore some errors

-AsuppressWarnings Suppress all errors and warnings matching the given key; see Section 28.3.

Suppress all errors and warnings matching the given key; see Section 28.3. -AskipUses , -AonlyUses Suppress all errors and warnings at all uses of a given class — or at all uses except those of a given class. See Section 28.4.

, Suppress all errors and warnings at all uses of a given class — or at all uses except those of a given class. See Section 28.4. -AskipDefs , -AonlyDefs Suppress all errors and warnings within the definition of a given class — or everywhere except within the definition of a given class. See Section 28.5.

, Suppress all errors and warnings within the definition of a given class — or everywhere except within the definition of a given class. See Section 28.5. -AassumeSideEffectFree , -AassumeDeterministic , -AassumePure Unsoundly assume that every method is side-effect-free, deterministic, or both; see Section 27.7.5.

, , Unsoundly assume that every method is side-effect-free, deterministic, or both; see Section 27.7.5. -AassumeAssertionsAreEnabled , -AassumeAssertionsAreDisabled Whether to assume that assertions are enabled or disabled; see Section 27.7.6.

, Whether to assume that assertions are enabled or disabled; see Section 27.7.6. -AignoreRangeOverflow Ignore the possibility of overflow for range annotations such as @IntRange ; see Section 21.4.

Ignore the possibility of overflow for range annotations such as ; see Section 21.4. -Awarns Treat checker errors as warnings. If you use this, you may wish to also supply -Xmaxwarns 10000 , because by default javac prints at most 100 warnings. If you use this, don’t supply -Werror , which is a javac argument to halt compilation if a warning is issued.

Treat checker errors as warnings. If you use this, you may wish to also supply , because by default prints at most 100 warnings. If you use this, don’t supply , which is a javac argument to halt compilation if a warning is issued. -AignoreInvalidAnnotationLocations Ignore annotations in bytecode that have invalid annotation locations.

More sound (strict) checking: enable errors that are disabled by default

-AcheckPurityAnnotations Check the bodies of methods marked @SideEffectFree , @Deterministic , and @Pure to ensure the method satisfies the annotation. By default, the Checker Framework unsoundly trusts the method annotation. See Section 27.7.5.

Check the bodies of methods marked , , and to ensure the method satisfies the annotation. By default, the Checker Framework unsoundly trusts the method annotation. See Section 27.7.5. -AinvariantArrays Make array subtyping invariant; that is, two arrays are subtypes of one another only if they have exactly the same element type. By default, the Checker Framework unsoundly permits covariant array subtyping, just as Java does. See Section 27.1.

Make array subtyping invariant; that is, two arrays are subtypes of one another only if they have exactly the same element type. By default, the Checker Framework unsoundly permits covariant array subtyping, just as Java does. See Section 27.1. -AcheckCastElementType In a cast, require that parameterized type arguments and array elements are the same. By default, the Checker Framework unsoundly permits them to differ, just as Java does. See Section 26.1.6 and Section 27.1.

In a cast, require that parameterized type arguments and array elements are the same. By default, the Checker Framework unsoundly permits them to differ, just as Java does. See Section 26.1.6 and Section 27.1. -AuseConservativeDefaultsForUncheckedCode Enables conservative defaults, and suppresses all type-checking warnings, in unchecked code. Takes arguments “source,bytecode”. “-source,-bytecode” is the (unsound) default setting. “bytecode” specifies whether the checker should apply conservative defaults to bytecode (that is, to already-compiled libraries); see Section 27.5.6. Outside the scope of any relevant @AnnotatedFor annotation, “source” specifies whether conservative default annotations are applied to source code and suppress all type-checking warnings; see Section 31.4.

Enables conservative defaults, and suppresses all type-checking warnings, in unchecked code. Takes arguments “source,bytecode”. “-source,-bytecode” is the (unsound) default setting. -AconcurrentSemantics Whether to assume concurrent semantics (field values may change at any time) or sequential semantics; see Section 35.4.5.

Whether to assume concurrent semantics (field values may change at any time) or sequential semantics; see Section 35.4.5. -AconservativeUninferredTypeArguments Whether an error should be issued if type arguments could not be inferred and whether method type arguments that could not be inferred should use conservative defaults. By default, such type arguments are (largely) ignored in later checks. Passing this option uses a conservative value instead. See Issue 979.

Whether an error should be issued if type arguments could not be inferred and whether method type arguments that could not be inferred should use conservative defaults. By default, such type arguments are (largely) ignored in later checks. Passing this option uses a conservative value instead. See Issue 979. -AignoreRawTypeArguments=false Do not ignore subtype tests for type arguments that were inferred for a raw type. Must also use -AconservativeUninferredTypeArguments . See Section 26.1.1.

Type-checking modes: enable/disable functionality

-Alint Enable or disable optional checks; see Section 28.6.

Enable or disable optional checks; see Section 28.6. -AsuggestPureMethods Suggest methods that could be marked @SideEffectFree , @Deterministic , or @Pure ; see Section 27.7.5.

Suggest methods that could be marked , , or ; see Section 27.7.5. -AresolveReflection Determine the target of reflective calls, and perform more precise type-checking based no that information; see Chapter 23. -AresolveReflection=debug causes debugging information to be output.

Determine the target of reflective calls, and perform more precise type-checking based no that information; see Chapter 23. causes debugging information to be output. -Ainfer= outputformat Output suggested annotations for method signatures and fields. These annotations may reduce the number of type-checking errors when running type-checking in the future; see Section 30.3. Using -Ainfer=jaifs produces .jaif files. Using -Ainfer=stubs produces .astub files.

Output suggested annotations for method signatures and fields. These annotations may reduce the number of type-checking errors when running type-checking in the future; see Section 30.3. Using produces files. Using produces files. -AshowSuppressWarningsStrings With each warning, show all possible strings to suppress that warning.

With each warning, show all possible strings to suppress that warning. -AwarnUnneededSuppressions Issue a warning if a @SuppressWarnings did not suppress a warning issued by the checker. This may issue false positive warnings about @SuppressWarnings strings without a checkername : prefix (Section 28.1.1). You can use the -ArequirePrefixInWarningSuppressions command-line argument to ensure your @SuppressWarnings always use a checkername : prefix.

Issue a warning if a did not suppress a warning issued by the checker. This may issue false positive warnings about strings without a prefix (Section 28.1.1). You can use the command-line argument to ensure your always use a prefix. -ArequirePrefixInWarningSuppressions Require that the string in a warning suppression annotation begin with a checker name. Otherwise, the suppress warning annotation does not suppress any warnings. For example, if this command-line option is supplied, then @SuppressWarnings("assignment.type.incompatible") has no effect, but @SuppressWarnings("nullness:assignment.type.incompatible") does.

Partially-annotated libraries

-Astubs List of stub files or directories; see Section 31.5.1.

List of stub files or directories; see Section 31.5.1. -AstubWarnIfNotFound Warn if a stub file entry could not be found; see Section 31.5.1.

Warn if a stub file entry could not be found; see Section 31.5.1. -AstubWarnIfNotFoundIgnoresClasses Don’t warn about missing classes (only methods and fields) even when -AwarnIfNotFound is true.

Don’t warn about missing classes (only methods and fields) even when is true. -AstubWarnIfRedundantWithBytecode Warn if a stub file entry is redundant with bytecode information; see Section 31.5.1.

Warn if a stub file entry is redundant with bytecode information; see Section 31.5.1. -AmergeStubsWithSource If both a stub file and a source file for a class are available, trust both and use the greatest lower bound of their annotations. The default behavior (without this flag) is to ignore types from the stub file if source is available.

If both a stub file and a source file for a class are available, trust both and use the greatest lower bound of their annotations. The default behavior (without this flag) is to ignore types from the stub file if source is available. -AuseConservativeDefaultsForUncheckedCode=source Outside the scope of any relevant @AnnotatedFor annotation, use conservative default annotations and suppress all type-checking warnings; see Section 31.4.

Debugging

-AprintAllQualifiers , -AprintVerboseGenerics , -Anomsgtext -AdumpOnErrors Amount of detail in messages; see Section 32.12.1.

, , Amount of detail in messages; see Section 32.12.1. -Adetailedmsgtext Format of diagnostic messages; see Section 32.12.2.

Format of diagnostic messages; see Section 32.12.2. -Aignorejdkastub , -ApermitMissingJdk , -AparseAllJdk , -AstubDebug Stub and JDK libraries; see Section 32.12.3.

, , , Stub and JDK libraries; see Section 32.12.3. -Afilenames , -Ashowchecks , -AshowInferenceSteps Progress tracing; see Section 32.12.4.

, , Progress tracing; see Section 32.12.4. -AoutputArgsToFile Output the compiler command-line arguments to a file. Useful when the command line is generated and executed by a tool, such as a build system. This produces a standalone command line that can be executed independently of the tool that generated it (such as a build system). That command line makes it easier to reproduce, report, and debug issues. For example, the command line can be modified to enable attaching a debugger. See Section 32.12.5.

Output the compiler command-line arguments to a file. Useful when the command line is generated and executed by a tool, such as a build system. This produces a standalone command line that can be executed independently of the tool that generated it (such as a build system). That command line makes it easier to reproduce, report, and debug issues. For example, the command line can be modified to enable attaching a debugger. See Section 32.12.5. -Aflowdotdir , -Averbosecfg , -Acfgviz Draw a visualization of the CFG (control flow graph); see Section 32.12.6.

, , Draw a visualization of the CFG (control flow graph); see Section 32.12.6. -AresourceStats , -AatfDoNotCache , -AatfCacheSize Miscellaneous debugging options; see Section 32.12.7.

, , Miscellaneous debugging options; see Section 32.12.7. -Aversion Print the Checker Framework version.

Print the Checker Framework version. -AprintGitProperties Print information about the git repository from which the Checker Framework was compiled.

Some checkers support additional options, which are described in that checker’s manual section. For example, -Aquals tells the Subtyping Checker (see Chapter 24) and the Fenum Checker (see Chapter 9) which annotations to check.

Here are some standard javac command-line options that you may find useful. Many of them contain the word “processor”, because in javac jargon, a checker is an “annotation processor”.

-processor Names the checker to be run; see Sections 2.2 and 2.2.4. May be a comma-separated list of multiple checkers. Note that javac stops processing an indeterminate time after detecting an error. When providing multiple checkers, if one checker detects any error, subsequent checkers may not run.

Names the checker to be run; see Sections 2.2 and 2.2.4. May be a comma-separated list of multiple checkers. Note that javac stops processing an indeterminate time after detecting an error. When providing multiple checkers, if one checker detects any error, subsequent checkers may not run. -processorpath Indicates where to search for the checker; should also contain any qualifiers used by the Subtyping Checker; see Section 24.2

Indicates where to search for the checker; should also contain any qualifiers used by the Subtyping Checker; see Section 24.2 -proc: { none , only } Controls whether checking happens; -proc:none means to skip checking; -proc:only means to do only checking, without any subsequent compilation; see Section 2.2.3

{ , } Controls whether checking happens; means to skip checking; means to do only checking, without any subsequent compilation; see Section 2.2.3 -implicit:class Suppresses warnings about implicitly compiled files (not named on the command line); see Section 34.3

Suppresses warnings about implicitly compiled files (not named on the command line); see Section 34.3 -J Supply an argument to the JVM that is running javac; for example, -J-Xmx2500m to increase its maximum heap size

Supply an argument to the JVM that is running javac; for example, to increase its maximum heap size -doe To “dump on error”, that is, output a stack trace whenever a compiler warning/error is produced. Useful when debugging the compiler or a checker.

The Checker Framework does not support -source 1.7 or earlier. You must supply -source 1.8 or later, or no -source command-line argument, when running javac.

2.2.3 Checker auto-discovery

“Auto-discovery” makes the javac compiler always run a checker plugin, even if you do not explicitly pass the -processor command-line option. This can make your command line shorter, and ensures that your code is checked even if you forget the command-line option.

To enable auto-discovery, place a configuration file named META-INF/services/javax.annotation.processing.Processor in your classpath. The file contains the names of the checkers to be used, listed one per line. For instance, to run the Nullness Checker and the Interning Checker automatically, the configuration file should contain:

org.checkerframework.checker.nullness.NullnessChecker org.checkerframework.checker.interning.InterningChecker

You can disable this auto-discovery mechanism by passing the -proc:none command-line option to javac, which disables all annotation processing including all pluggable type-checking.

2.2.4 Shorthand for built-in checkers

Ordinarily, javac’s -processor flag requires fully-qualified class names. When running a built-in checker, you may omit the package name and the Checker suffix. The following three commands are equivalent:

javac -processor org.checkerframework.checker.nullness.NullnessChecker MyFile.java javac -processor NullnessChecker MyFile.java javac -processor nullness MyFile.java

This feature also works when multiple checkers are specified. Their names are separated by commas, with no surrounding space. For example:

javac -processor NullnessChecker,RegexChecker MyFile.java javac -processor nullness,regex MyFile.java

This feature does not apply to javac @argfiles.

2.3 What the checker guarantees

A checker guarantees that a particular property holds throughout the code.

For example, the Nullness Checker (Chapter 3) guarantees lack of null pointer exceptions; more precisely, it guarantees that only expressions whose type is a @NonNull type are dereferenced, and that such expressions never evaluate to null. The Interning Checker (Chapter 6) guarantees that every expression whose type is an @Interned type evaluates to an interned value, and thereby it prevents incorrect equality tests.

The guarantee holds only if you run the checker on every part of your program and the checker issues no warnings anywhere in the code. You can also verify just part of your program.

There are some limitations to the guarantee.

A compiler plugin can check only those parts of your program that you run it on. If you compile some parts of your program without running the checker, then there is no guarantee that the entire program satisfies the property being checked. Some examples of un-checked code are: Code compiled without the -processor switch, including any external library supplied as a .class file. Code compiled with the -AskipUses , -AonlyUses , -AskipDefs or -AonlyDefs properties (see Chapter 28). Native methods (because the implementation is not Java code, it cannot be checked). Dynamically generated code, such as generated by Spring or MyBatis. Its bytecode is directly generated and run, not compiled by javac and not visible to the Checker Framework. In each of these cases, any use of the code is checked — for example, a call to a native method must be compatible with any annotations on the native method’s signature. However, the annotations on the un-checked code are trusted; there is no verification that the implementation of the native method satisfies the annotations.

You can suppress warnings, such as via the @SuppressWarnings annotation (Chapter 28). If you do so incorrectly, the checker’s guarantee no longer holds.

annotation (Chapter 28). If you do so incorrectly, the checker’s guarantee no longer holds. The Checker Framework is, by default, unsound in a few places where a conservative analysis would issue too many false positive warnings. These are listed in Section 2.2.2. You can supply a command-line argument to make the Checker Framework sound for each of these cases.

Specific checkers may have other limitations; see their documentation for details.

In order to avoid a flood of unhelpful warnings, many of the checkers avoid issuing the same warning multiple times. For example, in this code:

@Nullable Object x = ...; x.toString(); // warning x.toString(); // no warning

In this case, the second call to toString cannot possibly throw a null pointer warning — x is non-null if control flows to the second statement. In other cases, a checker avoids issuing later warnings with the same cause even when later code in a method might also fail. This does not affect the soundness guarantee, but a user may need to examine more warnings after fixing the first ones identified. (Often, a single fix corrects all the warnings.)

If you find that a checker fails to issue a warning that it should, then please report a bug (see Section 36.2).

2.4 Tips about writing annotations

Section 31.1 gives additional tips that are specific to annotating a third-party library.

2.4.1 Write annotations before you run a checker

Before you run a checker, annotate the code, based on its documentation. Then, run the checker to uncover bugs in the code or the documentation.

Don’t do the opposite, which is to run the checker and then add annotations according to the warnings issued. This approach is less systematic, so you may overlook some annotations. It often leads to confusion and poor results. It leads users to make changes not for any principled reason, but to “make the type-checker happy”, even when the changes are in conflict with the documentation or the code. Also see “Annotations are a specification”, below.

2.4.2 How to get started annotating legacy code

Annotating an entire existing program may seem like a daunting task. But, if you approach it systematically and do a little bit at a time, you will find that it is manageable.

Start small

Start small. Focus on one specific property that matters to you; in other words, run just one checker rather than multiple ones. You may choose a different checker for different programs. Focus on the most mission-critical or error-prone part of your code; don’t try to annotate your whole program at first.

It is easiest to add annotations if you know the code or the code contains documentation; you will find that you spend most of your time understanding the code, and very little time actually writing annotations or running the checker.

When annotating, be systematic; we recommend annotating an entire class at a time (not just some of the methods) so that you don’t lose track of your work or redo work. For example, working class-by-class avoids confusion about whether an unannotated type means you determined that the default is desirable, or it means you didn’t yet examine that type. You may find it helpful to start annotating the leaves of the call tree — that is, start with methods/classes/packages that have few dependencies on other code or, equivalently, start with code that a lot of your other code depends on. For example, within a package annotate supertypes before you annotated classes that extend or implement them. The reason for this rule is that it is easiest to annotate a class if the code it depends on has already been annotated.

Don’t overuse pluggable type-checking. If the regular Java type system can verify a property using Java subclasses, then that is a better choice than pluggable type-checking (see Section 35.1.2).

Annotations are a specification

When you write annotations, you are writing a specification, and you should think about them that way. Start out by understanding the program so that you can write an accurate specification. Sections 2.4.3 and 2.4.4 give more tips about writing specifications.

For each class, read its Javadoc. For instance, if you are adding annotations for the Nullness Checker (Section 3), then you can search the documentation for “null” and then add @Nullable anywhere appropriate. For now, just annotate signatures and fields; there is no need to annotate method bodies. The only reason to even read the method bodies yet is to determine signature annotations for undocumented methods — for example, if the method returns null, you know its return type should be annotated @Nullable, and a parameter that is compared against null may need to be annotated @Nullable.

The specification should state any facts that are relevant to callees. When checking a method, use only the specification, not the implementation, of other methods. (Equivalently, type-checking is “modular” or “intraprocedural”.)

After you have annotated all the signatures, run the checker. Then, fix bugs in code and add/modify annotations as necessary. Don’t get discouraged if you see many type-checker warnings at first. Often, adding just a few missing annotations will eliminate many warnings, and you’ll be surprised how fast the process goes overall.

You may wonder about the effect of adding a given annotation (that is, of changing the specification for a given method or class): how many other specification changes (added annotations) will it require, and will it conflict with other code? It’s best to reason about the desired design, but you can also do an experiment. Suppose you are considering adding an annotation to a method parameter. One approach is to manually examine all callees. A more automated approach is to save the checker output before adding the annotation, and to compare it to the checker output after adding the annotation. This helps you to focus on the specific consequences of your change.

Chapter 31 tells you how to annotate libraries that your code uses. Section 2.4.5 and Chapter 28 tell you what to do when you are unable to eliminate checker warnings by adding annotations.

Write good code

Avoid complex code, which is more error-prone. If you write your code to be simple and clean enough for the type-checker to verify, then it will also be easier for programmers to understand.

Your code should compile cleanly under the regular Java compiler. If you are not willing to write code that type-checks in Java, then there is little point in using an even more powerful, restrictive type system. As a specific example, your code should not use raw types like List; use parameterized types like List<String> instead (Section 26.1.1).

Do not write unnecessary annotations.

Do not annotate local variables unless necessary. The checker infers annotations for local variables (see Section 27.7). Usually, you only need to annotate fields and method signatures. You should add annotations inside method bodies only if the checker is unable to infer the correct annotation (usually on type arguments or array element types, rather than on top-level types).

Do not write annotations that are redundant with defaults. For example, when checking nullness (Chapter 3), the default annotation is @NonNull , in most locations other than some type bounds (Section 27.5.3). When you are starting out, it might seem helpful to write redundant annotations as a reminder, but that’s like when beginning programmers write a comment about every simple piece of code: // The below code increments variable i by adding 1 to it. i++; As you become comfortable with pluggable type-checking, you will find redundant annotations to be distracting clutter, so avoid putting them in your code in the first place.

, in most locations other than some type bounds (Section 27.5.3). When you are starting out, it might seem helpful to write redundant annotations as a reminder, but that’s like when beginning programmers write a comment about every simple piece of code: Avoid writing @SuppressWarnings annotations unless there is no alternative. It is tempting to think that your code is right and the checker’s warnings are false positives. Sometimes they are, but slow down and convince yourself of that before you dismiss them. Section 2.4.5 discusses what to do when a checker issues a warning about your code.

2.4.3 Annotations indicate non-exceptional behavior

You should use annotations to specify normal behavior. The annotations indicate all the values that you want to flow to a reference — not every value that might possibly flow there if your program has a bug.

As an example, the goal of the Nullness Checker is to guarantee that your program does not crash due to a null value. In a method like this:

/** @throws IllegalArgumentException if arg is null */ void m(Object arg) { if (arg == null) { throw new IllegalArgumentException(); } ... }

the program crashes if null is passed. Therefore, the type of arg should be @NonNull Object (which you can write as just Object due to defaulting). The Nullness Checker (Chapter 3) will warn whenever a client passes a value that might cause m to crash. If you wrote the type of the formal parameter as @Nullable Object, the Nullness Checker would permit clients to make calls that lead to a crash.

Many methods are guaranteed to throw an exception if they are passed null as an argument. Examples include

java.lang.Double.valueOf(String) java.lang.String.contains(CharSequence) java.lang.Object.checkNotNull(Object) org.junit.Assert.assertNotNull(Object) com.google.common.base.Preconditions.checkNotNull(Object)

@Nullable (see Section 3.2) might seem like a reasonable annotation for the parameter, for two reasons. First, null is a legal argument with a well-defined semantics: throw an exception. Second, @Nullable describes a possible program execution: it might be possible for null to flow there, if your program has a bug.

However, it is never useful for a programmer to pass null. It is the programmer’s intention that null never flows there. If null does flow there, the program will not continue normally (whether or not it throws a NullPointerException).

Therefore, you should specify such parameters as @NonNull, indicating the intended use of the method. When you specify the parameter as the @NonNull annotation, the checker is able to issue compile-time warnings about possible run-time exceptions, which is its purpose. Specifying the parameter as @Nullable would suppress such warnings, which is undesirable. (Since @NonNull is the default, you don’t have to write anything in the source code to specify the parameter as non-null. You are allowed to write a redundant @NonNull annotation, but it is discouraged.)

If a method can possibly throw an exception because its parameter is null, then that parameter’s type should be @NonNull, which guarantees that the type-checker will issue a warning for every client use that has the potential to cause an exception. Don’t write @Nullable on the parameter just because there exist some executions that don’t necessarily throw an exception.

Another example is the Optional Checker (Chapter 5) and the orElseThrow method. The goal of the Optional Checker is to ensure that the program does not crash due to use of a non-present Optional value. Therefore, the receiver of orElseThrow is annotated as @Present, and the optional Checker issues a warning if the client calls orElseThrow on a @MaybePresent value.

2.4.4 Subclasses must respect superclass annotations

An annotation indicates a guarantee that a client can depend upon. A subclass is not permitted to weaken the contract; for example, if a method accepts null as an argument, then every overriding definition must also accept null. A subclass is permitted to strengthen the contract; for example, if a method does not accept null as an argument, then an overriding definition is permitted to accept null.

As a bad example, consider an erroneous @Nullable annotation in com/google/common/collect/Multiset.java:

101 public interface Multiset<E> extends Collection<E> { ... 122 /** 123 * Adds a number of occurrences of an element to this multiset. ... 129 * @param element the element to add occurrences of; may be {@code null} only 130 * if explicitly allowed by the implementation ... 137 * @throws NullPointerException if {@code element} is null and this 138 * implementation does not permit null elements. Note that if {@code 139 * occurrences} is zero, the implementation may opt to return normally. 140 */ 141 int add(@Nullable E element, int occurrences);

There exist implementations of Multiset that permit null elements, and implementations of Multiset that do not permit null elements. A client with a variable Multiset ms does not know which variety of Multiset ms refers to. However, the @Nullable annotation promises that ms.add(null, 1) is permissible. (Recall from Section 2.4.3 that annotations should indicate normal behavior.)

If parameter element on line 141 were to be annotated, the correct annotation would be @NonNull. Suppose a client has a reference to same Multiset ms. The only way the client can be sure not to throw an exception is to pass only non-null elements to ms.add(). A particular class that implements Multiset could declare add to take a @Nullable parameter. That still satisfies the original contract. It strengthens the contract by promising even more: a client with such a reference can pass any non-null value to add(), and may also pass null.

However, the best annotation for line 141 is no annotation at all. The reason is that each implementation of the Multiset interface should specify its own nullness properties when it specifies the type parameter for Multiset. For example, two clients could be written as

class MyNullPermittingMultiset implements Multiset<@Nullable Object> { ... } class MyNullProhibitingMultiset implements Multiset<@NonNull Object> { ... }

or, more generally, as

class MyNullPermittingMultiset<E extends @Nullable Object> implements Multiset<E> { ... } class MyNullProhibitingMultiset<E extends @NonNull Object> implements Multiset<E> { ... }

Then, the specification is more informative, and the Checker Framework is able to do more precise checking, than if line 141 has an annotation.

It is a pleasant feature of the Checker Framework that in many cases, no annotations at all are needed on type parameters such as E in MultiSet.

2.4.5 What to do if a checker issues a warning about your code

When you run a type-checker on your code, it is likely to issue warnings or errors. Don’t panic! There are three general causes for the warnings:

There is a bug in your code, such as a possible null dereference. Fix your code to prevent that crash.

The annotations are too strong (they are incorrect) or too weak (they are imprecise). Improve the annotations, usually by writing more annotations in order to better express the specification. Only write annotations that accurately describe the intended behavior of the software — don’t write inaccurate annotations just for the purpose of eliminating type-checker warnings. Usually you need to improve the annotations in your source code. Sometimes you need to improve annotations in a library that your program uses (see Chapter 31).

There is a weakness in the type-checker. Your code is safe — it never suffers the error at run time — but the checker cannot prove this fact. If possible, rewrite your code to be simpler for the checker to analyze; this is likely to make it easier for people to understand, too. If that is not possible, suppress the warning (see Chapter 28); be sure to include a code comment explaining how you know the code is correct even though the type-checker cannot deduce that fact. (Do not add an if test that can never fail, just to suppress a warning. Adding a gratuitous if clutters the code and confuses readers, who will assume that every if condition can evaluate to true or false. It can be acceptable to add an if test that throws a descriptive error message.)

For each warning issued by the checker, you need to determine which of the above categories it falls into. Here is an effective methodology to do so. It relies mostly on manual code examination, but you may also find it useful to write test cases for your code or do other kinds of analysis, to verify your reasoning.

Write an explanation of why your code is correct and it never suffers the error at run time. In other words, this is an English proof that the type-checker’s warning is incorrect. Don’t skip any steps in your proof. (For example, don’t write an unsubstantiated claim such as “x is non-null here”; instead, give a justification.) Don’t let your reasoning rely on facts that you do not write down explicitly. For example, remember that calling a method might change the values of object fields; your proof might need to state that certain methods have no side effects. If you cannot write a proof, then there is a bug in your code (you should fix the bug) or your code is too complex for you to understand (you should improve its documentation and/or design). Translate the proof into annotations. Here are some examples. If your proof includes “variable x is never null at run time”, then annotate x ’s type with @NonNull .

is never at run time”, then annotate ’s type with . If your proof includes “method foo always returns a legal regular expression”, then annotate foo ’s return type with @Regex .

always returns a legal regular expression”, then annotate ’s return type with . If your proof includes “if method join ’s first argument is non-null, then join returns a non-null result”, then annotate join ’s first parameter and return type with @PolyNull .

’s first argument is non-null, then returns a non-null result”, then annotate ’s first parameter and return type with . If your proof includes “method processOptions has already been called and it set field tz1 ”, then annotate processOptions ’s declaration with @EnsuresNonNull ("tz1") .

has already been called and it set field ”, then annotate ’s declaration with . If your proof includes “method isEmpty returned false, so its argument must have been non-null”, then annotate isEmpty ’s declaration with @EnsuresNonNullIf (expression="#1",result=false) . All of these are examples of correcting weaknesses in the annotations you wrote. The Checker Framework provides many other powerful annotations; you may be surprised how many proofs you can express in annotations. If you need to annotate a method that is defined in a library that your code uses, see Chapter 31. Don’t omit any parts of your proof. When the Checker Framework analyzes a method, it examines only the specifications (not the implementations) of other methods. If there are complex facts in your proof that cannot be expressed as annotations, then that is a weakness in the type-checker. For example, the Nullness Checker cannot express “in list lst, elements stored at even indices are always non-null, but elements stored at odd elements might be null.” In this case, you have two choices. First, you can suppress the warning (Chapter 28); be sure to write a comment explaining your reasoning for suppressing the warning. You may wish to submit a feature request (Section 36.2) asking for annotations that handle your use case. Second, you can rewrite the code to make the proof simpler; in the above example, it might be better to use a list of pairs rather than a heterogeneous list. At this point, all the steps in your proof have been formalized as annotations. Re-run the checker and repeat the process for any new or remaining warnings. If every step of your proof can be expressed in annotations, but the checker cannot make one of the deductions (it cannot follow one of the steps), then that is a weakness in the type-checker. First, double-check your reasoning. Then, suppress the warning, along with a comment explaining your reasoning (Chapter 28). The comment is an excerpt from your English proof, and the proof guides you to the best place to suppress the warning. Finally, please submit a bug report so that the checker can be improved in the future (Section 36.2).

If you have trouble understanding a Checker Framework warning message, you can search for its text in this manual. Also see Section 36.1.4 and Chapter 36, Troubleshooting. In particular, Section 36.1.4 explains this same methodology in different words.

Chapter 3 Nullness Checker

If the Nullness Checker issues no warnings for a given program, then running that program will never throw a null pointer exception. This guarantee enables a programmer to prevent errors from occurring when a program is run. See Section 3.1 for more details about the guarantee and what is checked.

The most important annotations supported by the Nullness Checker are @NonNull and @Nullable. @NonNull is rarely written, because it is the default. All of the annotations are explained in Section 3.2.

To run the Nullness Checker, supply the -processor org.checkerframework.checker.nullness.NullnessChecker command-line option to javac. For examples, see Section 3.5.

The NullnessChecker is actually an ensemble of three pluggable type-checkers that work together: the Nullness Checker proper (which is the main focus of this chapter), the Initialization Checker (Section 3.8), and the Map Key Checker (Chapter 4). Their type hierarchies are completely independent, but they work together to provide precise nullness checking.

3.1 What the Nullness Checker checks

The checker issues a warning in these cases:

When an expression of non- @NonNull type is dereferenced, because it might cause a null pointer exception. Dereferences occur not only when a field is accessed, but when an array is indexed, an exception is thrown, a lock is taken in a synchronized block, and more. For a complete description of all checks performed by the Nullness Checker, see the Javadoc for NullnessVisitor . When an expression of @NonNull type might become null, because it is a misuse of the type: the null value could flow to a dereference that the checker does not warn about. As a special case of an of @NonNull type becoming null, the checker also warns whenever a field of @NonNull type is not initialized in a constructor.

This example illustrates the programming errors that the checker detects:

@Nullable Object obj; // might be null @NonNull Object nnobj; // never null ... obj.toString() // checker warning: dereference might cause null pointer exception nnobj = obj; // checker warning: nnobj may become null if (nnobj == null) // checker warning: redundant test

Parameter passing and return values are checked analogously to assignments.

The Nullness Checker also checks the correctness, and correct use, of initialization (see Section 3.8) and of map key annotations (see Chapter 4).

The checker performs additional checks if certain -Alint command-line options are provided. (See Section 28.6 for more details about the -Alint command-line option.)

3.1.1 Nullness Checker optional warnings

Options that control soundness: If you supply the -Alint=soundArrayCreationNullness command-line option, then the checker warns if it encounters an array creation with a non-null component type. See Section 3.3.4 for a discussion.

command-line option, then the checker warns if it encounters an array creation with a non-null component type. See Section 3.3.4 for a discussion. If you supply the -Astubs=collection-object-parameters-may-be-null.astub command-line option, then in JDK collection classes, the checker unsoundly permits null as an argument for any key or value formal parameter whose type is Object (instead of the element type). See Section 3.4.2.

command-line option, then in JDK collection classes, the checker unsoundly permits null as an argument for any key or value formal parameter whose type is (instead of the element type). See Section 3.4.2. If you supply the -Alint=trustArrayLenZero command-line option, then the checker will trust @ArrayLen (0) annotations. See Section 3.3.5 for a discussion. Options that warn about poor code style: If you supply the -Alint=redundantNullComparison command-line option, then the checker warns when a null check is performed against a value that is guaranteed to be non-null, as in ("m" == null) . Such a check is unnecessary and might indicate a programmer error or misunderstanding. The lint option is disabled by default because sometimes such checks are part of ordinary defensive programming. Options that enable checking modes: If you supply the -Alint=permitClearProperty command-line option, then the checker permits calls to System.setProperties() and calls to System.clearProperty that might clear one of the built-in properties. By default, the checker forbids calls to those methods, and also special-cases type-checking of calls to System.getProperty() and System.setProperties(). A call to one of these methods can return null in general, but by default the Nullness Checker treats it as returning non-null if the argument is one of the literal strings listed in the documentation of System.getProperties(). To make this behavior sound, the Nullness Checker forbids calls that might clear any built-in property, as described above.

3.2 Nullness annotations

The Nullness Checker uses three separate type hierarchies: one for nullness, one for initialization (Section 3.8), and one for map keys (Chapter 4) The Nullness Checker has four varieties of annotations: nullness type qualifiers, nullness method annotations, initialization type qualifiers, and map key type qualifiers.

3.2.1 Nullness qualifiers

The nullness hierarchy contains these qualifiers:

@Nullable indicates a type that includes the null value. For example, the type Boolean is nullable: a variable of type Boolean always has one of the values TRUE , FALSE , or null . @NonNull indicates a type that does not include the null value. The type boolean is non-null; a variable of type boolean always has one of the values true or false . The type @NonNull Boolean is also non-null: a variable of type @NonNull Boolean always has one of the values TRUE or FALSE — never null . Dereferencing an expression of non-null type can never cause a null pointer exception. The @NonNull annotation is rarely written in a program, because it is the default (see Section 3.3.2). @PolyNull indicates qualifier polymorphism. For a description of @PolyNull , see Section 26.2. @MonotonicNonNull indicates a reference that may be null , but if it ever becomes non- null , then it never becomes null again. This is appropriate for lazily-initialized fields, for field initialization that occurs in a lifecycle method other than the constructor (e.g., an override of android.app.Activity.onCreate ), and other uses. When the variable is read, its type is treated as @Nullable , but when the variable is assigned, its type is treated as @NonNull . Because the Nullness Checker works intraprocedurally (it analyzes one method at a time), when a MonotonicNonNull field is first read within a method, the field cannot be assumed to be non-null. The benefit of MonotonicNonNull over Nullable is its different interaction with type refinement (Section 27.7). After a check of a MonotonicNonNull field, all subsequent accesses within that method can be assumed to be NonNull, even after arbitrary external method calls that have access to the given field. It is permitted to initialize a MonotonicNonNull field to null, but the field may not be assigned to null anywhere else in the program. If you supply the noInitForMonotonicNonNull lint flag (for example, supply -Alint=noInitForMonotonicNonNull on the command line), then @MonotonicNonNull fields are not allowed to have initializers at their declarations. Use of @MonotonicNonNull on a static field is a code smell: it may indicate poor design. You should consider whether it is possible to make the field a member field that is set in the constructor.

Figure 3.1 shows part of the type hierarchy for the Nullness type system. (The annotations exist only at compile time; at run time, Java has no multiple inheritance.)

3.2.2 Nullness method annotations

The Nullness Checker supports several annotations that specify method behavior. These are declaration annotations, not type annotations: they apply to the method itself rather than to some particular type.

@RequiresNonNull indicates a method precondition: The annotated method expects the specified variables to be non-null when the method is invoked. Don’t use this for formal parameters (just annotate their type as @NonNull ). @RequiresNonNull is appropriate for a field that is @Nullable in general, but some method requires the field to be non-null. @EnsuresNonNull @EnsuresNonNullIf indicates a method postcondition. With @EnsuresNonNull , the given expressions are non-null after the method returns; this is useful for a method that initializes a field, for example. With @EnsuresNonNullIf , if the annotated method returns the given boolean value (true or false), then the given expressions are non-null. See Section 3.3.3 and the Javadoc for examples of their use.

3.2.3 Initialization qualifiers

The Nullness Checker invokes an Initialization Checker, whose annotations indicate whether an object is fully initialized — that is, whether all of its fields have been assigned.

Use of these annotations can help you to type-check more code. Figure 3.3 shows its type hierarchy. For details, see Section 3.8.

3.2.4 Map key qualifiers

indicates that a value is a key for a given map — that is, indicates whether map.containsKey(value) would evaluate to true.

This annotation is checked by a Map Key Checker (Chapter 4) that the Nullness Checker invokes. The @KeyFor annotation enables the Nullness Checker to treat calls to Map.get precisely rather than assuming it may always return null. In particular, a call mymap.get(mykey) returns a non-null value if two conditions are satisfied:

mymap ’s values are all non- null ; that is, mymap was declared as Map< KeyType , @NonNull ValueType > . Note that @NonNull is the default type, so it need not be written explicitly. mykey is a key in mymap ; that is, mymap.containsKey(mykey) returns true . You express this fact to the Nullness Checker by declaring mykey as @KeyFor("mymap") KeyType mykey . For a local variable, you generally do not need to write the @KeyFor("mymap") type qualifier, because it can be inferred.

If either of these two conditions is violated, then mymap.get(mykey) has the possibility of returning null.

3.3 Writing nullness annotations

3.3.1 Implicit qualifiers

The Nullness Checker adds implicit qualifiers, reducing the number of annotations that must appear in your code (see Section 27.4). For example, enum types are implicitly non-null, so you never need to write @NonNull MyEnumType.

If you want details about implicitly-added nullness qualifiers, see the implementation of NullnessAnnotatedTypeFactory.

3.3.2 Default annotation

Unannotated references are treated as if they had a default annotation. The standard defaulting rule is CLIMB-to-top, described in Section 27.5.3. Its effect is to default all types to @NonNull, except that @Nullable is used for casts, locals, instanceof, and implicit bounds. A user can choose a different defaulting rule by writing a @DefaultQualifier annotation on a package, class, or method. In the example below, fields are defaulted to @Nullable instead of @NonNull.

@DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.FIELD) class MyClass { Object nullableField = null; @NonNull Object nonNullField = new Object(); }

3.3.3 Conditional nullness

The Nullness Checker supports a form of conditional nullness types, via the @EnsuresNonNullIf method annotations. The annotation on a method declares that some expressions are non-null, if the method returns true (false, respectively).

Consider java.lang.Class. Method Class.getComponentType() may return null, but it is specified to return a non-null value if Class.isArray() is true. You could declare this relationship in the following way (this particular example is already done for you in the annotated JDK that comes with the Checker Framework):

class Class<T> { @EnsuresNonNullIf(expression="getComponentType()", result=true) public native boolean isArray(); public native @Nullable Class<?> getComponentType(); }

A client that checks that a Class reference is indeed that of an array, can then de-reference the result of Class.getComponentType safely without any nullness check. The Checker Framework source code itself uses such a pattern:

if (clazz.isArray()) { // no possible null dereference on the following line TypeMirror componentType = typeFromClass(clazz.getComponentType()); ... }

Another example is Queue.peek and Queue.poll, which return non-null if isEmpty returns false.

The argument to @EnsuresNonNullIf is a Java expression, including method calls (as shown above), method formal parameters, fields, etc.; for details, see Section 27.8. More examples of the use of these annotations appear in the Javadoc for @EnsuresNonNullIf.

3.3.4 Nullness and array initialization

Suppose that you declare an array to contain non-null elements:

Object [] oa = new Object[10];

(recall that Object means the same thing as @NonNull Object). By default, the Nullness Checker unsoundly permits this code.

To make the Nullness Checker conservatively reject code that may leave a non-null value in an array, use the command-line option -Alint=soundArrayCreationNullness. The option is currently disabled because it makes the checker issue many false positive errors.

With the option enabled, you can write your code to create a nullable or lazy-nonnull array, initialize each component, and then assign the result to a non-null array:

@MonotonicNonNull Object [] temp = new @MonotonicNonNull Object[10]; for (int i = 0; i < temp.length; ++i) { temp[i] = new Object(); } @SuppressWarnings("nullness") // temp array is now fully initialized @NonNull Object [] oa = temp;

Note that the checker is currently not powerful enough to ensure that each array component was initialized. Therefore, the last assignment needs to be trusted: that is, a programmer must verify that it is safe, then write a @SuppressWarnings annotation.

3.3.5 Nullness and conversions from collections to arrays

The semantics of Collection.toArray(T[]) cannot be captured by the nullness type system syntax. The nullness type of the returned array depends on the size of the passed parameter. In particular, the returned array component is of type @NonNull if the following conditions hold:

The receiver collection’s type argument (that is, the element type) is @NonNull , and

, and The passed array size is less than or equal to the collection size. The Nullness Checker uses these heuristics to handle the most common cases: the argument has length 0: an empty array initializer, e.g. c.toArray(new String[] {}) , or array creation tree of size 0, e.g. c.toArray(new String[0]) . array creation tree with a collection size() method invocation as argument c.toArray(new String[c.size()]) .



Additionally, when you supply the -Alint=trustArrayLenZero command-line option, a call to Collection.toArray will be estimated to return an array with a non-null component type if the argument is a field access where the field declaration has a @ArrayLen(0) annotation. This trusts the @ArrayLen(0) annotation, but does not verify it. Run the Constant Value Checker (see Chapter 21) to verify that annotation.

Note: The nullness of the returned array doesn’t depend on the passed array nullness. This is a fact about Collection.toArray(T[]), not a limitation of this heuristic.

3.3.6 Run-time checks for nullness

When you perform a run-time check for nullness, such as if (x != null) ..., then the Nullness Checker refines the type of x to @NonNull. The refinement lasts until the end of the scope of the test or until x may be side-effected. For more details, see Section 27.7.

3.3.7 Inference of @NonNull and @Nullable annotations

It can be tedious to write annotations in your code. Tools exist that can automatically infer annotations and insert them in your source code. (This is different than type qualifier refinement for local variables (Section 27.7), which infers a more specific type for local variables and uses them during type-checking but does not insert them in your source code. Type qualifier refinement is always enabled, no matter how annotations on signatures got inserted in your source code.)

Your choice of tool depends on what default annotation (see Section 3.3.2) your code uses. You only need one of these tools.

3.4 Suppressing nullness warnings

When the Nullness Checker reports a warning, it’s best to change the code or its annotations, to eliminate the warning. Alternately, you can suppress the warning, which does not change the code but prevents the Nullness Checker from reporting this particular warning to you.

The Checker Framework supplies several ways to suppress warnings, most notably the @SuppressWarnings("nullness") annotation (see Chapter 28). An example use is

// might return null @Nullable Object getObject(...) { ... } void myMethod() { @SuppressWarnings("nullness") // with argument x, getObject always returns a non-null value @NonNull Object o2 = getObject(x);

The Nullness Checker supports an additional warning suppression string, nullness:generic.argument. Use of @SuppressWarnings("nullness:generic.argument") causes the Nullness Checker to suppress warnings related to misuse of generic type arguments. One use for this key is when a class is declared to take only @NonNull type arguments, but you want to instantiate the class with a @Nullable type argument, as in List<@Nullable Object>.

The Nullness Checker also permits you to use assertions or method calls to suppress warnings; see below.

3.4.1 Suppressing warnings with assertions and method calls

Occasionally, it is inconvenient or verbose to use the @SuppressWarnings annotation. For example, Java does not permit annotations such as @SuppressWarnings to appear on statements. In such cases, you can use the @AssumeAssertion string in an assert message (see Section 28.2).

If you need to suppress a warning within an expression, then sometimes writing an assertion is not convenient. In such a case, you can suppress warnings by writing a call to the NullnessUtil.castNonNull method. The rest of this section discusses the castNonNull method.

The Nullness Checker considers both the return value, and also the argument, to be non-null after the castNonNull method call. The Nullness Checker issues no warnings in any of the following code:

// One way to use castNonNull as a cast: @NonNull String s = castNonNull(possiblyNull1); // Another way to use castNonNull as a cast: castNonNull(possiblyNull2).toString(); // It is possible, but not recommmended, to use castNonNull as a statement: // (It would be better to write an assert statement with @AssumeAssertion // in its message, instead.) castNonNull(possiblyNull3); possiblyNull3.toString();

The castNonNull method throws AssertionError if Java assertions are enabled and the argument is null. However, it is not intended for general defensive programming; see Section 28.2.1.

To use the castNonNull method, the checker-qual.jar file must be on the classpath at run time.

The Nullness Checker introduces a new method, rather than re-using an existing method such as org.junit.Assert.assertNotNull(Object) or com.google.common.base.Preconditions.checkNotNull(Object). Those methods are commonly used for defensive programming, so it is impossible to know the programmer’s intent when writing them. Therefore, it is important to have a method call that is used only for warning suppression. See Section 28.2.1 for a discussion of the distinction between warning suppression and defensive programming.

3.4.2 Null arguments to collection classes

For collection methods with Object formal parameter type, such as contains, indexOf, and remove, the annotated JDK forbids null as an argument.

The reason is that some implementations (like ConcurrentHashMap) throw NullPointerException if null is passed. It would be unsound to permit null, because it could lead to a false negative: the Checker Framework issuing no warning even though a NullPointerException can occur at run time.

However, many other common implementations permit such calls, so some users may wish to sacrifice soundness for a reduced number of false positive warnings. To permit null as an argument to these methods, pass the command-line argument -Astubs=collection-object-parameters-may-be-null.astub.

3.5 Examples

3.5.1 Tiny examples

To try the Nullness Checker on a source file that uses the @NonNull qualifier, use the following command (where javac is the Checker Framework compiler that is distributed with the Checker Framework, see Section 34.5 for details):

javac -processor org.checkerframework.checker.nullness.NullnessChecker docs/examples/NullnessExample.java

Compilation will complete without warnings.

To see the checker warn about incorrect usage of annotations (and therefore the possibility of a null pointer exception at run time), use the following command:

javac -processor org.checkerframework.checker.nullness.NullnessChecker docs/examples/NullnessExampleWithWarnings.java

The compiler will issue two warnings regarding violation of the semantics of @NonNull.

3.5.2 Example annotated source code

Some libraries that are annotated with nullness qualifiers are:

The Nullness Checker itself.

The Java projects in the plume-lib GitHub organization. Type-checking occurs on each build.

The Daikon invariant detector. Run the command make check-nullness .

3.6 Tips for getting started

Here are some tips about getting started using the Nullness Checker on a legacy codebase. For more generic advice (not specific to the Nullness Checker), see Section 2.4.2.

Your goal is to add @Nullable annotations to the types of any variables that can be null. (The default is to assume that a variable is non-null unless it has a @Nullable annotation.) Then, you will run the Nullness Checker. Each of its errors indicates either a possible null pointer exception, or a wrong/missing annotation. When there are no more warnings from the checker, you are done!

We recommend that you start by searching the code for occurrences of null in the following locations; when you find one, write the corresponding annotation:

in Javadoc: add @Nullable annotations to method signatures (parameters and return types).

annotations to method signatures (parameters and return types). return null : add a @Nullable annotation to the return type of the given method.

: add a annotation to the return type of the given method. param == null : when a formal parameter is compared to null , then in most cases you can add a @Nullable annotation to the formal parameter’s type

: when a formal parameter is compared to , then in most cases you can add a annotation to the formal parameter’s type TypeName field = null; : when a field is initialized to null in its declaration, then it needs either a @Nullable or a @MonotonicNonNull annotation. If the field is always set to a non-null value in the constructor, then you can just change the declaration to Type field ; , without an initializer, and write no type annotation (because the default is @NonNull ).

: when a field is initialized to in its declaration, then it needs either a or a annotation. If the field is always set to a non-null value in the constructor, then you can just change the declaration to , without an initializer, and write no type annotation (because the default is ). declarations of contains , containsKey , containsValue , equals , get , indexOf , lastIndexOf , and remove (with Object as the argument type): change the argument type to @Nullable Object ; for remove , also change the return type to @Nullable Object .

You should ignore all other occurrences of null within a method body. In particular, you rarely need to annotate local variables (except their type arguments or array element types).

Only after this step should you run the Nullness Checker. The reason is that it is quicker to search for places to change than to repeatedly run the checker and fix the errors it tells you about, one at a time.

Here are some other tips:

In any file where you write an annotation such as @Nullable , don’t forget to add import org.checkerframework.checker.nullness.qual.*; .

, don’t forget to add . To indicate an array that can be null, write, for example: int @Nullable [] .

By contrast, @Nullable Object [] means a non-null array that contains possibly-null objects.

. By contrast, means a non-null array that contains possibly-null objects. If you know that a particular variable is definitely not null, but the Nullness Checker estimates that the variable might be null, then you can make the Nullness Checker trust your judgment by writing an assertion (see Section 28.2): assert var != null : "@AssumeAssertion(nullness)";

To indicate that a routine returns the same value every time it is called, use @Pure (see Section 27.7.5).

(see Section 27.7.5). To indicate a method precondition (a contract stating the conditions under which a client is allowed to call it), you can use annotations such as @RequiresNonNull (see Section 3.2.2).

3.7 Other tools for nullness checking

The Checker Framework’s nullness annotations are similar to annotations used in other tools. You might prefer to use the Checker Framework because it has a more powerful analysis that can warn you about more null pointer errors in your code. Most of the other tools are bug-finding tools rather than verification tools, since they give up precision, soundness, or both in favor of being fast and easy to use. Also see Section 35.10.1 for a comparison to other tools.

If your code is already annotated with a different nullness annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Nullness Checker, as described in Figure 3.2. If the other annotation is a declaration annotation, it may be moved; see Section 29.1.1.

The Checker Framework may issue more or fewer errors than another tool. This is expected, since each tool uses a different analysis. Remember that the Checker Framework aims at soundness: it aims to never miss a possible null dereference, while at the same time limiting false reports. Also, note FindBugs’s non-standard meaning for @Nullable (Section 3.7.2).

Java permits you to import at most one annotation of a given name. For example, if you use both android.annotation.NonNull and lombok.NonNull in your source code, then you must write at least one of them in fully-qualified form, as @android.annotation.NonNull rather than as @NonNull.

3.7.1 Which tool is right for you?

Different tools are appropriate in different circumstances. Here is a brief comparison with FindBugs, but similar points apply to other tools. (For example, the NullAway and Eradicate tools are more like sound type-checking than FindBugs, but all of those tools accept unsoundness — that is, false negatives or missed warnings — in exchange for analysis speed.)

The Checker Framework has a more powerful nullness analysis; FindBugs misses some real errors. FindBugs requires you to annotate your code, but usually not as thoroughly as the Checker Framework does. Depending on the importance of your code, you may desire: no nullness checking, the cursory checking of FindBugs, or the thorough checking of the Checker Framework. You might even want to ensure that both tools run, for example if your coworkers or some other organization are still using FindBugs. If you know that you will eventually want to use the Checker Framework, there is no point using FindBugs first; it is easier to go straight to using the Checker Framework.

FindBugs can find other errors in addition to nullness errors; here we focus on its nullness checks. Even if you use FindBugs for its other features, you may want to use the Checker Framework for analyses that can be expressed as pluggable type-checking, such as detecting nullness errors.

Regardless of whether you wish to use the FindBugs nullness analysis, you may continue running all of the other FindBugs analyses at the same time as the Checker Framework; there are no interactions among them.

If FindBugs (or any other tool) discovers a nullness error that the Checker Framework does not, please report it to us (see Section 36.2) so that we can enhance the Checker Framework.

3.7.2 Incompatibility note about FindBugs @Nullable

FindBugs has a non-standard definition of @Nullable. FindBugs’s treatment is not documented in its own Javadoc; it is different from the definition of @Nullable in every other tool for nullness analysis; it means the same thing as @NonNull when applied to a formal parameter; and it invariably surprises programmers. Thus, FindBugs’s @Nullable is detrimental rather than useful as documentation. In practice, your best bet is to not rely on FindBugs for nullness analysis, even if you find FindBugs useful for other purposes.

You can skip the rest of this section unless you wish to learn more details.

FindBugs suppresses all warnings at uses of a @Nullable variable. (You have to use @CheckForNull to indicate a nullable variable that FindBugs should check.) For example:

// declare getObject() to possibly return null @Nullable Object getObject() { ... } void myMethod() { @Nullable Object o = getObject(); // FindBugs issues no warning about calling toString on a possibly-null reference! o.toString(); }

The Checker Framework does not emulate this non-standard behavior of FindBugs, even if the code uses FindBugs annotations.

With FindBugs, you annotate a declaration, which suppresses checking at all client uses, even the places that you want to check. It is better to suppress warnings at only the specific client uses where the value is known to be non-null; the Checker Framework supports this, if you write @SuppressWarnings at the client uses. The Checker Framework also supports suppressing checking at all client uses, by writing a @SuppressWarnings annotation at the declaration site. Thus, the Checker Framework supports both use cases, whereas FindBugs supports only one and gives the programmer less flexibility.

In general, the Checker Framework will issue more warnings than FindBugs, and some of them may be about real bugs in your program. See Section 3.4 for information about suppressing nullness warnings.

(FindBugs made a poor choice of names. The choice of names should make a clear distinction between annotations that specify whether a reference is null, and annotations that suppress false warnings. The choice of names should also have been consistent for other tools, and intuitively clear to programmers. The FindBugs choices make the FindBugs annotations less helpful to people, and much less useful for other tools. As a separate issue, the FindBugs analysis is also very imprecise. For type-related analyses, it is best to stay away from the FindBugs nullness annotations, and use a more capable tool like the Checker Framework.)

3.7.3 Relationship to Optional<T>

Many null pointer exceptions occur because the programmer forgets to check whether a reference is null before dereferencing it. Java 8’s Optional<T> class provides a partial solution: a programmer must call the get method to access the value, and the designers of Optional hope that the syntactic occurrence of the get method will remind programmers to first check that the value is present. This is still easy to forget, however.

The Checker Framework contains an Optional Checker (see Chapter 5) that guarantees that programmers use Optional correctly, such as calling isPresent before calling get.

There are some limitations to the utility of Optional, which might lead to you choose to use regular Java references rather than Optional. (For more details, see the article “Nothing is better than the Optional type”.)

It is still possible to call get on a non-present Optional , leading to a NoSuchElementException . In other words, Optional doesn’t solve the underlying problem — it merely converts a NullPointerException into a NoSuchElementException exception, and in either case your code crashes.

on a non-present , leading to a . In other words, doesn’t solve the underlying problem — it merely converts a into a exception, and in either case your code crashes. NullPointerException is still possible in code that uses Optional .

is still possible in code that uses . Optional adds syntactic complexity, making your code longer and harder to read.

adds syntactic complexity, making your code longer and harder to read. Optional adds time and space overhead.

adds time and space overhead. Optional does not address important sources of null pointer exceptions, such as partially-initialized objects and calls to Map.get .

The Nullness Checker does not suffer these limitations. Furthermore, it works with existing code and types, it ensures that you check for null wherever necessary, and it infers when the check for null is not necessary based on previous statements in the method.

Java’s Optional class provides utility routines to reduce clutter when using Optional. The Nullness Checker provides an Opt class that provides all the same methods, but written for regular possibly-null Java references.

3.8 Initialization Checker

The Initialization Checker determines whether an object is initialized or not. For any object that is not fully initialized, the Nullness Checker treats its fields as possibly-null — even fields annotated as @NonNull.

Every object’s fields start out as null. By the time the constructor finishes executing, the @NonNull fields have been set to a different value. Your code can suffer a NullPointerException when using a @NonNull field, if your code uses the field during initialization. The Nullness Checker prevents this problem by warning you anytime that you may be accessing an uninitialized field. This check is useful because it prevents errors in your code. However, the analysis can be confusing to understand. If you wish to disable the initialization checks, see Section 3.8.8.

An object is partially initialized from the time that its constructor starts until its constructor finishes. This is relevant to the Nullness Checker because while the constructor is executing — that is, before initialization completes — a @NonNull field may be observed to be null, until that field is set. In particular, the Nullness Checker issues a warning for code like this:

public class MyClass { private @NonNull Object f; public MyClass(int x) { // Error because constructor contains no assignment to this.f. // By the time the constructor exits, f must be initialized to a non-null value. } public MyClass(int x, int y) { // Error because this.f is accessed before f is initialized. // At the beginning of the constructor's execution, accessing this.f // yields null, even though field f has a non-null type. this.f.toString(); f = new Object(); } public MyClass(int x, int y, int z) { m(); f = new Object(); } public void m() { // Error because this.f is accessed before f is initialized, // even though the access is not in a constructor. // When m is called from the constructor, accessing f yields null, // even though field f has a non-null type. this.f.toString(); }

When a field f is declared with a @NonNull type, then code can depend on the fact that the field is not null. However, this guarantee does not hold for a partially-initialized object.

The Nullness Checker uses three annotations to indicate whether an object is initialized (all its @NonNull fields have been assigned), under initialization (its constructor is currently executing), or its initialization state is unknown.

These distinctions are mostly relevant within the constructor, or for references to this that escape the constructor (say, by being stored in a field or passed to a method before initialization is complete). Use of initialization annotations is rare in most code.

3.8.1 Initialization qualifiers

The initialization hierarchy is shown in Figure 3.3. The initialization hierarchy contains these qualifiers:

@Initialized indicates a type that contains a fully-initialized object. Initialized is the default, so there is little need for a programmer to write this explicitly. @UnknownInitialization indicates a type that may contain a partially-initialized object. In a partially-initialized object, fields that are annotated as @NonNull may be null because the field has not yet been assigned. @UnknownInitialization takes a parameter that is the class the object is definitely initialized up to. For instance, the type @UnknownInitialization(Foo.class) denotes an object in which every fields declared in Foo or its superclasses is initialized, but other fields might not be. Just @UnknownInitialization is equivalent to @UnknownInitialization(Object.class). @UnderInitialization indicates a type that contains a partially-initialized object that is under initialization — that is, its constructor is currently executing. It is otherwise the same as @UnknownInitialization . Within the constructor, this has @UnderInitialization type until all the @NonNull fields have been assigned.

A partially-initialized object (this in a constructor) may be passed to a helper method or stored in a variable; if so, the method receiver, or the field, would have to be annotated as @UnknownInitialization or as @UnderInitialization.

If a reference has @UnknownInitialization or @UnderInitialization type, then all of its @NonNull fields are treated as @MonotonicNonNull: when read, they are treated as being @Nullable, but when written, they are treated as being @NonNull.

The initialization hierarchy is orthogonal to the nullness hierarchy. It is legal for a reference to be @NonNull @UnderInitialization, @Nullable @UnderInitialization, @NonNull @Initialized, or @Nullable @Initialized. The nullness hierarchy tells you about the reference itself: might the reference be null? The initialization hierarchy tells you about the @NonNull fields in the referred-to object: might those fields be temporarily null in contravention of their type annotation? Figure 3.4 contains some examples.

Declarations Expression Expression’s nullness type, or checker error class C { @NonNull Object f; @Nullable Object g; ... } @NonNull @Initialized C a; a @NonNull a.f @NonNull a.g @Nullable @NonNull @UnderInitialization C b; b @NonNull b.f @MonotonicNonNull b.g @Nullable @Nullable @Initialized C c; c @Nullable c.f error: deref of nullable c.g error: deref of nullable @Nullable @UnderInitialization C d; d @Nullable d.f error: deref of nullable d.g error: deref of nullable

3.8.2 How an object becomes initialized

Within the constructor, this starts out with @UnderInitialization type. As soon as all of the @NonNull fields in class C have been initialized, then this is treated as @UnderInitialization(C). This means that this is still being initialized, but all initialization of C’s fields is complete, including all fields of supertypes. Eventually, when all constructors complete, the type is @Initialized.

The Initialization Checker issues an error if the constructor fails to initialize any @NonNull field. This ensures that the object is in a legal (initialized) state by the time that the constructor exits. This is different than Java’s test for definite assignment (see JLS ch.16), which does not apply to fields (except blank final ones, defined in JLS §4.12.4) because fields have a default value of null.

All @NonNull fields must either have a default in the field declaration, or be assigned in the constructor or in a helper method that the constructor calls. If your code initializes (some) fields in a helper method, you will need to annotate the helper method with an annotation such as @EnsuresNonNull({"field1", "field2"}) for all the fields that the helper method assigns.

3.8.3 @UnderInitialization examples

The most common use for the @UnderInitialization annotation is for a helper routine that is called by constructor. For example:

class MyClass { Object field1; Object field2; Object field3; public MyClass(String arg1) { this.field1 = arg1; init_other_fields(); } // A helper routine that initializes all the fields other than field1. @EnsuresNonNull({"field2", "field3"}) private void init_other_fields(@UnderInitialization(Object.class) MyClass this) { field2 = new Object(); field3 = new Object(); } public MyClass(String arg1, String arg2, String arg3) { this.field1 = arg1; this.field2 = arg2; this.field3 = arg3; checkRep(); } // Verify that the representation invariants are satisfied. // Works as long as the MyClass fields are initialized, even if the receiver's // class is a subclass of MyClass and not all of the subclass fields are initialized. private void checkRep(@UnderInitialization(MyClass.class) MyClass this) { ... } }

At the end of the constructor, this is not fully initialized. Rather, it is @UnderInitialization(CurrentClass.class). The reason is that there might be subclasses with uninitialized fields. The following example illustrates this:

class A { @NonNull String a; public A() { a = ""; // Now, all fields of A are initialized. // However, if this constructor is invoked as part of 'new B()', then // the fields of B are not yet initialized. // If we would type 'this' as @Initialized, then the following call is valid: doSomething(); } void doSomething() {} } class B extends A { @NonNull String b; @Override void doSomething() { // Dereferencing 'b' is ok, because 'this' is @Initialized and 'b' @NonNull. // However, when executing 'new B()', this line throws a null-pointer exception. b.toString(); } }

3.8.4 Partial initialization

So far, we have discussed initialization as if it is an all-or-nothing property: an object is non-initialized until initialization completes, and then it is initialized. The full truth is a bit more complex: during the initialization process an object can be partially initialized, and as the object’s superclass constructors complete, its initialization status is updated. The Initialization Checker lets you express such properties when necessary.

Consider a simple example:

class A { Object aField; A() { aField = new Object(); } } class B extends A { Object bField; B() { super(); bField = new Object(); } }

Consider what happens during execution of new B().

B ’s constructor begins to execute. At this point, neither the fields of A nor those of B have been initialized yet. B ’s constructor calls A ’s constructor, which begins to execute. No fields of A nor of B have been initialized yet. A ’s constructor completes. Now, all the fields of A have been initialized, and their invariants (such as that field a is non-null) can be depended on. However, because B ’s constructor has not yet completed executing, the object being constructed is not yet fully initialized. When treated as an A (e.g., if only the A fields are accessed), the object is initialized, but when treated as a B , the object is still non-initialized. B ’s constructor completes. The object is initialized when treated as an A or a B . (And, the object is fully initialized if B ’s constructor was invoked via a new B() . But the type system cannot assume that — there might be a class C extends B { ... } , and B ’s constructor might have been invoked from that.)

At any moment during initialization, the superclasses of a given class can be divided into those that have completed initialization and those that have not yet completed initialization. More precisely, at any moment there is a point in the class hierarchy such that all the classes above that point are fully initialized, and all those below it are not yet initialized. As initialization proceeds, this dividing line between the initialized and uninitialized classes moves down the type hierarchy.

The Nullness Checker lets you indicate where the dividing line is between the initialized and non-initialized classes. The @UnderInitialization(classliteral) indicates the first class that is known to be fully initialized. When you write @UnderInitialization(OtherClass.class) MyClass x;, that means that variable x is initialized for OtherClass and its superclasses, and x is (possibly) uninitialized for MyClass and all subclasses.

The example above lists 4 moments during construction. At those moments, the type of the object being constructed is:

@UnderInitialization B @UnderInitialization A @UnderInitialization(A.class) A @UnderInitialization(B.class) B

3.8.5 Method calls from the constructor

Consider the following incorrect program.

class A { Object aField; A() { aField = new Object(); process(5); // illegal call } public void process(int arg) { ... } }

The call to process() is not legal. process() is declared to be called on a fully-initialized receiver, which is the default if you do not write a different initialization annotation. At the call to process(), all the fields of A have been set, but this is not fully initialized because fields in subclasses of A have not yet been set. The type of this is @UnderInitialization(A.class), meaning that this is partially-initialized, with the A part of initialization done but the initialization of subclasses not yet complete.

The Initialization Checker output indicates this problem:

Client.java:7: error: [method.invocation.invalid] call to process(int) not allowed on the given receiver. process(5); // illegal call ^ found : @UnderInitialization(A.class) A required: @Initialized A

Here is a subclass and client code that crashes with a NullPointerException.

class B extends A { List<Integer> processed; B() { super(); processed = new ArrayList<Integer>(); } @Override public void process(int arg) { super(); processed.add(arg); } } class Client { public static void main(String[] args) { new B(); } }

You can correct the problem in multiple ways.

One solution is to not call methods that can be overridden from the constructor: move the call to process() to after the constructor has completed.

Another solution is to change the declaration of process():

public void process(@UnderInitialization(A.class) A this, int arg) { ... }

If you choose this solution, you will need to rewrite the definition of B.process() so that it is consistent with the declared receiver type.

A non-solution is to prevent subclasses from overriding process() by using final on the method. This doesn’t work because even if process() is not overridden, it might call other methods that are overridden.

As final classes cannot have subclasses, they can be handled more flexibly: once all fields of the final class have been initialized, this is fully initialized.

3.8.6 Initialization of circular data structures

There is one final aspect of the initialization type system to be considered: the rules governing reading and writing to objects that are currently under initialization (both reading from fields of objects under initialization, as well as writing objects under initialization to fields). By default, only fully-initialized objects can be stored in a field of another object. If this was the only option, then it would not be possible to create circular data structures (such as a doubly-linked list) where fields have a @NonNull type. However, the annotation @NotOnlyInitialized can be used to indicate that a field can store objects that are currently under initialization. In this case, the rules for reading and writing to that field become a little bit more interesting, to soundly support circular structures.

The rules for reading from a @NotOnlyInitialized field are summarized in Figure 3.5. Essentially, nothing is known about the initialization status of the value returned unless the receiver was @Initialized.

x.f f is @NonNull f is @Nullable x is @Initialized @Initialized @NonNull @Initialized @Nullable x is @UnderInitialization @UnknownInitialization @Nullable @UnknownInitialization @Nullable x is @UnknownInitialization @UnknownInitialization @Nullable @UnknownInitialization @Nullable

Similarly, Figure 3.6 shows under which conditions an assignment x.f = y is allowed for a @NotOnlyInitialized field f. If the receiver x is @UnderInitialization, then any y can be of any initialization state. If y is known to be fully initialized, then any receiver is allowed. All other assignments are disallowed.

x.f = y y is @Initialized y is @UnderInitialization y is @UnknownInitialization x is @Initi