RFR: 8024688: j.u.Map.merge doesn't work as specified if contains key:null pair

Mike Duigou mike.duigou at oracle.com
Tue Oct 8 00:51:49 UTC 2013


On Oct 3 2013, at 22:15 , David Holmes wrote:

> On 4/10/2013 1:35 PM, Mike Duigou wrote:
>> Hello all;
>> 
>> This is a changeset which improves the consistency of several Map.merge implementations for handling of null values.
> 
> It isn't at all clear to me what specification you are using to define the expected behaviour here.

Most of the new Map methods provide only limited support for null values. This is not due to null-hostility but generally a consequence of trying to avoid the additional overhead of disambiguating between value absent and null value. This includes the result of get() and null returns are also used from the functions to provide special behaviour (usually removal). 

The merge() spec is typical in that it promises to treat null and absent values the same. Some might suggest this is a cunning plot by Doug to discourage use of null values. Perhaps. ;-) The utility of being able to use null values as sentinels, whether from get() or the use function is primary reason for this behaviour. Making the behaviour of the Map defaults and core collections "not horrible" and "not astonishing" in the presence null values has been a fun challenge. :-) It's sometimes necessitated providing alternative defaults for ConcurrentMap. I believe that the defaults in Map and ConcurrentMap could further diverge over time if it turns out to be a problem that the current Map defaults don't look aggressively enough for concurrent modification.

> I would have thought that anyone supplying a remapping function needs to be aware of whether the target map supports nulls or not, and that the remapping function should then do the right thing if null is encountered. Instead you are making the decision to bypass the remapping function if you encounter a null.

Bypassing the function is what merge() does when there's no existing values. This behaviour is not specific to my changes. Here's the logic table I used to construct the tests:

current	value	merged	put/rem	returned
=======	=====	======	=======	========
absent	null	n/a	remove	null
absent	newval	n/a	newval	newval
null	null	n/a	remove	null
null	newval	n/a	newval	newval
oldval	null    null	remove	null
oldval	null	result	result	result
oldval	newval	null	remove	null
oldval	newval	result	result	result

Alternatively, compute() offers the more general behaviour of always calling the remapping function though it does not offer the opportunity to directly provide a "newValue" (capture by the lambda is the alternative). 

If I had designed the API I would have had only a single method that provided newValue and unconditionally called the function. This is probably would have been naive--I don't know Doug's reasons behind choosing this behaviour for merge() and compute() though I am certainly curious about why it is done the way it is.

Mike

> 
> David
> 
>> The existing unit tests hadn't considered several cases where the result of the remapper was not the same as the value. I've restructured the merge tests to be more thorough and systematic this revealed a couple of problems.
>> 
>> http://cr.openjdk.java.net/~mduigou/JDK-8024688/0/webrev/
>> 
>> Like several of the previous patches, this one introduces an alternative default for ConcurrentMap to work around issues involving null values where the handling in the general Map default would be incorrect.
>> 
>> Mike
>> 




More information about the core-libs-dev mailing list