blob: a4adaf4f92369ccb45814d29c7d9efc1a7423b9e [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.atlas.lib;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A key {@literal ->} value 'map' which reference counts entries.
*
* <p>
* The same (key,value) pair can be added to the map several times and then
* removed several times. A reference count is incremented for each addition
* and, provided the count is greater than 0, decremented on removal.
* <p>
*
* <p>
* The pair is removed from the map when a remove decrements the reference count to 0.
* </p>
*
* <p>
* This class is thread safe.
* </p>
*
* @param <K>
* @param <T>
*/
public class RefCountingMap<K, T> {
/*
* Uses CountedRef instances which are pairs of an integer and
* and a reference. These instances are immutable - a new instance
* is created on each increment/decrement operation. This could
* result in churn in the garbage collector under heavy use.
*/
protected Map<K, CountedRef<T>> map = new ConcurrentHashMap<>() ;
public RefCountingMap() {}
public boolean contains(K key) { return map.containsKey(key) ; }
public Collection<K> keys() { return map.keySet() ; }
public int size() { return map.size() ; }
public boolean isEmpty() { return map.isEmpty() ; }
/** Clear the map of all keys regardless of reference counts. */
public void clear() { map.clear() ; }
public Set<K> keySet() { return map.keySet(); }
public boolean containsKey(Object key) { return map.containsKey(key); }
/**
* Add a key value pair to the map.
*
* <p>
* if there is no entry in the map for the key, then a key value pair is added
* to the map with a reference count of 1.
* </p>
*
* <p>
* If there is already an entry in the map for the same key and value,
* the reference count for that entry is incremented.
* </p>
*
* <p>
* if there is an entry in the map for the key, but with a different value,
* then that entry is replaced with a new entry for the key and value with
* a reference count of 1.
* </p>
*
* @param key
* @param value
*/
public void add(K key, T value) {
// map.compute is atomic
map.compute(key,
(k, v) -> {
int refCount = 1 ;
if (v != null && ( v.getRef().equals(value) ) ) {
refCount = v.getCount() + 1;
}
return new CountedRef<>(value, refCount );
});
}
/**
* Decrement the reference count for a key, and remove the corresponding entry from the map
* if the result is 0.
*
* <p>
* Do nothing if there is no entry in the map corresponding to the key.
* </p>
* @param key
*/
public void remove(K key) {
// map.compute is atomic
map.compute(key,
(k, v) -> {
if (v == null)
return null ;
int refCount = v.getCount() - 1 ;
if ( refCount == 0 )
return null ;
else
return new CountedRef<>(v.getRef(), refCount);
});
}
/**
* Remove the entry corresponding to the key from the map completely.
*
* <p>
* This method ignores the reference count.
* </p>
*
* @param key
*/
public void removeAll(K key) {
map.remove(key);
}
/**
* Return the reference count for the entry corresponding to a key in the map.
*
* <p>
* Returns 0 if there is no entry in the map corresponding to the key.
* </p>
* @param key
* @return the reference count for the entry corresponding to key in the map,
* or 0 if there is no corresponding entry.
*/
public int refCount(K key) {
CountedRef<T> ref = map.get(key);
if (ref == null) {
return 0 ;
} else {
return ref.getCount();
}
}
/**
* Return the value associated with a key in the map.
*
* @param key
* @return the value associated with the key, or null if there is no such value.
*/
public T get(Object key) {
CountedRef<T> ref = map.get(key);
if ( ref == null ) return null ;
return ref.getRef();
}
/*
* An immutable pair of an integer count and an object reference
*/
class CountedRef<R> {
final int refCount;
final R ref;
CountedRef(R ref, int refCount) {
this.refCount = refCount;
this.ref = ref;
}
int getCount() { return refCount ; }
R getRef() { return ref; }
}
}