Eclipse null analysis has @NonNull on generic parameter

719 views Asked by At

I am having trouble with this code...

import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
...
StdSerializer<Object> stdSerializer;

stdSerializer = ToStringSerializer.instance;

Note: The package is annotated with @NonNullByDefault in package-info.java.

On the last line of the code above, ToStringSerializer.instance has the following error...

Null type safety (type annotations): The expression of type 'ToStringSerializer' needs unchecked conversion to conform to 'StdSerializer<@NonNull Object>', corresponding supertype is 'StdSerializer'

The ToStringSerializer.eea file has...

class com/fasterxml/jackson/databind/ser/std/ToStringSerializer

instance
 Lcom/fasterxml/jackson/databind/ser/std/ToStringSerializer;
 L1com/fasterxml/jackson/databind/ser/std/ToStringSerializer;
...

Why does Eclipse 2021-03 (4.19.0 build 20210312-0638) say ToStringSerializer.instance needs to be ...<@NonNull...>? How does Eclipse determine that Object must be @NonNull? How do I fix this?

I discovered this problem on Eclipse 2020-12 and it also happens on Eclipse 2021-03.

1

There are 1 answers

6
rzwitserloot On BEST ANSWER

why does eclipse say ToStringSerializer.instance needs to be ...<@NonNull...>?

Because you said so. Specifically, you said all types are to be presumed as inherently @NonNull, unless they are explicitly marked as @Nullable. That's what @NonNullByDefault does. That means: Everywhere a type appears that has no nullity-indicating annotation slapped on it, are to be presumed to have a @NonNull annotation.

The problem is twofold.

  1. Nullity is a type tag. It is a separate dimension. Generics are invariant (that means: a List<Number> cannot be used when a List<Object> is required; in contrast to just a Number, which can be used when an Object is required. This is because that's how the math works out: Otherwise you could assign a List<Number> to a variable of type List<Object>, then add some non-number to it, but that means you just stuffed a non-number in your list of numbers, whooooops. Combine these two facts and it gets a bit hairy. Let's opt into covariance, which you can do with generics (List<? extends Number> is a subtype of List<? extends Object>. This works because you can't add anything to a List<? extends Object>, whereas you can add anything to a List<Object>.

Here is a little ASCII art picture. @NN means 'nonnull', and @Nul means nullable:


List<? extends @Nul Integer> ➞ List<? extends @Nul Number> ➞ List<? extends @Nul Object>
            ↑                             ↑                        ↑
List<? extends @NN Integer> ➞ List<? extends @NN Number> ➞ List<? extends @NN Object>

In this image, if you have one of these 6 items, then you can pass that item as argument to a method whose argument can be 'reached' with the arrows.

As you can see, you cannot unfold this picture into a single line: It has two dimensions.

It gets even more complicated if you pile in the fact that generics types, themselves, can have their togs modified.

For example, imagine you have this type:

Map<@NonNull String, @NonNull String> map;

Makes sense, right? It's a map that maps strings to strings. It cannot contain null keys. Any key is mapped to a guaranteed non-null string.

The definition of the Map interface is: public interface Map<K, V>, where K and V represent the key and value types. This, in this example, V is bound to @NonNull String.

Let's look at the get method of Map:

public V get(K key);

NB: It's actually 'Object key', but the reason for that is irrelevant to this explanation, and this is easier to follow.

Soo.. get returns a @NN String if invoked on an expression of type Map<@NN String, @NN String>, right?

Wrong!

It can return null, because the get method's implementation does that if the provided key is not in the map!

So, the get method definition needs to be marked down with a type tag modifier. It needs to say: "This method returns V, but modified so that it is @Nul. If it was of unknown nullity state, well, it's known now: It can definitely be null, calling code should check. If it was @NN, that is immaterial; the type returned by this method should be @Nul String. If it was already @Nul, great, no need for modifications.

This gets us to multiple significant realizations about using annotations to track nullity:

  • If a library does not have any nullity annotations, then it becomes incredibly difficult to work with it from code that assumes nullity annotations. Even with generics, you can't get away from this, as methods like map.get modify the tag, but the java core libraries do not have nullity annotations. Eclipse fixes this in the most recent version by letting you add an external file (you can make these files within eclipse with a quickfix) with 'add-on annotations', so that you (or somebody else) can make a template that tells eclipse: "The V in public V get(K key) in type java.util.Map is supposed to be annotated with @Nul'.

  • The full universe of types is considerably more complicated than just 'a type can be nullable, or not nullable'. Just like generics comes in 4 flavors: List, List<Number>, List<? extends Number>, List<? super Number>, so does the nullity type tag on any type. There's legacy, nullable, nonnull, and arbitrary nullity. That last one is required if you want to write generic methods. For the same reason generics needs those 4 modes. No annotation based nullity system in common use on the java ecosystem, except checker framework, has sufficient nullities. Lack of the full range of nullity state means that methods will exist that cannot be properly typed using annotations.

  • The above two facts combine into a simple conclusion: The nullity warnings emitted by annotation-based checkers can be wrong.

In this specific case? It's probably a simple matter, where Jackson's ToStringSerializer has no nullity annotations at all, so eclipse presumes all things are nullable, and thus ToStringSerializer.instance's type is @Nullable StdSerializer<@Nullable Object>. In other words: The field may be null. If it is not null, it is definitely an StdSerializer. What it can serialize, though, is presumably any object, and even null. Whereas your variable type is (due to the @ByDefault effect) a 'actual StdSerializer and not a null reference. Furthermore, said serializer can serialize actual objects. It is a compiler error to even attempt to ask it to serialize a value that could potentially hold null'.

Those two notions are type-wise incompatible. The fix is presumably one of these:

  1. Make your variable type StdSerializer<@Nullable Object>.
  2. Reconfigure eclipse to just silently ignore any null issues stemming from 'legacy' nullity info (as in, missing nullity info), and double check that eclipse realizes that it has no idea about the nullity of the types present in the signatures of the jackson library.
  3. Use that external annotation system to properly 'externally annotate' jackson.
  4. Find somebody who has done that work and published it on the web. Then download that and use it.
  5. Disable the nullity checker system entirely, or reconfigure it to apply only to interactions within the project and not with any external-to-the-project APIs you are using.