4

I was thinking about an answer to the question: How to test for null keys on any Java map implementation?

My first thought was to check if the Spliterator of the keyset of a map has the characteristic Spliterator.NONNULL:

map.keySet().spliterator().hasCharacteristics(Spliterator.NONNULL)

The JavaDoc says:

Characteristic value signifying that the source guarantees that encountered elements will not be null. (This applies, for example, to most concurrent collections, queues, and maps.)

Before answering I did some checks:

The Spliterator of aTreeMap without a provided Compararator does not have this characteristic even though the natural ordering does not allow null-keys.

new TreeMap<>().keySet().spliterator().hasCharacteristics(Spliterator.NONNULL); // false 

Even more suprising was the fact that Spliterators of an EnumMap keyset and of EnumSet itself do not have this characteristic.

EnumSet.allOf(DayOfWeek.class).spliterator().hasCharacteristics(Spliterator.NONNULL); // false

I understand that the result of spliterator().hasCharacteristics(Spliterator.NONNULL) in the above cases returns false as the default implementation of Set.spliterator() is evaluated.

But is there a reason why the spliterators of these sets do not override Set.spliterator() to create a Spliterator with Spliterator.NONNULL? Would this break a specification I am not aware of?

LuCio
  • 5,055
  • 2
  • 18
  • 34
  • 2
    Although a `TreeMap` relying on its elements' natural ordering cannot accommodate null keys, one based on a suitable `Comparator` can do. Thus, whether null keys are accepted is not a characteristic of the `TreeMap` class itself, and is not reflected in its key set's `Spliterator`. – John Bollinger Sep 11 '18 at 13:45
  • @JohnBollinger Right - therefore I described the case of _natural ordering_. `keySet().spliterator()` of `TreeMap` could check if a comparator is set as the `TreeMap` does in other places. – LuCio Sep 11 '18 at 13:49
  • 2
    It *could do*, @LuCio, but it does not. My point is that this is a property deriving from the `TreeMap` *class*, not an instance-specific property. This is what I would expect anyway, but it is in fact documented. Pursuant to the docs for `Set.spliterator()`, which specify that implementations should report characteristics other than `Spliterator.DISTINCT` that they provide, `TreeMap.keySet()` declares the characteristics that key sets provide, and they do not include `Spliterator.NONNULL`, not even conditionally. Thus, you should not expect these spliterators to have that characteristic. – John Bollinger Sep 11 '18 at 13:57
  • @JohnBollinger Ok ... in case of `TreeMap` that might be quite understandable. And it wasn't that suprising as for `EnumMap`. – LuCio Sep 11 '18 at 14:04
  • @JohnBollinger I am a little bit confused by your last comment. `TreeMap` *could* implement a specific `keySet` method that would return a specific `Set` that reports that `NONNULL`. Are you saying no way this can happen? Sorry, I might just be confused here – Eugene Sep 11 '18 at 14:13
  • @Eugene, I have expanded my comments into an answer, which I hope is clearer on that topic. – John Bollinger Sep 11 '18 at 14:16

2 Answers2

4

Even worse:

System.out.println(Set.of(1)
             .spliterator()
             .hasCharacteristics(Spliterator.NONNULL)); // false

Even if those Set::of methods are documented as:

throws NullPointerException if the element is null

So there is no way to end up with a null in that Set. I guess the real answer is that this is not yet done.

EDIT

See the comment from Stuart Marks about this

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Yes .. _not done yet_ is something I could imagine too. Thus there is the possibility it will be done. – LuCio Sep 11 '18 at 13:39
  • But you can create an `EnumSet` with `EnumSet.copyOf(Collections.singletonList(null))`, it will contain a `null` then... or am I missing something? EDIT: `Set`s Javadoc states "contain no pair of elements [...] and at most one null element`. So why would its spliterator guarantee non-null? – daniu Sep 11 '18 at 13:40
  • He says: "_all of these are implementation specifics and may change from one release to the next,_". Thus this implementation detail too. – LuCio Sep 11 '18 at 13:47
  • @LuCio well, if `Set::of` is documented to throw an exception on nulls, this will not change. my point was that he says this specific `spliterator` methods are not yet done – Eugene Sep 11 '18 at 13:50
  • 4
    …and [as discussed here](https://stackoverflow.com/a/46649315/2711488), the current Stream implementation doesn’t utilize the `NONNULL` characteristic, so the JRE maintainers feel no urge to implement reporting it where feasible. Though, in my opinion, it would be really useful for the `Set.of(…)` variants to report it, to allow simple checking, as `contains(null)` will throw an NPE, so the only alternative is a linear loop (to be done whenever we suspect that a collection *could* be a `Set.of(…)`)… – Holger Sep 11 '18 at 14:52
  • @Holger Thx for the link. It makes clear it's not only about `NONNULL`. There is a greater gap. – LuCio Sep 11 '18 at 15:05
  • 1
    @daniu the contract of `Set` allows implementations supporting `null`, but does not require it. So `EnumSet` is an implementation which does not support `null` and `EnumSet.copyOf(Collections.singletonList(null))` does not even compile, as there is no actual `enum` type to infer. If you provide a type, it will throw a `NullPointerException` at runtime. By the way, since `EnumSet.copyOf` must be able to determine the actual `enum` type at runtime, the source collection must contain at least one actual constant (unless the source itself is an `EnumSet` too). – Holger Sep 12 '18 at 06:35
  • @Holger I see. I did cut the code down (from two lines which compiled) so it would fit into a comment properly, but didn't actually run it. – daniu Sep 12 '18 at 08:07
3

But is there a reason why the spliterators of these sets do not override Set.spliterator() to create a Spliterator with Spliterator.NONNULL?

We can only speculate, but certainly it is the case that some TreeMap instances that use Comparators for ordering do accommodate null keys, and therefore their key sets' spliterators must not have characteristic Spliterator.NONNULL. Although it is true that a TreeMap that uses its keys' natural ordering cannot accommodate null keys, I do not personally find it surprising that TreeMap's key sets do not use that to make a distinction. This is the kind of property that I would expect to be driven exclusively by the classes involved, not by per-instance details.

Would this break a specification I am not aware of?

Perhaps. The docs for Set.spliterator() specify

The Spliterator reports Spliterator.DISTINCT. Implementations should document the reporting of additional characteristic values.

(Emphasis added.) The docs for TreeMap.keySet() say

The set's spliterator is late-binding, fail-fast, and additionally reports Spliterator.SORTED and Spliterator.ORDERED with an encounter order that is ascending key order. The spliterator's comparator (see Spliterator.getComparator()) is null if the tree map's comparator (see SortedMap.comparator()) is null. Otherwise, the spliterator's comparator is the same as or imposes the same total ordering as the tree map's comparator.

Note well that the docs for this set comply with the expectation set in the Set.spliterator() docs without designating, even conditionally, that Spliterator.NONNULL is among the characteristics that key sets' spliterators will report. Note further that those same docs do describe other characteristics of those sets that vary depending on whether the map's order is based on a comparator.

Thus, no, you should not expect TreeMap key sets', spliterators to have report Spliterator.NONNULL under any circumstances. I cannot definitively speak to why this choice was made, but it is consistent with my perception of Java design philosophy.

You also wrote,

Even more suprising was the fact that Spliterators of an EnumMap keyset and of EnumSet itself do not have this characteristic.

I agree that these spliterators could reasonably report Spliterator.NONNULL. I do not know why the choice was made that they would not do so, unless it was simply an oversight. Nevertheless, I observe that their docs indeed do not specify that these spliterators will report Spliterator.NONNULL. With that being the case, it is to be expected that those spliterators will not report that characteristic.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • I understand. But I don't see a possible violation of the specification. The docs of `TreeMap.keySet()` don't mention `Spliterator.NONNULL` and the docs of `EnumMap.keySet()` don't do it too. IMO this doesn't mean, other characterisitcs are excluded. The docs are saying what the code actually does. If other characterisitcs will come, they will have to be documented too. – LuCio Sep 11 '18 at 14:29
  • @LuCio, that undocumented characteristics are excluded is *exactly* how I read the provision of the `Set.spliterator()` docs that I presented. Or if you think "excluded" is too strong, then at minimum I fully expect the spliterators provided by the standard library itself to comply with the "should" clause in the docs of `Set.spliterator()`, so that the absence of any documentation of them reporting `Spliterator.NONNULL` is, for them, a reliable indication that indeed they will not report that characteristic. – John Bollinger Sep 11 '18 at 14:42
  • By _excluded_ I wanted to say they aren't _excluded to come_ regarding @Eugene answer. As of now they are _excluded to be reported_ as they aren't documented and all reported characteristics have to be documented. So you're right: this excludes them. Thus I can't expect `Spliterator.NONNULL` to be set, +1 – LuCio Sep 11 '18 at 14:49