Author: desruisseaux Date: Thu Oct 4 12:47:05 2012 New Revision: 1394018 URL: http://svn.apache.org/viewvc?rev=1394018&view=rev Log: Ported WeakValueHashMap.
Added: sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java (with props) sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java (with props) Modified: sis/trunk/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakHashSetTest.java Added: sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java URL: http://svn.apache.org/viewvc/sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java?rev=1394018&view=auto ============================================================================== --- sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java (added) +++ sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java Thu Oct 4 12:47:05 2012 @@ -0,0 +1,478 @@ +/* + * 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.sis.util.collection; + +import java.util.Map; +import java.util.Set; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Arrays; +import java.lang.reflect.Array; +import java.lang.ref.WeakReference; +import net.jcip.annotations.ThreadSafe; + +import org.apache.sis.util.Debug; +import org.apache.sis.util.Utilities; +import org.apache.sis.util.Workaround; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.NullArgumentException; + +import static org.apache.sis.util.Arrays.resize; +import static org.apache.sis.util.collection.WeakEntry.*; + +// Related to JDK7 +import org.apache.sis.internal.util.Objects; + + +/** + * A hashtable-based map implementation that uses {@linkplain WeakReference weak references}, + * leaving memory when an entry is not used anymore. An entry in a {@code WeakValueHashMap} + * will automatically be removed when its value is no longer in ordinary use. This class is + * similar to the standard {@link java.util.WeakHashMap} class provided in J2SE, except that + * weak references are hold on values instead of keys. + * <p> + * This class is convenient for avoiding the creation of duplicated elements, as in the + * example below: + * + * {@preformat java + * K key = ... + * V value; + * synchronized (map) { + * value = map.get(key); + * if (value != null) { + * value = ...; // Create the value here. + * map.put(key, value); + * } + * } + * } + * + * The calculation of a new value should be fast, because it is performed inside a synchronized + * statement blocking all other access to the map. This is okay if that particular map instance + * is not expected to be used in a highly concurrent environment. + * <p> + * Note that this class is <strong>not</strong> a cache, because the entries are discarded + * as soon as the garbage collector determines that they are no longer in use. If caching + * service are wanted, or if concurrency are wanted, consider using {@link Cache} instead. + * + * @param <K> The class of key elements. + * @param <V> The class of value elements. + * + * @author Martin Desruisseaux (IRD, Geomatys) + * @since 0.3 (derived from geotk-2.0) + * @version 0.3 + * @module + * + * @see java.util.WeakHashMap + * @see WeakHashSet + * @see Cache + */ +@ThreadSafe +public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { + /** + * An entry in the {@link WeakValueHashMap}. This is a weak reference + * to a value together with a strong reference to a key. + */ + private final class Entry extends WeakEntry<V> implements Map.Entry<K,V> { + /** + * The key. + */ + final K key; + + /** + * Constructs a new weak reference. + */ + Entry(final K key, final V value, final Entry next, final int hash) { + super(value, next, hash); + this.key = key; + this.next = next; + } + + /** + * Returns the key corresponding to this entry. + */ + @Override + public K getKey() { + return key; + } + + /** + * Returns the value corresponding to this entry. + */ + @Override + public V getValue() { + return get(); + } + + /** + * Replaces the value corresponding in this entry with the specified value. + * This method can be used only for setting the value to {@code null}. + */ + @Override + public V setValue(final V value) { + if (value != null) { + throw new UnsupportedOperationException(); + } + final V old = get(); + dispose(); + return old; + } + + /** + * Invoked by {@link org.apache.sis.internal.util.ReferenceQueueConsumer} + * for removing the reference from the enclosing collection. + */ + @Override + public void dispose() { + clear(); + removeEntry(this); + } + + /** + * Compares the specified object with this entry for equality. + */ + @Override + public boolean equals(final Object other) { + if (other instanceof Map.Entry<?,?>) { + final Map.Entry<?,?> that = (Map.Entry<?,?>) other; + return equal(key, that.getKey()) && Objects.equals(get(), that.getValue()); + } + return false; + } + + /** + * Returns the hash code value for this map entry. <strong>This hash code + * is not stable</strong>, since it will change after GC collect the value. + */ + @Override + public int hashCode() { + int code = hash(key); + final V val = get(); + if (val != null) { + code ^= val.hashCode(); + } + return code; + } + } + + /** + * Table of weak references. + */ + private Entry[] table; + + /** + * Number of non-null elements in {@link #table}. + */ + private int count; + + /** + * The type of the keys in this map. + */ + private final Class<K> keyType; + + /** + * {@code true} if the keys in this map may be arrays. If the keys can not be + * arrays, then we can avoid the calls to the costly {@link Utilities} methods. + */ + private final boolean mayContainArrays; + + /** + * The set of entries, created only when first needed. + */ + private transient Set<Map.Entry<K,V>> entrySet; + + /** + * Creates a {@code WeakValueHashMap} for keys of the specified type. + * + * @param <K> The type of keys in the map. + * @param <V> The type of values in the map. + * @param keyType The type of keys in the map. + * @return An initially empty map for keys of the given type. + */ + public static <K,V> WeakValueHashMap<K,V> newInstance(final Class<K> keyType) { + return new WeakValueHashMap<K,V>(keyType); + } + + /** + * Creates a new {@code WeakValueHashMap}. + * + * @param keyType The type of keys in the map. + */ + protected WeakValueHashMap(final Class<K> keyType) { + this.keyType = keyType; + mayContainArrays = keyType.isArray() || keyType.equals(Object.class); + /* + * Workaround for the "generic array creation" compiler error. + * Otherwise we would use the commented-out line instead. + */ + @SuppressWarnings("unchecked") + @Workaround(library="JDK", version="1.7") + final Entry[] table = (Entry[]) Array.newInstance(Entry.class, MIN_CAPACITY); +// table = new Entry[size]; + this.table = table; + } + + /** + * Invoked by {@link Entry} when an element has been collected by the garbage + * collector. This method removes the weak reference from the {@link #table}. + */ + @SuppressWarnings("unchecked") + private synchronized void removeEntry(final Entry toRemove) { + assert isValid(); + final int capacity = table.length; + if (toRemove.removeFrom(table, toRemove.hash % capacity)) { + count--; + assert isValid(); + if (count < lowerCapacityThreshold(capacity)) { + table = (Entry[]) WeakEntry.rehash(table, count, "remove"); + assert isValid(); + } + } + } + + /** + * Checks if this {@code WeakValueHashMap} is valid. This method counts the number of elements + * and compares it to {@link #count}. If the check fails, the number of elements is corrected + * (if we didn't, an {@link AssertionError} would be thrown for every operations after the first + * error, which make debugging more difficult). The set is otherwise unchanged, which should + * help to get similar behavior as if assertions hasn't been turned on. + */ + @Debug + final boolean isValid() { + assert Thread.holdsLock(this); + assert count <= upperCapacityThreshold(table.length); + final int n = count(table); + if (n != count) { + count = n; + return false; + } else { + return true; + } + } + + /** + * Returns the number of key-value mappings in this map. + * + * @return The number of entries in this map. + */ + @Override + public synchronized int size() { + assert isValid(); + return count; + } + + /** + * Returns the hash code value for the given key. + */ + final int hash(final Object key) { + return mayContainArrays ? Utilities.deepHashCode(key) : key.hashCode(); + } + + /** + * Returns {@code true} if the two given keys are equal. + */ + final boolean equal(final Object k1, final Object k2) { + return mayContainArrays ? Objects.deepEquals(k1, k2) : k1.equals(k2); + } + + /** + * Returns {@code true} if this map contains a mapping for the specified key. + * Null keys are considered never present. + * + * @param key key whose presence in this map is to be tested. + * @return {@code true} if this map contains a mapping for the specified key. + */ + @Override + public boolean containsKey(final Object key) { + return get(key) != null; + } + + /** + * Returns {@code true} if this map maps one or more keys to this value. + * Null values are considered never present. + * + * @param value value whose presence in this map is to be tested. + * @return {@code true} if this map maps one or more keys to this value. + */ + @Override + public synchronized boolean containsValue(final Object value) { + return super.containsValue(value); + } + + /** + * Returns the value to which this map maps the specified key. + * Returns {@code null} if the map contains no mapping for this key. + * Null keys are considered never present. + * + * @param key Key whose associated value is to be returned. + * @return The value to which this map maps the specified key. + */ + @Override + @SuppressWarnings("unchecked") + public synchronized V get(final Object key) { + assert isValid(); + if (key != null) { + final Entry[] table = this.table; + final int index = (hash(key) & HASH_MASK) % table.length; + for (Entry e = table[index]; e != null; e = (Entry) e.next) { + if (equal(key, e.key)) { + return e.get(); + } + } + } + return null; + } + + /** + * Implementation of {@link #put(Object, Object)} and {@link #remove(Object)} operations. + */ + @SuppressWarnings("unchecked") + private synchronized V intern(final Object key, final V value) { + assert isValid(); + /* + * If 'obj' is already contained in this WeakValueHashMap, we need to clear it. + */ + V oldValue = null; + Entry[] table = this.table; + final int hash = hash(key) & HASH_MASK; + int index = hash % table.length; + for (Entry e = table[index]; e != null; e = (Entry) e.next) { + if (equal(key, e.key)) { + oldValue = e.get(); + e.dispose(); + table = this.table; // May have changed. + index = hash % table.length; + } + } + if (value != null) { + if (count >= upperCapacityThreshold(table.length)) { + this.table = table = (Entry[]) rehash(table, count, "put"); + index = hash % table.length; + } + table[index] = new Entry(keyType.cast(key), value, table[index], hash); + count++; + } + assert isValid(); + return oldValue; + } + + /** + * Associates the specified value with the specified key in this map. + * The value is associated using a {@link WeakReference}. + * + * @param key key with which the specified value is to be associated. + * @param value value to be associated with the specified key. + * @return previous value associated with specified key, or {@code null} + * if there was no mapping for key. + * + * @throws NullArgumentException if the key or the value is {@code null}. + */ + @Override + public V put(final K key, final V value) throws NullArgumentException { + ArgumentChecks.ensureNonNull("key", key); + ArgumentChecks.ensureNonNull("value", value); + return intern(key, value); + } + + /** + * 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. + */ + @Override + public V remove(final Object key) { + return intern(key, null); + } + + /** + * Removes all of the elements from this map. + */ + @Override + public synchronized void clear() { + Arrays.fill(table, null); + count = 0; + } + + /** + * Returns a set view of the mappings contained in this map. + * Each element in this set is a {@link java.util.Map.Entry}. + * + * @return a set view of the mappings contained in this map. + */ + @Override + public synchronized Set<Map.Entry<K,V>> entrySet() { + if (entrySet == null) { + entrySet = new EntrySet(); + } + return entrySet; + } + + /** + * The set of entries. + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.3 (derived from geotk-3.13) + * @version 0.3 + * @module + */ + private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { + /** + * Returns the number of entries in the map. + */ + @Override + public int size() { + return WeakValueHashMap.this.size(); + } + + /** + * Returns a view of this set as an array. Note that this array contains strong references. + * Consequently, no object reclamation will occur as long as a reference to this array is + * hold. + */ + @Override + @SuppressWarnings("unchecked") + public Map.Entry<K,V>[] toArray() { + synchronized (WeakValueHashMap.this) { + assert isValid(); + @SuppressWarnings({"unchecked","rawtypes"}) + final Map.Entry<K,V>[] elements = new Map.Entry[size()]; + int index = 0; + final Entry[] table = WeakValueHashMap.this.table; + for (int i=0; i<table.length; i++) { + for (Entry el=table[i]; el!=null; el=(Entry) el.next) { + final Map.Entry<K,V> entry = new SimpleEntry<K,V>(el); + if (entry.getValue() != null) { + elements[index++] = entry; + } + } + } + return resize(elements, index); + } + } + + /** + * Returns an iterator over the elements contained in this collection. No element from + * this set will be garbage collected as long as a reference to the iterator is hold. + */ + @Override + public Iterator<Map.Entry<K, V>> iterator() { + return Arrays.asList(toArray()).iterator(); + } + } +} Propchange: sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Modified: sis/trunk/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java URL: http://svn.apache.org/viewvc/sis/trunk/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java?rev=1394018&r1=1394017&r2=1394018&view=diff ============================================================================== --- sis/trunk/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java (original) +++ sis/trunk/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java Thu Oct 4 12:47:05 2012 @@ -45,7 +45,8 @@ import org.junit.runners.Suite; org.apache.sis.util.type.SimpleInternationalStringTest.class, org.apache.sis.util.type.DefaultInternationalStringTest.class, org.apache.sis.internal.util.ReferenceQueueConsumerTest.class, - org.apache.sis.util.collection.WeakHashSetTest.class + org.apache.sis.util.collection.WeakHashSetTest.class, + org.apache.sis.util.collection.WeakValueHashMapTest.class }) public final strictfp class UtilityTestSuite extends TestSuite { } Modified: sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakHashSetTest.java URL: http://svn.apache.org/viewvc/sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakHashSetTest.java?rev=1394018&r1=1394017&r2=1394018&view=diff ============================================================================== --- sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakHashSetTest.java (original) +++ sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakHashSetTest.java Thu Oct 4 12:47:05 2012 @@ -144,7 +144,8 @@ public final strictfp class WeakHashSetT * Tests with array elements. */ @Test - public void testArray() { + @DependsOnMethod("testStrongReferences") + public void testWithArrayElements() { final WeakHashSet<int[]> weakSet = WeakHashSet.newInstance(int[].class); final int[] array = new int[] {2, 5, 3}; assertTrue (weakSet.add(array)); Added: sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java URL: http://svn.apache.org/viewvc/sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java?rev=1394018&view=auto ============================================================================== --- sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java (added) +++ sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java Thu Oct 4 12:47:05 2012 @@ -0,0 +1,152 @@ +/* + * 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.sis.util.collection; + +import java.util.HashMap; +import java.util.Random; +import org.apache.sis.test.TestCase; +import org.apache.sis.test.DependsOnMethod; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests the {@link WeakValueHashMap}. + * A standard {@link HashMap} object is used for comparison purpose. + * + * @author Martin Desruisseaux (IRD, Geomatys) + * @since 0.3 (derived from geotk-2.0) + * @version 0.3 + * @module + */ +public final strictfp class WeakValueHashMapTest extends TestCase { + /** + * The size of the test sets to be created. + */ + private static final int SAMPLE_SIZE = 500; + + /** + * Tests the {@link WeakValueHashMap} using strong references. + * The tested {@link WeakValueHashMap} should behave like a standard {@link Map} object. + */ + @Test + public void testStrongReferences() { + final Random random = new Random(); + for (int pass=0; pass<4; pass++) { + final WeakValueHashMap<Integer,Integer> weakMap = WeakValueHashMap.newInstance(Integer.class); + final HashMap<Integer,Integer> strongMap = new HashMap<Integer,Integer>(); + for (int i=0; i<SAMPLE_SIZE; i++) { + final Integer key = random.nextInt(SAMPLE_SIZE); + final Integer value = random.nextInt(SAMPLE_SIZE); + assertEquals("containsKey:", strongMap.containsKey(key), weakMap.containsKey(key)); + assertEquals("containsValue:", strongMap.containsValue(value), weakMap.containsValue(value)); + assertSame ("get:", strongMap.get(key), weakMap.get(key)); + if (random.nextBoolean()) { + // Test addition. + assertSame("put:", strongMap.put(key, value), weakMap.put(key, value)); + } else { + // Test remove + assertSame("remove:", strongMap.remove(key), weakMap.remove(key)); + } + assertEquals("size:", strongMap.size(), weakMap.size()); + assertEquals("equals:", strongMap, weakMap); + } + } + } + + /** + * Tests the {@link WeakValueHashMap} using weak references. + * In this test, we have to keep in mind than some elements + * in {@code weakMap} may disappear at any time. + * + * @throws InterruptedException If the test has been interrupted. + */ + @Test + @DependsOnMethod("testStrongReferences") + public void testWeakReferences() throws InterruptedException { + final Random random = new Random(); + for (int pass=0; pass<2; pass++) { + final WeakValueHashMap<Integer,Integer> weakMap = WeakValueHashMap.newInstance(Integer.class); + final HashMap<Integer,Integer> strongMap = new HashMap<Integer,Integer>(); + for (int i=0; i<SAMPLE_SIZE; i++) { + // We really want new instances here. + final Integer key = new Integer(random.nextInt(SAMPLE_SIZE)); + final Integer value = new Integer(random.nextInt(SAMPLE_SIZE)); + if (random.nextBoolean()) { + /* + * Tests addition. + */ + final Integer weakPrevious = weakMap .put(key, value); + final Integer strongPrevious = strongMap.put(key, value); + if (weakPrevious == null) { + /* + * The element was not in the WeakValueHashMap, possibly GC collected it. + * Consequently that element can not be in the HashMap neither, otherwise + * a strong reference would exist which should have prevented the element + * from being removed from the WeakValueHashMap. + */ + assertNull("put:", strongPrevious); + } else { + assertNotSame(value, weakPrevious); + } + if (strongPrevious != null) { + // Note: If 'strongPrevious==null', 'weakPrevious' can not + // be null since GC has not collected its entry yet. + assertSame("put:", strongPrevious, weakPrevious); + } + } else { + /* + * Tests remove. + */ + final Integer weakPrevious = weakMap.get(key); + final Integer strongPrevious = strongMap.remove(key); + if (strongPrevious != null) { + assertSame("remove:", strongPrevious, weakPrevious); + } + } + assertTrue("containsAll:", weakMap.entrySet().containsAll(strongMap.entrySet())); + } + // Do our best to lets GC finish its work. + for (int i=0; i<4; i++) { + Thread.sleep(50); + System.gc(); + } + assertEquals("equals:", strongMap, weakMap); + } + } + + /** + * Tests with array keys. + */ + @Test + @DependsOnMethod("testStrongReferences") + public void testWithArrayKeys() { + final WeakValueHashMap<int[],Integer> weakMap = WeakValueHashMap.newInstance(int[].class); + final int[] k1 = new int[] {2, 5, 3}; + final int[] k2 = new int[] {2, 5, 4}; + final Integer v1 = 1; + final Integer v2 = 2; + assertNull ( weakMap.put(k1, v1)); + assertSame (v1, weakMap.put(k1, v1)); + assertSame (v1, weakMap.put(k1.clone(), v1)); + assertNull ( weakMap.put(k2, v2)); + assertSame (v2, weakMap.put(k2, v2)); + assertSame (v1, weakMap.get(k1)); + assertSame (v2, weakMap.get(k2)); + } +} Propchange: sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/trunk/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java ------------------------------------------------------------------------------ svn:mime-type = text/plain