Search code examples
javanulltypecheckingchecker-framework

Using the java checker framework, why is a NonNull value not accepted into a Nullable value location?


Using the java checker framework, why is a Map NonNull value not accepted into a Map Nullable value location?

With this code invocation:

        schema.validate(
            MapMaker.makeMap(
                new AbstractMap.SimpleEntry<>(
                    "foo",
                    "baz"
                ),
                new AbstractMap.SimpleEntry<>(
                    "bar",
                    2
                )
            ),
            configuration
        );

I get this error:

java: [argument] incompatible argument for parameter arg of validate.
  found   : @Initialized @NonNull Map<@Initialized @NonNull String, @Initialized @NonNull Object>
  required: @Initialized @NonNull Map<@Initialized @NonNull String, @Initialized @Nullable Object>

And the validate method is defined as: public FrozenMap<@Nullable Object> validate(Map<String, @Nullable Object> arg, SchemaConfiguration configuration) throws ValidationException, InvalidTypeException {

Any non null Object is a subset of the set of nullable Objects, so why does this not work? How do I get this to work for non-nullable key input also? Do I need to have a different input method signature for the nullable and the non-nullable input arg?

My arguments passed in to the validate method will be used for reading only. Per the checker framework javadocs, @Covariant may be a solution here: For example, consider Iterator. A client can read elements but not write them, so Iterator<@Nullable String> can be a subtype of Iterator<String> without introducing a hole in the type system. Therefore, its type parameter is annotated with @Covariant. The first type parameter of Map.Entry is also covariant. Another example would be the type parameter of a hypothetical class ImmutableList.

But that only applies to interfaces.


Solution

  • So this is happening because the checker framework sees

    • String
    • @Nullable String as two different classes, where String (@NonNull String) inherits from @Nullable string Like in normal Java, covariance does not apply.

    So the solutions here are to:

    1. use extends to allow covariance like public FrozenMap<@Nullable Object> validate(Map<String, ? extends @Nullable Object> arg, SchemaConfiguration configuration) throws ValidationException, InvalidTypeException {
    2. update the validate method to accept Map<String, ?> This is not great because type checking info is lost from the signature
    3. or write many methods that accept all combinations with/without @Nullable like Map<String, @Nullable Object> arg Map<String, Object> arg
    4. or write an interface that makes a generic parameter for each nullable parameter, and implement it.

    Number 4 example code would look like

    @Covariant(0)
    public interface MapValidator <InType extends @Nullable Object, OutType> {
        OutType validate(Map<String, InType> arg, SchemaConfiguration configuration) throws ValidationException, InvalidTypeException;
    }
    
    public class SomeSchema implements MapValidator<@Nullable Object, FrozenMap<@Nullable Object>> {
        OutType validate(Map<String, @Nullable Object> arg, SchemaConfiguration configuration) throws ValidationException, InvalidTypeException {
            ...
        }
    
    }