Chris Hubick
Chris Hubick

Reputation: 105

Java Map w @NonNull Value Type and @Nullable get() method result?

I have a method returning a custom Map implementation, whose entries consist entirely of non-null keys and values, so I would like to add type annotations in order to indicate that clients may iterate over the Map.Entry's without having to check them for null values: Map<@NonNull String,@NonNull String>

The problem is that the Map.get method API specifies that null be returned for any attempt to retrieve a value for a key which isn't present in the Map, and annotating my get method implementation to return a @Nullable String generates a compiler warning that the return type is then incompatible with the @NonNull return specified by the Map.

I understand that a Map.get API created today might perhaps return a java.util.Optional result or throw a NoSuchElementException, but, being beholden to the existing Collections API, is it possible to remain compliant with the get method specification and also specify that my Map contains only @NonNull values?

Much Thanks.

Upvotes: 3

Views: 4294

Answers (4)

Rosario Mezzatesta
Rosario Mezzatesta

Reputation: 11

ObjectMapper mapper = new ObjectMapper(); 
mapper.setSerializationInclusion(Include.NON_NULL);

When you have this error with null value or key:

c.f.j.c.JsonGenerationException:  
  Null key for a Map not allowed in JSON (use a converting NullKeySerializer?)     
            at c.f.j.d.s.i.FailingSerializer.serialize(FailingSerializer.java:36)

You solve override Serialize method like:

class MySerializer extends StdSerializer<Object> {     
        public MyDtoNullKeySerializer() {
             this(null);}     
             public MySerializer(Class<Object> t) { 
                super(t);     
            }     
      @Override   
      public void serialize(Object nullKey, JsonGenerator jsonGenerator,SerializerProvider unused) 
          throws IOException, JsonProcessingException { 
            jsonGenerator.writeFieldName("");    
        } 
}

For example complete see here: https://lentux-informatica.com/jackson-problema-con-maps-e-valori-null/

Upvotes: 0

Ga&#235;tan Sheridan
Ga&#235;tan Sheridan

Reputation: 165

Here's a simple library that can be used as a work around:

package xxxx;

import java.util.*;

import org.eclipse.jdt.annotation.*;

public class NullUtil
{
    @SuppressWarnings ("null")
    public static <@NonNull K, @NonNull T> @NonNull Optional<@Nullable T> get (@NonNull Map<@NonNull K, @NonNull T> m, @NonNull K key)
    {
        return Optional.ofNullable(m.get(key));
    }

    @SuppressWarnings ("null")
    public static <@NonNull K, @NonNull T> @NonNull T getOrThrow (@NonNull Map<@NonNull K, @NonNull T> m, @NonNull K key)
    {
        return Optional.ofNullable(m.get(key)).orElseThrow(
            () -> new RuntimeException("Map does not contain element '" + key + "'"));
    }

    @SuppressWarnings ("null")
    public static <@NonNull K, @NonNull T> @Nullable T getOrNull (@NonNull     Map<@NonNull K, @NonNull T> m, @NonNull K key)
    {
        return Optional.ofNullable(m.get(key)).orElse(null);
    }
}

And here are some example uses:

package xxxx;

import static org.junit.Assert.*;

import java.util.*;

import org.eclipse.jdt.annotation.NonNull;
import org.junit.*;

public class NullUtilTest
{
    @Before
    public void setup ()
    {
        _m.put("a", "va");
        _m.put("b", "vb");
        _m.put("c", "vc");
    }

    @Test
    @SuppressWarnings ("null")
    public void testOptional ()
    {
        assertTrue(NullUtil.get(_m, "a").isPresent());
        assertFalse(NullUtil.get(_m, "x").isPresent());

        assertEquals("va", NullUtil.get(_m, "a").orElse(null));
        assertEquals(null, NullUtil.get(_m, "x").orElse(null));

    }

    @Test
    public void testOrNull ()
    {
        assertEquals("va", NullUtil.getOrNull(_m, "a"));
        assertEquals(null, NullUtil.getOrNull(_m, "x"));
    }

    @Test
    public void testOrThrow ()
    {
        assertEquals("va", NullUtil.getOrThrow(_m, "a"));

        try
        {
            assertEquals("vx", NullUtil.getOrThrow(_m, "x"));
            fail();
        }
        catch (Exception e)
        {
            assertEquals("Map does not contain element 'x'", e.getMessage());
        }
    }

    private final @NonNull Map<@NonNull String, @NonNull String> _m = new HashMap<>();
}

Upvotes: 0

mernst
mernst

Reputation: 8117

You seem to be referring to Eclipse's implementation of nullity checking. You are right that it does not reason precisely about calls to Map.get.

If more precise reasoning about calls to Map.get is important to you, you might want to consider using a different nullity checker. For example, the Nullness Checker that is built on the Checker Framework handles your case. It treats the return value of Map.get as non-null if the key is in the map and all the elements of the map are non-null.

Upvotes: 1

Sean Van Gorder
Sean Van Gorder

Reputation: 3453

Unfortunately, the Map API is not really compatible with null annotations. Map.get returns the generic type V, even when you define V to be @NonNull, which then violates the API since null must be an allowable return value.

This is a known limitation of null annotations, and will probably only be resolved when nullity profiles for libraries are implemented. Until then, the only workarounds are to check with Map.containsKey before getting a value instead of checking the value afterwards for null, or just avoid using @NonNull on map value types.

Upvotes: 3

Related Questions