ConcurrentMap.compute is used as delete
As of Java 8, I am implementing a wrapper on an interface java.util.concurrent.ConcurrentMap
, especially the remove(Object key)
. Since I need to do more checks during deletion, I need to implement the functionality using
compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
However, here's the problem: I have to accept Object
, but pass it compute
as K
. With generics I can't even do a runtime check likeif (key instanceof K) ...
I understand why the signature for compute
was chosen this way; if that was easy Object
, in case a computation should create a new record, it can't just use the key. But I'm not sure how to solve this - is there a recommended pattern, but use multiple calls like below?
for (;;) {
V v = map.get();
if (check(k, v)) {
if (map.remove(k, v)) return true;
} else return false;
}
thank
source to share
By looking at the default implementation ConcurrentMap.compute
, you can safely use unchecked cast, assuming yours remappingFunction
can handle Object
non-type K
and return null
. Thus, you can use:
public V remove(Object key) {
@SuppressWarnings("unchecked")
V result = compute((K)key, (k, v) -> {
if(v == null) return null;
...
});
...
}
In implementation it is compute
first used get(key)
(which accepts any object, thus it is safe) and passes the result to remappingFunction
. If the key is of an invalid type, the result will be null
, so yours remappingFunction
should return as well null
. In this case, it will be called containsKey(key)
, which also accepts any object and will return false
for an invalid object, but compute
will return null
.
Note that the behavior is ConcurretMap.compute
well documented (even if there is equivalent code), so it is unlikely that such an implementation will break in the future.
source to share
The first thought that came to my mind was to use it containsKey
to pre-check if the card can handle a particular key object. If this method returns true
, the map can handle it, so an uncontrolled cast to K
will be fine.
But...
ConcurrentMap<String,Integer> map=new ConcurrentSkipListMap<>();
map.put("foo", 42);
map.containsKey(0);
enough to prove that even though a method containsKey
has Object
a parameter type as a parameter, it won't necessarily work on arbitrary argument types - in which case it will issue ClassCastException
.
Nevertheless, even your source cycle using get(Object)
and remove(Object,Object)
not guaranteed to work.
Without an additional type token, there is only one safe way:
return map.entrySet().removeIf(e ->
Objects.equals(key, e.getKey()) && check(e.getKey(), e.getValue()));
but besides performing a linear search, there is a theoretical possibility that it will delete a key more than once if a concurrent insertion of the same key occurs during a traversal.
So the only solution to ensure both atomic and type safety is to use a runtime type marker (usually initialized via a class literal):
ConcurrentMap<K, V> map;
Class<K> keyType;
public boolean remove(Object key) {
if(!keyType.isInstance(key)) return false;
K k=keyType.cast(key);
for(;;) {
V v=map.get(k);
if(v==null || !check(k,v)) return false;
if(map.remove(k, v)) return true;
}
}
Note that you can use something like
if(keyType.isInstance(key))
map.computeIfPresent(keyType.cast(key), (k,v) -> check(k, v)? null: v);
to atomically delete the mapping if it exists and check
succeeds, but you cannot use it to return whether the deletion actually occurred, since the methods computeβ¦
make no difference between missing keys and keys that were deleted as in both cases is returned null
.
source to share