The Object Teams Blog

Adding team spirit to your objects.

Help the JDT Compiler helping you! – 3: The essence of null annotations

with 2 comments

After my little excursion to detecting resource leaks let me return to our favorite bug: NPE.

Basic support for null annotations was big news at the release of the Eclipse SDK Juno M4.

To fully understand the approach it’s good to sit back and think of what exactly we’re telling the compiler when we add null annotations to a program.

Annotations as filters

One could certainly use the annotations to mean the following:

@NonNull
“I don’t think this variable will ever hold a null, no need to warn me, when I dereference that value.”
@Nullable
“I guess this variable could be null, so please warn me when I forget a null check before dereferencing.”

Interpreted in this way, the annotations would already be helpful, and actually this would rank them in the same category as @SuppressWarnings("null"): no matter what the compiler analyzed up-to this point, please take my word that this thing is / is not dangerous. In both cases we’d ask the compiler to look at certain things and ignore others, because we “know better”.

However, telling the compiler what’s dangerous and what’s not puts the cart before the horse. If I do so, I will be the weakest link in the chain of analysis. I could err, I do err – that’s why I have NPEs in the first place, so I shouldn’t tell the compiler to trust my judgment.

The good news is: when used appropriately null annotations provide a lot more help.

Annotations as an extension of the type system

The essence of static typing

Lets step back and imagine Java wouldn’t have static typing. A variable declaration would be nothing more than a name, introduced -say- using an imaginary keyword var:

   var o;
   o = new Person();
   o.meow();
 

Right, we could now assign any object, any value, to the variable o and on the other hand we could attempt to invoke any method. Only at runtime will we notice whether the object refered to by o actually has a method meow(). Obviously, this code is unsafe, it could abort saying “Message on understood”. As Java programmers we don’t accept this unsafety, so we use types as a specification:

A typed variable declaration adds a specification with these two sides:

  • It adds a constraint such that only certain values can legally be assigned to the variable
  • It establishes a guarantee that certain operations are well-defined wrt the value of the variable.

A statically typed language forces a decision: what values do I want to allow to be bound to the variable? If I declare o as of type Cat the compiler can conclude that “o = new Person();” violates the constraint and cannot be accepted. If, OTOH, I declared o as of type Person the compiler won’t complain at the assignment but typically it will not find a meow() method in a class Person so that line is now illegal. Only if all things match: the declaration, the assignment, and the usage of a variable, only then will the compiler accept the program and certify that this program will not raise “Message not understood”. It’s a trade: constraint for guarantee.
In a statically typed language we are constrained in what we can say, but we gain the guarantee that a certain kind of error will not occur at runtime.

Sounds fair?

More constraints – more guarantees

Standard static typing constrains the program such that values match to the operations we perform with these values, except – there’s a big loop-hole: in traditional object-oriented type systems each class type contains a value that is not suitable for most operations: the value null, which Tony Hoare called his “billion dollar mistake”.

At a closer look, static type checking in Java is founded on a contradiction:

  • When analyzing an assignment assume that null is a legal value for every object type.
  • When looking at a method call / a field reference assume that null cannot occur (otherwise no unchecked dereference could be considered as legal).

This is exactly, what null annotations fix: they split each traditional object type into two types:

@NonNull Cat
This is the type that contains only cat values
@Nullable Cat
This is the union of the above type and the null-type which contains only one value: null

You can read the type @Nullable Cat as: either a cat or null.

Null warnings vs. type errors

Those users who try the new feature in the JDT may be surprised to see a whole new kind of error messages. While the original goal is to get alerted about potential NPEs, the compiler may now complain with messages like:

Type mismatch: required ‘@NonNull Cat’ but the provided value can be null

The question may arise, why this is reported as an error, even if no NPE can be directly caused at the statement in question. The answer can be deduced from the following analogy:

void foo(Object o, @Nullable Cat c) {
    Cat aCat = o;                 // "Type mismatch: cannot convert from Object to Cat"
    @NonNull Cat reallyACat  = c; // "Type mismatch: required '@NonNull Cat' but the provided value can be null."
}
 

(The wording of the second message will be still improved to better reflect different kinds of RHS values).

The analogy shows:

  • The assignment itself could actually succeed, and even if types don’t match, a language without static typing could actually accept both assignments.
  • If, however, the assignment were accepted, all subsequent analysis of the use of this variable is useless, because the assumption about the variable’s type may be broken.

Therefor, a first step towards making NPE impossible is to be strict about these rules. Assigning a value to a @NonNull variable without being able to prove that the value is not null is illegal. Just as assigning an Object value to a Cat variable without being able to prove that the value is indeed a cat is illegal.

Interestingly, for the first assignment, Java offers a workaround:

    Cat aCat = (Cat) o;
 

Using the cast operator has two implications: we tell the compiler that we “know better”, that o is actually a cat (we do believe so) and secondly, as compiler and JVM cannot fully trust our judgment a check operation will be generated that will raise a ClassCastException if our assumption was wrong.

Can we do something similar for @NonNull conversion? Without the help of JSR 308 we cannot use annotations in a cast, but we can use a little helper:

void foo(Object o, @Nullable Cat c) {
    @NonNull Cat reallyACat  = assertNonNull(c);
}
@NonNull  T assertNonNull(T val) {
    if (val == null) throw new NullPointerException("NonNull assertion violated");
    return val;
}
 

corrected on 2012/03/05

What? We deliberately throw an NPE although the value isn’t even dereferenced? Why that?

The helper mimics exactly what a cast does for normal type conversions: check if the given value conforms to the required type. If not, raise an exception. If the check succeeds re-type the value to the required type.

Here’s an old school alternative:

void foo(@Nullable Cat c) {
    @SuppressWarnings("null") @NonNull Cat reallyACat  = c;
}
 

(Requires that you enable using @SuppressWarnings for optional errors).

Which approach is better? Throwing an exception as soon as something unexpected happens is far better than silencing the warning and waiting for it to explode sometime later at some other location in the code. The difference is felt during debugging. It’s about blame assignment.
If things blow up at runtime, I want to know which part of the code caused the problem. If I use @SuppressWarnings that part is in stealth mode, and an innocent part of the code will get the blame when it uses the wrong-typed value.

Remember, however, that cast and assertNonNull are not the solution, those are workarounds. Solutions must explicitly perform the check and provide application specific behavior to both outcomes of the check. Just as a cast without an instanceof check is still a land-mine, so is the use of the above helper: NPE can still occur. If you need to dereference a variable that’s not @NonNull you should really ask yourself:

  • How can it happen that I end up with a null value in this position?
  • How can the application safely and soundly continue in that situation?

These questions cannot be answered by any tool, these relate to the design of your software.

Help the JDT compiler helping you

This post showed you two things you can and should do to help the compiler helping you:

Add null annotations to resolve the contradiction that’s inherent in Java’s type system: a type can only either contain the value null or not contain the value null. Still Java’s type system opportunistically assumes a little bit of both. With annotations you can resolve the ambiguity and state which of the two possible types you mean.

Second, listen to the new type error messages. They’re fundamental to the analysis. If you disregard (or even disable) these messages there’s no point in letting the analysis apply all its sophisticated machinery. From false assumptions we cannot conclude anything useful.

If you apply these two hints, the compiler will be your friend and report quite some interesting findings. For a project that uses null annotations right from the first line of code written, this advice should be enough. The difficult part is: if you have a large existing code base already, the compiler will have a lot to complain. Think of migrating a fully untyped program to Java. You bet you could use some more help here. Let’s talk about that in future posts.

Advertisements

Written by Stephan Herrmann

February 21, 2012 at 20:39

Posted in Eclipse

Tagged with , , , ,

2 Responses

Subscribe to comments with RSS.

  1. Hi! thanks for this great compiler enhancement!!

    @NonNull T assertNonNull(T val) {
    if (val == null) throw new NullPointerException(“NonNull assertion violated”);
    return val;
    }

    is not valid java syntax.
    has to be

    @NonNull T assertNonNull(T val) {
    if (val == null) throw new NullPointerException(“NonNull assertion violated”);
    return val;
    }

    Markus

    March 5, 2012 at 18:25

  2. Thanks Markus,

    Yes, I broke the rule: never post any code before running it through the compiler. Sorry. But then blog comments are a difficult place to make corrections, so I edited the code in the original post.

    The essence is: currently Java needs the annotation before the type variable, the syntax I originally wrote requires JSR 308 to place the annotation right next to the return type.

    Thanks again for pointing this out.

    stephan

    March 5, 2012 at 20:08


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: