This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 553261974bb73309eb0436c47fe5f3cff12ca41e Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Feb 15 14:57:46 2022 +0100 Implement more optional methods in `WeakHashMap`. --- .../sis/util/collection/WeakValueHashMap.java | 140 ++++++++++++++++++--- .../sis/util/collection/WeakValueHashMapTest.java | 47 +++++++ 2 files changed, 170 insertions(+), 17 deletions(-) diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java index e01b390..617bad1 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java @@ -72,7 +72,7 @@ import static org.apache.sis.util.collection.WeakEntry.*; * then the caller can synchronize on {@code this}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 0.7 + * @version 1.2 * * @param <K> the class of key elements. * @param <V> the class of value elements. @@ -390,13 +390,43 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { } /** - * Implementation of {@link #put(Object, Object)} and {@link #remove(Object)} operations + * Wildcard for {@link #intern(Object, Object, Object)} condition meaning whether a key shall be associated + * to a value or not. Note that {@link #equals(Object)} and {@link #hashCode()} methods are inconsistent in + * this class; the {@code hashCode()} method should never be invoked. + */ + @SuppressWarnings("overrides") + private static final class Wildcard { + static final Wildcard ANY_VALUE = new Wildcard(true); + static final Wildcard NO_VALUE = new Wildcard(false); + + /** Whether the key shall be associated to a value. */ + private final boolean present; + + /** Creates the {@link #ANY_VALUE} or {@link #NO_VALUE} constant. */ + private Wildcard(final boolean present) { + this.present = present; + } + + /** Tests for the {@link #ANY_VALUE} or {@link #NO_VALUE} condition. */ + @Override public boolean equals(Object oldValue) { + return (oldValue != null) == present; + } + } + + /** + * Implementation of {@link #put(Object, Object)}, {@link #putIfAbsent(Object, Object)}, {@link #remove(Object)}, + * {@link #replace(Object, Object)} and {@link #replace(Object, Object, Object)} operations. + * + * @param key key with which the specified value is to be associated. + * @param value value to be associated with the specified key, or {@code null} for removing the entry. + * @param condition previous value that entry must have for doing the action, or {@code null} if no restriction. + * @return the previous value associated with specified key, or {@code null} if there was no mapping for the key. */ @SuppressWarnings("unchecked") - private synchronized V intern(final Object key, final V value, final boolean replace) { + private synchronized V intern(final Object key, final V value, final Object condition) { assert isValid(); /* - * If 'value' is already contained in this WeakValueHashMap, we need to clear it. + * If `value` is already contained in this WeakValueHashMap, we need to clear it. */ V oldValue = null; Entry[] table = this.table; @@ -405,15 +435,20 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { for (Entry e = table[index]; e != null; e = (Entry) e.next) { if (keyEquals(key, e.key)) { oldValue = e.get(); - if (oldValue != null && !replace) { + if (condition != null && !condition.equals(oldValue)) { return oldValue; } e.dispose(); - table = this.table; // May have changed. + table = this.table; // May have changed. index = hash % table.length; } } - if (value != null) { + /* + * If a value has been specified, add it after above removal of previous value except if + * this method is invoked from `replace(key, old)` (condition = `Wildcard.ANY_VALUE`) or + * `replace(key, old, new)` (condition = valid object) and no previous value was mapped. + */ + if (value != null && (condition == null || condition == Wildcard.NO_VALUE || oldValue != null)) { if (++count >= lowerCapacityThreshold(table.length)) { if (count > upperCapacityThreshold(table.length)) { this.table = table = (Entry[]) rehash(table, count, "put"); @@ -433,17 +468,16 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * * @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 specified key, or {@code null} if there was no mapping for key. - * + * @return the previous value associated with specified key, or {@code null} if there was no mapping for the key. * @throws NullArgumentException if the key or the value is {@code null}. */ @Override - public V put(final K key, final V value) throws NullArgumentException { + public V put(final K key, final V value) { if (key == null || value == null) { throw new NullArgumentException(Errors.format(key == null ? Errors.Keys.NullMapKey : Errors.Keys.NullMapValue)); } - return intern(key, value, true); + return intern(key, value, null); } /** @@ -454,30 +488,102 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * * @param key key with which the specified value is to be associated. * @param value value to be associated with the specified key. - * @return the current value associated with specified key, or {@code null} if there was no mapping for key. - * + * @return the current value associated with specified key, or {@code null} if there was no mapping for the key. * @throws NullArgumentException if the key or the value is {@code null}. * * @since 0.7 */ @Override - public V putIfAbsent(final K key, final V value) throws NullArgumentException { + public V putIfAbsent(final K key, final V value) { if (key == null || value == null) { throw new NullArgumentException(Errors.format(key == null ? Errors.Keys.NullMapKey : Errors.Keys.NullMapValue)); } - return intern(key, value, false); + return intern(key, value, Wildcard.NO_VALUE); + } + + /** + * Replaces the entry for the specified key only if it is currently mapped to some value. + * + * @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 specified key, or {@code null} if there was no mapping for the key. + * @throws NullArgumentException if the value is {@code null}. + * + * @since 1.2 + */ + @Override + public V replace(final K key, final V value) { + if (value == null) { + throw new NullArgumentException(Errors.format(Errors.Keys.NullMapValue)); + } + if (key == null) return null; + return intern(key, value, Wildcard.ANY_VALUE); + } + + /** + * Replaces the entry for the specified key only if currently mapped to the specified value. + * + * @param key key with which the specified value is to be associated. + * @param oldValue value expected to be associated with the specified key. + * @param newValue value to be associated with the specified key. + * @return {@code true} if the value was replaced. + * @throws NullArgumentException if the new value is {@code null}. + * + * @since 1.2 + */ + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + if (newValue == null) { + throw new NullArgumentException(Errors.format(Errors.Keys.NullMapValue)); + } + return replaceOrRemove(key, oldValue, newValue); } /** * Removes the mapping for this key from this map if present. * * @param key key whose mapping is to be removed from the map. - * @return previous value associated with specified key, or {@code null} if there was no entry for key. + * @return previous value associated with specified key, or {@code null} if there was no entry for the key. */ @Override public V remove(final Object key) { - return intern(key, null, true); + if (key == null) return null; + return intern(key, null, null); + } + + /** + * Removes the entry for the specified key only if it is currently mapped to the specified value. + * + * @param key key whose mapping is to be removed from the map. + * @param value value expected to be associated with the specified key. + * @return {@code true} if the value was removed. + * + * @since 1.2 + */ + @Override + public boolean remove(final Object key, final Object value) { + return replaceOrRemove(key, value, null); + } + + /** + * Implementation of {@link #replace(Object, Object, Object)} and {@link #remove(Object, Object)}. + * The replace action has a non-null {@code newValue} and the remove action has a null new value. + */ + private boolean replaceOrRemove(final Object key, final Object oldValue, final V newValue) { + if (key == null || oldValue == null) { + return false; + } + @SuppressWarnings("overrides") + final class Observer { + boolean equals; + + @Override public boolean equals(final Object other) { + return equals = oldValue.equals(other); + } + } + final Observer observer = new Observer(); + return intern(key, newValue, observer) != null && observer.equals; } /** diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java b/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java index 2053aad..aac2160 100644 --- a/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java +++ b/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java @@ -23,6 +23,7 @@ import org.apache.sis.test.TestCase; import org.apache.sis.test.DependsOn; import org.apache.sis.test.DependsOnMethod; import org.apache.sis.test.TestConfiguration; +import org.apache.sis.test.TestUtilities; import org.junit.Test; import static org.apache.sis.test.Assert.*; @@ -216,4 +217,50 @@ public final strictfp class WeakValueHashMapTest extends TestCase { assertSame(v2, weakMap.get(k2)); assertSame(v3, weakMap.get(k3)); } + + /** + * Tests {@code putIfAbsent(…)}, {@code replace(…)} and other optional methods. + */ + @Test + public void testOptionalMethods() { + final WeakValueHashMap<Integer,Integer> weakMap = new WeakValueHashMap<>(Integer.class); + final HashMap<Integer,Integer> reference = new HashMap<>(); + final Random random = TestUtilities.createRandomNumberGenerator(); + for (int i=0; i<100; i++) { + final Integer key = random.nextInt(10); + final Integer value = random.nextInt(20); + switch (random.nextInt(7)) { + case 0: { + assertEquals(reference.get(key), weakMap.get(key)); + break; + } + case 1: { + assertEquals(reference.put(key, value), weakMap.put(key, value)); + break; + } + case 2: { + assertEquals(reference.putIfAbsent(key, value), weakMap.putIfAbsent(key, value)); + break; + } + case 3: { + assertEquals(reference.replace(key, value), weakMap.replace(key, value)); + break; + } + case 4: { + final Integer condition = random.nextInt(20); + assertEquals(reference.replace(key, condition, value), weakMap.replace(key, condition, value)); + break; + } + case 5: { + assertEquals(reference.remove(key), weakMap.remove(key)); + break; + } + case 6: { + assertEquals(reference.remove(key, value), weakMap.remove(key, value)); + break; + } + } + } + assertMapEquals(reference, weakMap); + } }