map and null values

Peter Levart peter.levart at gmail.com
Thu Jan 3 10:05:00 PST 2013


On 01/03/2013 06:10 PM, Brian Goetz wrote:
> You are correct.  But you want a choice that is not on the menu.
>
> The choices were:
>
> 1.  These are methods from ConcurrentMap.  The ConcurrentMap semantics 
> are hostile to null.  We cannot have these methods both on Map and 
> ConcurrentMap with different semantics, so we cannot put these methods 
> on Map.  End of story.
>
> 2.  We can pull these down to Maps that are willing to be consistent 
> with the null behavior of ConcurrentMap.  It's already a stretch to do 
> this because of atomicity / exactly-once invocation concerns, but we 
> managed to work this out with careful spec'ing (thanks Doug!), and the 
> added benefit of Map users getting these methods is worth the stretch.
>
> We chose (2), which offers enhanced behavior to a broader range of 
> users.  Would you really prefer we had chosen (1)?
>
Hi Brian,

What I'm trying to prove (might be wrong) is that at least the following 
methods: remove(key, value), replace(key, oldValue, newValue),
replace(key, value) & putIfAbsent(key, value) don't have to be different 
semantics for ConcurrentMap vs. for Map. At least in a sense that other 
Map methods inherited by ConcurrentMap are not. They simply specify that 
their behaviour regarding null keys/values is implementation dependent. 
The same can be true for the former methods.

Let's take for example the method ConcurrentMap.putIfAbsent(k, v):

      * If the specified key is not already associated
      * with a value, associate it with the given value.
      * This is equivalent to
      *  <pre> {@code
      * if (!map.containsKey(key))
      *   return map.put(key, value);
      * else
      *   return map.get(key);}</pre>
      *
      * except that the action is performed atomically.
      *
      * @param key key with which the specified value is to be associated
      * @param value value to be associated with the specified key
      * @return the previous value associated with the specified key, or
      *         <tt>null</tt> if there was no mapping for the key.
      *         (A <tt>null</tt> return can also indicate that the map
      *         previously associated <tt>null</tt> with the key,
      *         if the implementation supports null values.)
      * @throws UnsupportedOperationException if the <tt>put</tt> operation
      *         is not supported by this map
      * @throws ClassCastException if the class of the specified key or 
value
      *         prevents it from being stored in this map
      * @throws NullPointerException if the specified key or value is null,
      *         and this map does not permit null keys or values
      * @throws IllegalArgumentException if some property of the 
specified key
      *         or value prevents it from being stored in this map

...even the specification on ConcurrentMap allows implementations that 
allow null values...

But let's take the one that always throws NPE with ConcurrentHashMap for 
null keys and/or values. The ConcurrentMap.replace(k, oldV, newV):

      * Replaces the entry for a key only if currently mapped to a given 
value.
      * This is equivalent to
      *  <pre> {@code
      * if (map.containsKey(key) && map.get(key).equals(oldValue)) {
      *   map.put(key, newValue);
      *   return true;
      * } else
      *   return false;}</pre>
      *
      * except that the action is performed atomically.
      *
      * @param key key with which the specified value is associated
      * @param oldValue value expected to be associated with the 
specified key
      * @param newValue value to be associated with the specified key
      * @return <tt>true</tt> if the value was replaced
      * @throws UnsupportedOperationException if the <tt>put</tt> operation
      *         is not supported by this map
      * @throws ClassCastException if the class of a specified key or value
      *         prevents it from being stored in this map
      * @throws NullPointerException if a specified key or value is null,
      *         and this map does not permit null keys or values
      * @throws IllegalArgumentException if some property of a specified key
      *         or value prevents it from being stored in this map

This one is tricky, because if we take it literally, invoking replace(k, 
oldV, newV) on a map that allows null values and already has a mapping 
{k -> null} should always throw NPE even if all (k, oldV, newV) are 
non-null. So I doubt this was literally meant by the spec. At least the 
@throws NPE section does not mention this situation. (Would have to look 
at 3rd party implementations of ConcurrentMap that allow null values, 
how they manage this situation, since there are no such implementations 
in JDK. But the spec does allow them.)

The same goes for other two: replace(k, v) & remove(k, v);

So these 4 methods are already specified on ConcurrentMap to allow 
implementations that allow null keys/values. Moving them up to Map does 
not have to change this specification in any way.

Regards, Peter

>
>
> On 1/3/2013 11:37 AM, Peter Levart wrote:
>>
>> On 01/03/2013 04:46 PM, Brian Goetz wrote:
>>> Another way of saying this is: this is easy for non-concurrent maps,
>>> but basically impossible for concurrent maps.  Since these methods are
>>> being ported down from ConcurrentMap to Map, bending over backwards to
>>> accomodate uses that won't work anyway in their intended domain is a
>>> complete waste of energy.
>>
>> The methods: remove(key, value), replace(key, oldValue, newValue),
>> replace(key, value) & putIfAbsent(key, value) are perfectly usable also
>> in non-concurrent and non-atomic domain, since they provide an
>> opportunity for optimization in various implementations. Currently there
>> is no way to achieve their semantics on plain Map (like j.u.HashMap)
>> without doing two lookups.
>>
>> That's also true for computeIfAbsent and friends but the benefit is
>> questionable, since another lookup might be faster then the construction
>> of a lambda object.
>>
>> Regards, Peter
>>
>>>
>>> On 1/3/2013 10:40 AM, Doug Lea wrote:
>>>> On 01/03/13 09:57, Peter Levart wrote:
>>>>>
>>>>> And if there is a way to provide a single default
>>>>> implementation in j.u.Map that satisfies both ConcurrentMap
>>>>> implementations that
>>>>> don't allow nulls and plain Map implementations that allow nulls,
>>>>> why not
>>>>> provide it?
>>>>>
>>>>
>>>> Feel free to try. The main constraint is that, to apply to
>>>> possibly-concurrent maps, the  four new function-accepting
>>>> methods must be expressed only in terms of putIfAbsent,
>>>> replace, and/or two-argument remove. While I haven't tried
>>>> to prove this, I believe that all ways of doing it encounter
>>>> the null ambiguity.
>>>>
>>>> -Doug
>>>>
>>>>
>>>>
>>



More information about the lambda-dev mailing list