diff --git a/data/test/ReferenceIdentityMap.emptyCollection.version3.1.obj b/data/test/ReferenceIdentityMap.emptyCollection.version3.1.obj new file mode 100644 index 000000000..64ebde74b Binary files /dev/null and b/data/test/ReferenceIdentityMap.emptyCollection.version3.1.obj differ diff --git a/data/test/ReferenceIdentityMap.fullCollection.version3.1.obj b/data/test/ReferenceIdentityMap.fullCollection.version3.1.obj new file mode 100644 index 000000000..6d42d87b0 Binary files /dev/null and b/data/test/ReferenceIdentityMap.fullCollection.version3.1.obj differ diff --git a/src/java/org/apache/commons/collections/map/ReferenceIdentityMap.java b/src/java/org/apache/commons/collections/map/ReferenceIdentityMap.java new file mode 100644 index 000000000..c9bb30d7c --- /dev/null +++ b/src/java/org/apache/commons/collections/map/ReferenceIdentityMap.java @@ -0,0 +1,212 @@ +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed 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.commons.collections.map; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.ref.Reference; + +/** + * A Map implementation that allows mappings to be + * removed by the garbage collector and matches keys and values based + * on == not equals(). + *

+ *

+ * When you construct a ReferenceIdentityMap, you can specify what kind + * of references are used to store the map's keys and values. + * If non-hard references are used, then the garbage collector can remove + * mappings if a key or value becomes unreachable, or if the JVM's memory is + * running low. For information on how the different reference types behave, + * see {@link Reference}. + *

+ * Different types of references can be specified for keys and values. + * The default constructor uses hard keys and soft values, providing a + * memory-sensitive cache. + *

+ * This map is similar to + * {@link org.apache.commons.collections.map.ReferenceMap ReferenceMap}. + * It differs in that keys and values in this class are compared using ==. + *

+ * This map will violate the detail of various Map and map view contracts. + * As a general rule, don't compare this map to other maps. + *

+ * This {@link Map} implementation does not allow null elements. + * Attempting to add a null key or value to the map will raise a NullPointerException. + *

+ * This implementation is not synchronized. + * You can use {@link java.util.Collections#synchronizedMap} to + * provide synchronized access to a ReferenceIdentityMap. + * Remember that synchronization will not stop the garbage collecter removing entries. + *

+ * All the available iterators can be reset back to the start by casting to + * ResettableIterator and calling reset(). + * + * @see java.lang.ref.Reference + * + * @since Commons Collections 3.0 (previously in main package v2.1) + * @version $Revision: 1.1 $ $Date: 2004/04/27 21:37:32 $ + * + * @author Stephen Colebourne + */ +public class ReferenceIdentityMap extends AbstractReferenceMap implements Serializable { + + /** Serialization version */ + private static final long serialVersionUID = -1266190134568365852L; + + /** + * Constructs a new ReferenceIdentityMap that will + * use hard references to keys and soft references to values. + */ + public ReferenceIdentityMap() { + super(HARD, SOFT, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false); + } + + /** + * Constructs a new ReferenceIdentityMap that will + * use the specified types of references. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + */ + public ReferenceIdentityMap(int keyType, int valueType) { + super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false); + } + + /** + * Constructs a new ReferenceIdentityMap that will + * use the specified types of references. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param purgeValues should the value be automatically purged when the + * key is garbage collected + */ + public ReferenceIdentityMap(int keyType, int valueType, boolean purgeValues) { + super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, purgeValues); + } + + /** + * Constructs a new ReferenceIdentityMap with the + * specified reference types, load factor and initial capacity. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param capacity the initial capacity for the map + * @param loadFactor the load factor for the map + */ + public ReferenceIdentityMap(int keyType, int valueType, int capacity, float loadFactor) { + super(keyType, valueType, capacity, loadFactor, false); + } + + /** + * Constructs a new ReferenceIdentityMap with the + * specified reference types, load factor and initial capacity. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param capacity the initial capacity for the map + * @param loadFactor the load factor for the map + * @param purgeValues should the value be automatically purged when the + * key is garbage collected + */ + public ReferenceIdentityMap(int keyType, int valueType, int capacity, + float loadFactor, boolean purgeValues) { + super(keyType, valueType, capacity, loadFactor, purgeValues); + } + + //----------------------------------------------------------------------- + /** + * Gets the hash code for the key specified. + *

+ * This implementation uses the identity hash code. + * + * @param key the key to get a hash code for + * @return the hash code + */ + protected int hash(Object key) { + return System.identityHashCode(key); + } + + /** + * Gets the hash code for a MapEntry. + *

+ * This implementation uses the identity hash code. + * + * @param key the key to get a hash code for, may be null + * @param value the value to get a hash code for, may be null + * @return the hash code, as per the MapEntry specification + */ + protected int hashEntry(Object key, Object value) { + return System.identityHashCode(key) ^ + System.identityHashCode(value); + } + + /** + * Compares two keys for equals. + *

+ * This implementation converts the key from the entry to a real reference + * before comparison and uses ==. + * + * @param key1 the first key to compare passed in from outside + * @param key2 the second key extracted from the entry via entry.key + * @return true if equal by identity + */ + protected boolean isEqualKey(Object key1, Object key2) { + key2 = (keyType > HARD ? ((Reference) key2).get() : key2); + return (key1 == key2); + } + + /** + * Compares two values for equals. + *

+ * This implementation uses ==. + * + * @param value1 the first value to compare passed in from outside + * @param value2 the second value extracted from the entry via getValue() + * @return true if equal by identity + */ + protected boolean isEqualValue(Object value1, Object value2) { + return (value1 == value2); + } + + //----------------------------------------------------------------------- + /** + * Write the map out using a custom routine. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + doWriteObject(out); + } + + /** + * Read the map in using a custom routine. + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + doReadObject(in); + } + +} diff --git a/src/test/org/apache/commons/collections/map/TestReferenceIdentityMap.java b/src/test/org/apache/commons/collections/map/TestReferenceIdentityMap.java new file mode 100644 index 000000000..d9234937f --- /dev/null +++ b/src/test/org/apache/commons/collections/map/TestReferenceIdentityMap.java @@ -0,0 +1,292 @@ +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed 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.commons.collections.map; + +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.Map; + +import junit.framework.Test; + +import org.apache.commons.collections.BulkTest; +import org.apache.commons.collections.IterableMap; + +/** + * Tests for ReferenceIdentityMap. + * + * @version $Revision: 1.1 $ + * + * @author Paul Jack + * @author Stephen Colebourne + */ +public class TestReferenceIdentityMap extends AbstractTestIterableMap { + + private static final Integer I1A = new Integer(1); + private static final Integer I1B = new Integer(1); + private static final Integer I2A = new Integer(2); + private static final Integer I2B = new Integer(2); + + public TestReferenceIdentityMap(String testName) { + super(testName); + } + + public static Test suite() { + return BulkTest.makeSuite(TestReferenceIdentityMap.class); + } + + public static void main(String args[]) { + String[] testCaseName = { TestReferenceIdentityMap.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + public Map makeEmptyMap() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + return map; + } + + public Map makeConfirmedMap() { + // Testing against another [collections] class generally isn't a good idea, + // but the alternative is a JDK1.4 dependency in the tests + return new IdentityMap(); + } + + public boolean isAllowNullKey() { + return false; + } + + public boolean isAllowNullValue() { + return false; + } + + //----------------------------------------------------------------------- + public void testBasics() { + IterableMap map = new ReferenceIdentityMap(ReferenceIdentityMap.HARD, ReferenceIdentityMap.HARD); + assertEquals(0, map.size()); + + map.put(I1A, I2A); + assertEquals(1, map.size()); + assertSame(I2A, map.get(I1A)); + assertSame(null, map.get(I1B)); + assertEquals(true, map.containsKey(I1A)); + assertEquals(false, map.containsKey(I1B)); + assertEquals(true, map.containsValue(I2A)); + assertEquals(false, map.containsValue(I2B)); + + map.put(I1A, I2B); + assertEquals(1, map.size()); + assertSame(I2B, map.get(I1A)); + assertSame(null, map.get(I1B)); + assertEquals(true, map.containsKey(I1A)); + assertEquals(false, map.containsKey(I1B)); + assertEquals(false, map.containsValue(I2A)); + assertEquals(true, map.containsValue(I2B)); + + map.put(I1B, I2B); + assertEquals(2, map.size()); + assertSame(I2B, map.get(I1A)); + assertSame(I2B, map.get(I1B)); + assertEquals(true, map.containsKey(I1A)); + assertEquals(true, map.containsKey(I1B)); + assertEquals(false, map.containsValue(I2A)); + assertEquals(true, map.containsValue(I2B)); + } + + //----------------------------------------------------------------------- + public void testHashEntry() { + IterableMap map = new ReferenceIdentityMap(ReferenceIdentityMap.HARD, ReferenceIdentityMap.HARD); + + map.put(I1A, I2A); + map.put(I1B, I2A); + + Map.Entry entry1 = (Map.Entry) map.entrySet().iterator().next(); + Iterator it = map.entrySet().iterator(); + Map.Entry entry2 = (Map.Entry) it.next(); + Map.Entry entry3 = (Map.Entry) it.next(); + + assertEquals(true, entry1.equals(entry2)); + assertEquals(true, entry2.equals(entry1)); + assertEquals(false, entry1.equals(entry3)); + } + + + //----------------------------------------------------------------------- + // Unfortunately, these tests all rely on System.gc(), which is + // not reliable across platforms. Not sure how to code the tests + // without using System.gc() though... + // They all passed on my platform though. :) +/* + public void testPurge() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + Object[] hard = new Object[10]; + for (int i = 0; i < hard.length; i++) { + hard[i] = new Object(); + map.put(hard[i], new Object()); + } + System.gc(); + assertTrue("map should be empty after purge of weak values", map.isEmpty()); + + for (int i = 0; i < hard.length; i++) { + map.put(new Object(), hard[i]); + } + System.gc(); + assertTrue("map should be empty after purge of weak keys", map.isEmpty()); + + for (int i = 0; i < hard.length; i++) { + map.put(new Object(), hard[i]); + map.put(hard[i], new Object()); + } + + System.gc(); + assertTrue("map should be empty after purge of weak keys and values", map.isEmpty()); + } + + + public void testGetAfterGC() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + for (int i = 0; i < 10; i++) { + map.put(new Integer(i), new Integer(i)); + } + + System.gc(); + for (int i = 0; i < 10; i++) { + Integer I = new Integer(i); + assertTrue("map.containsKey should return false for GC'd element", !map.containsKey(I)); + assertTrue("map.get should return null for GC'd element", map.get(I) == null); + } + } + + + public void testEntrySetIteratorAfterGC() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + Object[] hard = new Object[10]; + for (int i = 0; i < 10; i++) { + hard[i] = new Integer(10 + i); + map.put(new Integer(i), new Integer(i)); + map.put(hard[i], hard[i]); + } + + System.gc(); + Iterator iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry)iterator.next(); + Integer key = (Integer)entry.getKey(); + Integer value = (Integer)entry.getValue(); + assertTrue("iterator should skip GC'd keys", key.intValue() >= 10); + assertTrue("iterator should skip GC'd values", value.intValue() >= 10); + } + + } + + public void testMapIteratorAfterGC() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + Object[] hard = new Object[10]; + for (int i = 0; i < 10; i++) { + hard[i] = new Integer(10 + i); + map.put(new Integer(i), new Integer(i)); + map.put(hard[i], hard[i]); + } + + System.gc(); + MapIterator iterator = map.mapIterator(); + while (iterator.hasNext()) { + Object key1 = iterator.next(); + Integer key = (Integer) iterator.getKey(); + Integer value = (Integer) iterator.getValue(); + assertTrue("iterator keys should match", key == key1); + assertTrue("iterator should skip GC'd keys", key.intValue() >= 10); + assertTrue("iterator should skip GC'd values", value.intValue() >= 10); + } + + } + + public void testMapIteratorAfterGC2() { + ReferenceIdentityMap map = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.WEAK); + Object[] hard = new Object[10]; + for (int i = 0; i < 10; i++) { + hard[i] = new Integer(10 + i); + map.put(new Integer(i), new Integer(i)); + map.put(hard[i], hard[i]); + } + + MapIterator iterator = map.mapIterator(); + while (iterator.hasNext()) { + Object key1 = iterator.next(); + System.gc(); + Integer key = (Integer) iterator.getKey(); + Integer value = (Integer) iterator.getValue(); + assertTrue("iterator keys should match", key == key1); + assertTrue("iterator should skip GC'd keys", key.intValue() >= 10); + assertTrue("iterator should skip GC'd values", value.intValue() >= 10); + } + + } +*/ +/* + // Uncomment to create test files in /data/test + public void testCreateTestFiles() throws Exception { + ReferenceIdentityMap m = (ReferenceIdentityMap) makeEmptyMap(); + writeExternalFormToDisk(m, getCanonicalEmptyCollectionName(m)); + m = (ReferenceIdentityMap) makeFullMap(); + writeExternalFormToDisk(m, getCanonicalFullCollectionName(m)); + } +*/ + + + public String getCompatibilityVersion() { + return "3.1"; + } + + /** Tests whether purge values setting works */ + public void testPurgeValues() throws Exception { + // many thanks to Juozas Baliuka for suggesting this method + Object key = new Object(); + Object value = new Object(); + + WeakReference keyReference = new WeakReference(key); + WeakReference valueReference = new WeakReference(value); + + Map testMap = new ReferenceIdentityMap(ReferenceIdentityMap.WEAK, ReferenceIdentityMap.HARD, true); + testMap.put(key, value); + + assertEquals("In map", value, testMap.get(key)); + assertNotNull("Weak reference released early (1)", keyReference.get()); + assertNotNull("Weak reference released early (2)", valueReference.get()); + + // dereference strong references + key = null; + value = null; + + int iterations = 0; + int bytz = 2; + while(true) { + System.gc(); + if(iterations++ > 50){ + fail("Max iterations reached before resource released."); + } + testMap.isEmpty(); + if( + keyReference.get() == null && + valueReference.get() == null) { + break; + + } else { + // create garbage: + byte[] b = new byte[bytz]; + bytz = bytz * 2; + } + } + } +}