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.
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.
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:
StdSerializer<@Nullable Object>
.