128

With Java 9, new factory methods have been introduced for the List, Set and Map interfaces. These methods allow quickly instantiating a Map object with values in one line. Now, if we consider:

Map<Integer, String> map1 = new HashMap<Integer, String>(Map.of(1, "value1", 2, "value2", 3, "value3"));
map1.put(4, null);

The above is allowed without any exception while if we do:

Map<Integer, String> map2 = Map.of(1, "value1", 2, "value2", 3, "value3", 4, null );

It throws:

Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:221)
..

I am not able to get, why null is not allowed in second case.

I know HashMap can take null as a key as well as a value but why was that restricted in the case of Map.of?

The same thing happens in the case of java.util.Set.of("v1", "v2", null) and java.util.List.of("v1", "v2", null).

Naman
  • 27,789
  • 26
  • 218
  • 353
hi.nitish
  • 2,732
  • 2
  • 14
  • 21
  • 24
    The question is wrong. Should be: why does HashMap allow nulls? – ZhekaKozlov Jul 20 '17 at 09:55
  • 2
    @ZhekaKozlov When we are able to wrap a Map with HashMap, it should allow the features of hashmap or Hashmap should not be allowed to wrap it. Since "of" fn is related to immutability, it should be there in the ConcurrentMap may be which is extended by ConcurrentHashmap or so. – hi.nitish Jul 20 '17 at 10:22
  • 1
    @hi.nitish: It may be time to accept an answer. – Nicolai Parlog Dec 20 '17 at 12:29
  • 7
    prohibiting null keys I can understand but null values seems viable to me – mjj1409 Aug 21 '20 at 19:58
  • 3
    @ZhekaKozlov There is no reason to restrict keys or values to being non-null in a general purpose utility class. It is especially pointless to provide a builder abstraction that does not support what the API supports. – absmiths Apr 22 '21 at 16:30
  • Java usually made very practical decisions on usability and try to be less opinionated so that it can be such a good general purpose language for so many years. This single decision seems very opinionated and seems largely influenced by guava ImmutableMap. That's an unfortunate. Hopefully Java can be less affected by some opinionated library just because they are popular or back by famous company or people. – Jianwu Chen Jul 20 '23 at 06:12
  • In my case, I faced a hard to detected bugs with `Map.copyOf()`, it throws NPE if the Map contains null value. This is a violation of Map contract. As Map.copyOf() is supposed to be a generic method to accept any kinds of Maps. This breaks existing code at runtime. It's hard to detect with test and compiler check. Maybe a more graceful way is to drop those null values instead of blaster with exceptions. Now we have to write ugly code to get around this check. This makes it very less useful. – Jianwu Chen Jul 20 '23 at 17:38

7 Answers7

124

As others pointed out, the Map contract allows rejecting nulls...

[S]ome implementations prohibit null keys and values [...]. Attempting to insert an ineligible key or value throws an unchecked exception, typically NullPointerException or ClassCastException.

... and the collection factories (not just on maps) make use of that.

They disallow null keys and values. Attempts to create them with null keys or values result in NullPointerException.

But why?

Allowing null in collections is by now seen as a design error. This has a variety of reasons. A good one is usability, where the most prominent trouble maker is Map::get. If it returns null, it is unclear whether the key is missing or the value was null. Generally speaking, collections that are guaranteed null free are easier to use. Implementation-wise, they also require less special casing, making the code easier to maintain and more performant.

You can listen to Stuart Marks explain it in this talk but JEP 269 (the one that introduced the factory methods) summarizes it as well:

Null elements, keys, and values will be disallowed. (No recently introduced collections have supported nulls.) In addition, prohibiting nulls offers opportunities for a more compact internal representation, faster access, and fewer special cases.

Since HashMap was already out in the wild when this was slowly discovered, it was too late to change it without breaking existing code but most recent implementations of those interfaces (e.g. ConcurrentHashMap) do not allow null anymore and the new collections for the factory methods are no exception.

(I thought another reason was that explicitly using null values was seen as a likely implementation error but I got that wrong. That was about to duplicate keys, which are illegal as well.)

So disallowing null had some technical reason but it was also done to improve the robustness of the code using the created collections.

Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
  • 5
    well... I can think of case: when you have an Entry in your HashMap that has a certain key and value == null. You do `get`, it returns null. What does the mean? It has a mapping of null or it is not present? Also handling null keys is *always* different, you can't compute hashcode... disallowing it to begin with makes it easier to work – Eugene Jul 20 '17 at 09:30
  • 3
    I wouldn't regard that as a technical but a usability reason. Still, a good point, so I included it. – Nicolai Parlog Jul 20 '17 at 09:38
  • 6
    [JEP 269](https://bugs.openjdk.java.net/browse/JDK-8048330) has this on *null*: "Null elements, keys, and values will be disallowed. (No recently introduced collections have supported nulls.) In addition, prohibiting nulls offers opportunities for a more compact internal representation, faster access, and fewer special cases." – Stefan Zobel Jul 20 '17 at 15:23
  • 1
    @StefanZobel Thanks for the source, just added it. – Nicolai Parlog Jul 21 '17 at 05:00
  • 11
    The only usability reason would be `get` returning `null`. But instead of extending `Map` with a new method that returns an `Optional` this *petty* restriction was put into place which solves absolutely nothing because any code dealing with a `Map` still can't rely on not getting `null` back. In the mean while, people that want to pass a constant `Map` somewhere that happens to allow `null` values with good reason can't make use of this *very* convenient feature. – john16384 Mar 30 '20 at 19:12
  • 1
    Furthermore, the technical reason given about a more compact internal representation is ridiculous. `Map.of` could easily return a different structure depending on contents if this argument about saving a few bytes held any water. – john16384 Mar 30 '20 at 19:16
  • 1
    @john16384 Returning `Optional` wouldn't solve anything because `Optional.ofNullable(null) == Optional.empty()`, i.e. you still couldn't distinguish the two cases. Re "could easily return a different structure depending on contents" you must have missed "[and] faster access, and fewer special cases". I suggest you be more generous with your opinion until you have understood all the trade-offs. – Nicolai Parlog Mar 31 '20 at 07:54
  • 6
    @Nicolai such a method would have a 3rd option, returning `null`, allowing you to distinguish all cases with a single call. My main beef with this ridiculous restriction (and the weak arguments attempting to support it) is that code using this very important basic language feature will be **surprised** at **runtime** when they forget about this ridiculous restriction. – john16384 Mar 31 '20 at 08:10
  • 2
    @john16384 `Optional`-s sole mission is to avoid explicit `null` handling and if your suggestion were put in practice **every** call to an `Optional`-returning method would have to make a `null` check and **still** deal with the empty case. That's a preposterous proposal. Regarding runtime surprises, you are aware that `Map` implementations are [free](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Map.html#put(K,V)) to throw NPEs if you try to `put` a `null` key or value, right? (`ConcurrentHashMap` does that.) How did you cope with that so far? – Nicolai Parlog Apr 05 '20 at 11:30
  • `Map.of` is for constants only because of this decision. I can't guarantee that neither I nor anyone else will ever use a null, so I am stuck with `Collections.singletonMap` or nothing. – Noumenon May 27 '20 at 23:22
  • 1
    This is wrong. Anyone who asserts that a null value or key in a map is always a design flaw is making a grand assertion that is at least sometimes false. Map.get(o) will return null if they key does not exist, and the caller can easily check Map.containsKey(o) to distinguish the difference. – absmiths Apr 22 '21 at 16:32
  • 1
    Some map implementations may reject null, but since this is a builder it could easily select an implementation that does not reject null. The net affect is to require me to roll my own builder which is what I had to do before. – absmiths Apr 22 '21 at 16:34
  • This is _not_ wrong. It _is_ the reason why the collection factories don't allow `null`. You might disagree with the rationale, but that doesn't change the fact that it's the reason. Having to check with `get` and `containsKey` makes code more error-prone, cumbersome, slower, and thread-unsafe - it's a bad solution. Better ones are always available - one of them is not to use `null` as a legal value. – Nicolai Parlog Apr 30 '21 at 09:21
  • The links on the original post are broken, they redirect to an Oracle URL which doesn't exist anymore – fedest Aug 09 '21 at 21:17
  • Thanks @fedest for pointing that out. Fixed it. – Nicolai Parlog Aug 28 '21 at 10:07
  • can't be more than disagree, I always face null exception because Map.of doesn't allow null, it's the case when in theory is one thing but in reality it's a different thing – Next Developer Nov 19 '21 at 14:22
  • lets say I want to use a builder to construct a map of key/values. Sometimes, the value will be null, like when it shouldn't be there. I dont know that though, because values are computed and returned from a function. So its pretty easy to "just add everything" and then do a entryMap stream after the fact to filter null values. This prevents that (imo very nice) idiom. Instead I have to make an empty map, and do a bunch of mutating map.put calls, with null checks. Its certifiably gross. – Kevin Aug 16 '22 at 22:03
  • @Kevin I can't quite follow what exactly the builder's task is. Is it to accept unknown keys and values and then on `build` return a map for them? Or will it build a map and pass it on to the constructor of the object the builder is supposed to create? Either way, do I understand you correctly that if `Map::of` would accept `null` that code would be simpler? If so, what would that look like? (Lots of questions, sorry - just want to make sure I understand you correctly.) – Nicolai Parlog Sep 01 '22 at 11:19
  • Seems to be a problem in some scenarios. For example, how can I create an object to send a PATCH request to delete a field content? – marciel.deg Sep 13 '22 at 13:34
19

In my opinion non-nullability makes sense for keys, but not for values.

  1. The Map interface becomes a weak contract, you can't trust it's behaviour without looking at the implementation, and that is assuming you have access to see it. Java11's Map.of() does not allow null values while HashMap does, but they both implement the same Map contract - how come?
  2. Having a null value or no value at all could not, and arguably should not be considered as a distinct scenario. When you get the value for a key and you get null, who cares if it was explicitly put there or not? there's simply no value for the supplied key, the map does not contain such key, simple as that. Null is null, there's no nullnull or null^2
  3. Existing libraries make extensive use of map as a means to pass properties to them, many of them optional, this makes Map.of() useless as a construct for such structures.
  4. Kotlin enforces nullability at compile time and allows for maps with null values with no known issues.
  5. The reason behind not allowing null values is probably just due to implementation detail and convenience of the creator.
Pedro Borges
  • 1,568
  • 1
  • 14
  • 25
  • I'd agree your opinions. I'm converting a list of name/value pair of arguments with maps. The values are optional (nullable). When I use Map.of(), it surprisingly throws NPE. I have to use a very ugly method to convert null to "" and inside method, treat "" as null. This just create un-necessary in-convenience for developers. This seems smart and safe decision actually make API less usable. It's a opinionated wrong decision. – Jianwu Chen Jul 20 '23 at 00:36
18

Exactly - a HashMap is allowed to store null, not the Map returned by the static factory methods. Not all maps are the same.

Generally as far as I know it has a mistake in the first place to allow nulls in the HashMap as keys, newer collections ban that possibility to start with.

Think of the case when you have an Entry in your HashMap that has a certain key and value == null. You do get, it returns null. What does the mean? It has a mapping of null or it is not present?

Same goes for a Key - hashcode from such a null key has to treated specially all the time. Banning nulls to start with - make this easier.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Yes it might be considered as a fault left in the implementation but so are many other things that do not conform to object oriented principles. However, the developers are using the null frequently in hashmap and they put it in a check of if case to initialise a lot of other things in the business logic. – hi.nitish Jul 20 '17 at 09:42
  • 5
    I like your answer - as it gets to the **why** part of things! – GhostCat Jul 20 '17 at 13:21
  • 2
    This is nonsense. A Java programmer using an API will know how to make it work - or else learn how to. Programmers are not children. Making a breakproof API also makes it less generally useful. – absmiths Apr 22 '21 at 19:59
  • 1
    @absmiths what exactly in nonsense? some decision that I am explaining in this answer, but have not done? I dont get it – Eugene Apr 22 '21 at 21:31
8

While HashMap does allow null values, Map.of does not use a HashMap and throws an exception if one is used either as key or value, as documented:

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

  • ...

  • They disallow null keys and values. Attempts to create them with null keys or values result in NullPointerException.

1615903
  • 32,635
  • 12
  • 70
  • 99
5

The major difference is: when you build your own Map the "option 1" way ... then you are implicitly saying: "I want to have full freedom in what I am doing".

So, when you decide that your map should have a null key or value (maybe for the reasons listed here) then you are free to do so.

But "option 2" is about a convenience thing - probably intended to be used for constants. And the people behind Java simply decided: "when you use these convenience methods, then the resulting map shall be null-free".

Allowing for null values means that

 if (map.contains(key)) 

is not the same as

 if (map.get(key) != null)

which might be a problem sometimes. Or more precisely: it is something to remember when dealing with that very map object.

And just an anecdotal hint why this seems to be a reasonable approach: our team implemented similar convenience methods ourselves. And guess what: without knowing anything about plans about future Java standard methods - our methods do the exact same thing: they return an immutable copy of the incoming data; and they throw up when you provide null elements. We are even so strict that when you pass in empty lists/maps/... we complain as well.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
2

The documentation does not say why null is not allowed:

They disallow null keys and values. Attempts to create them with null keys or values result in NullPointerException.

In my opinion, the Map.of() and Map.ofEntries() static factory methods, which are going to produce a constant, are mostly formed by a developer at the compile type. Then, what is the reason to keep a null as the key or the value?

Whereas, the Map#put is usually used by filling a map at runtime where null keys/values could occur.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 4
    @Eugene if every one had read the documentation (and understood it), this site would have only a fraction of the questions it has now. – user85421 Jul 20 '17 at 09:33
  • @CarlosHeuberger It may be possible that there is a tendency to get rid of the null in future jdks and so the primitives I guess. I read the Map.of() always return an immutable map so does that null have anything to do with immutability here? – hi.nitish Jul 21 '17 at 06:05
  • 3
    @hi.nitish actually it is somewhat related to the immutability: `Map.of()` returns an instance of `ImmutableCollections.MapN` which uses a table having `null` to indicate its end. Doc of that class: "There is a single array "table" that * contains keys and values interleaved ... The table size must be even. It must also be strictly larger than the size (the number of key-value pairs contained in the map) so that **at least one null key is always present**" Why it is done that way, instead of just using an int to hold the size, I don't know; maybe hashing... (the code is a bit *scary*) – user85421 Jul 21 '17 at 07:56
  • 3
    Just because you can't see a reason does not mean there aren't valid use cases. A `Map` with key/value pairs intended for fields in a database for example benefits from null values. I guess it's back to a `NULL` placeholder object for those. IMHO the reason is petty, or simply an overreaction to the anti-null mentality. – john16384 Mar 30 '20 at 19:07
1

Not all Maps allow null as key

The reason mentioned in the docs of Map.

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.

Suresh Atta
  • 120,458
  • 37
  • 198
  • 307