2

I was fetching key from a constant map earlier using HashMap.

On passing a NULL key at containsKey(), I used to get FALSE.

To make the code look fancy, I tried java-8 over it. So, instead of HashMap, I started using Map.ofEntries to build my map

Surprisingly, I got Null Pointer Exception when a Null key was passed to containsKey() method

String str = null;

Map<String,String> hashMap = new HashMap<>();
hashMap.put("k1", "v1");
System.out.print(hashMap.containsKey(str)); // This gives false

Map<String,String> ofEntriesMap = Map.ofEntries( Map.entry("k1", "v1")); 
System.out.print(ofEntriesMap.containsKey(str)); // Why this gives Null Pointer Exception ?

I am unable to figure out, why it is behaving differently at Map.ofEntries.

What is the best way to handle this situation ?

Aditya Rewari
  • 2,343
  • 2
  • 23
  • 36
  • `Map` implementations have the choice whether to support null keys and values. This is probably a design flaw, but it's the spec, so if you have just `Map`, you don't know whether it will support them. – chrylis -cautiouslyoptimistic- Jul 29 '21 at 18:11
  • Making the code look fancy is a poor reason for any change, especially to code that already works. Making it *clearer* or *easier to read* would be a different story. – John Bollinger Jul 29 '21 at 18:28

3 Answers3

7

The javadoc of Map says:

Unmodifiable Maps

The Map.of, Map.ofEntries, and Map.copyOf static factory methods provide a convenient way to create unmodifiable maps. The Map instances created by these methods have the following characteristics:

  • They are unmodifiable. Keys and values cannot be added, removed, or updated. Calling any mutator method on the Map will always cause UnsupportedOperationException to be thrown. However, if the contained keys or values are themselves mutable, this may cause the Map to behave inconsistently or its contents to appear to change.
  • They disallow null keys and values. Attempts to create them with null keys or values result in NullPointerException.
  • ...

In contrast, the javadoc of HashMap says:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

...

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • That the map in question does not permit null keys makes it *allowable* for that map's `containsKey()` method to throw an NPE when a null argument is presented, but does not require it to do so. Thus, the situation is a little more nuanced than this answer seems to suggest. – John Bollinger Jul 29 '21 at 18:45
4

instead of HashMap, I started using Map.ofEntries to build my map

Surprisingly, I got Null Pointer Exception when a Null key was passed to containsKey() method

The documentation for java.util.Map says, in part:

Some map implementations have restrictions on the keys and values they may contain. For example, some implementations prohibit null keys and values, and some have restrictions on the types of their keys. Attempting to insert an ineligible key or value throws an unchecked exception, typically NullPointerException or ClassCastException. Attempting to query the presence of an ineligible key or value may throw an exception, or it may simply return false; some implementations will exhibit the former behavior and some will exhibit the latter.

(Emphasis added.)

As @Andreas's answer already observes, the maps created via Map.ofEntries() are of such an implementation. Specifically, they disallow null keys and values. Although it is not documented whether their containsKey() methods exercise the option to throw when presented with a null argument, you need to use them with that possibility in mind.

On the other hand, as Andreas also shows, HashMap is documented to permit null keys and values, so its containsKey() method is expected to complete normally when passed a null argument.

What is the best way to handle this situation ?

You have two main choices:

  • If you want to continue to (directly) use a map created via Map.ofEntries() then you must avoid testing whether it contains null keys. Since you know that it cannot contain null keys, such tests are unnecessary.

  • If you want to rely on being able to test null keys' presence in your map, and especially if you want the option of having null keys or null values in it, then you must not use Map.ofEntries() to create it. You might, however, use Map.ofEntries() to initialize it. For example:

    Map<String, String> myMap = Collections.unmodifiableMap(
        new HashMap<String, String>(
            Map.ofEntries(
                Map.Entry("k1", "v1")
            )
        )
    );
    

    Note also that if you are putting fewer than 11 entries in your map, then Map.of() is a bit tidier than Map.ofEntries(). And, of course, if you don't care whether the map is modifiable then you don't have to put it into that unmodifiable wrapper.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • LOL!! Taking an unmodifiable map, making it modifiable, just to make it unmodifiable again. Sorry, it works for the task at hand (+1), it just struck my funny bone. – Andreas Jul 29 '21 at 19:17
  • 1
    Of course, instead of `Map.ofEntries( Map.entry("k1", "v1") )` you can simply write `Map.of("k1", "v1")` and instead of `Collections.unmodifiableMap( new HashMap( Map.of("k1", "v1") ) )` you can use `Collections.singletonMap("k1", "v1")` – Holger Jul 30 '21 at 11:20
  • Indeed yes, @Holger, as this answer already mentions, albeit without example code. I demonstrate `Map.ofEntries()` because the OP asks specifically about that. And if the OP's real code needs to set up a map with more than ten entries then `Map.of()` is not a viable option there. – John Bollinger Jul 30 '21 at 13:24
2

This is implementation detail of the unmodifiable map, created by Map.ofEntries.

When you're adding null key to HashMap, it calculates hash of null equal to 0.

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

But Map.ofEntries creates ImmutableCollections.Map1 in case when only one pair was provided and ImmutableCollections.MapN otherwise.

Here is implementation of ImmutableCollections.Map1::containsKey

public boolean containsKey(Object o) {
    return o.equals(k0); // implicit nullcheck of o
}

You can see that comment says that NullPointerException is expected behaviour. As for ImmutableCollections.MapN::containsKey it uses explicit null-check.

public boolean containsKey(Object o) {
        Objects.requireNonNull(o);
        return size > 0 && probe(o) >= 0;
}

If you refer Map::containsKey Javadoc, you can see that it's explicitly said that this method may or may not produce NPE.

Returns true if this map contains a mapping for the specified key. More formally, returns true if and only if this map contains a mapping for a key k such that Objects.equals(key, k). (There can be at most one such mapping.)

Params: key – key whose presence in this map is to be tested

Returns: true if this map contains a mapping for the specified key

Throws: ClassCastException – if the key is of an inappropriate type for this map (optional)

NullPointerException – if the specified key is null and this map does not permit null keys (optional)

geobreze
  • 2,274
  • 1
  • 10
  • 15
  • 3
    No, it is not an implementation detail, it is a specification, i.e. it is documented in the javadoc of `Map`. – Andreas Jul 29 '21 at 18:22
  • I would say that `Map`s javadoc allows some ambiguity and it's for every implementation to decide what to do. So, I consider this as an implementation detail – geobreze Jul 29 '21 at 18:27
  • I'm referring only to javadoc of `Map::containsKey`. Javadoc of `Map` says that `Map.of`, `Map.ofEntries`, and `Map.copyOf` are return unmodifiable map, which is required to return NPE, there is no ambiguity for those, but in *general* `Map::containsKey`'s javadoc allows ambiguity. – geobreze Jul 29 '21 at 18:36
  • I do (still) think that calling it an implementation detail is misleading, but upon further review, I find that the fact that the `Map` provided by `Map.ofEntries()` does not allow null keys does not require that its `containsKey()` method throw an NPE when a null argument is presented. It still has the option of returning `false`. Follow the link in the JavaDocs to the meaning of "optional" as it is used there. – John Bollinger Jul 29 '21 at 18:41
  • @JohnBollinger it can be called an implementation detail since method `void test(Map,?> m) { m.containsKey(null); }` may or may not produce NPE depending on *implementation* of `Map`. Javadoc of `containsKey` doesn't say in what cases NPE is produced, so result of this operation is defined by implementation of `Map`. And both NPE and true/false are allowed by method's contract. – geobreze Jul 29 '21 at 18:53
  • 1
    @geobreze, I acknowledge that there is an argument that can be made for calling it an implementation detail. That does not mean that using that description is not misleading. "Implementation detail" suggests (to me) something that is altogether undocumented, which users of the class don't need to know about and should not care about. None of those apply in this case. – John Bollinger Jul 29 '21 at 18:58
  • @JohnBollinger implementation details may be documented too. The question was "why it is behaving differently at Map.ofEntries" and I find an answer "because of different implementation" reasonable. – geobreze Jul 29 '21 at 19:13
  • @geobreze, I, too, find the answer "because of different implementation" reasonable (though rather bare boned by itself). Perhaps changing to that wording would improve this answer, then. – John Bollinger Jul 29 '21 at 19:35
  • @geobreze Interesting code you have put for passing ```NULL to hash() method```. That it is changing ```NULL key to 0```. So would that mean if I then add a ```key as integer 0```, this would replace the NULL key value ??? hmm.. let me try this – Aditya Rewari Jul 30 '21 at 07:38
  • @geobreze I tried.. Null key and 0 Key both entered map without Replace. So they must be at same Bucket coz of same Hash!!! Please validate y supposition – Aditya Rewari Jul 30 '21 at 07:44
  • @AdityaRewari hashmap doesn't use hashcode only. You're right `null` and `0` have the same hashcode of `0` which means that they will be placed in the same bucket. But `null` and `0` are not equal. When not equal objects have the same hash it's called a collision. There are different types of collision resolution, but `java.util.HashMap` uses separate chaining. You can add as many items as you want with a hash equal to 0, they will be placed in the same bucket and this bucked will contain a linked list (or binary tree) of items you've added. More: https://stackoverflow.com/a/43911638/4655217 – geobreze Jul 30 '21 at 08:57