diff --git a/data/test/HashedMap.emptyCollection.version3.obj b/data/test/HashedMap.emptyCollection.version3.obj index bda164e44..f89ef0ca6 100644 Binary files a/data/test/HashedMap.emptyCollection.version3.obj and b/data/test/HashedMap.emptyCollection.version3.obj differ diff --git a/data/test/HashedMap.fullCollection.version3.obj b/data/test/HashedMap.fullCollection.version3.obj index f542b20fd..7ec98a6ed 100644 Binary files a/data/test/HashedMap.fullCollection.version3.obj and b/data/test/HashedMap.fullCollection.version3.obj differ diff --git a/data/test/IdentityMap.emptyCollection.version3.obj b/data/test/IdentityMap.emptyCollection.version3.obj index fdeb41c8c..52731116a 100644 Binary files a/data/test/IdentityMap.emptyCollection.version3.obj and b/data/test/IdentityMap.emptyCollection.version3.obj differ diff --git a/data/test/IdentityMap.fullCollection.version3.obj b/data/test/IdentityMap.fullCollection.version3.obj index 981b87192..c24840231 100644 Binary files a/data/test/IdentityMap.fullCollection.version3.obj and b/data/test/IdentityMap.fullCollection.version3.obj differ diff --git a/data/test/LRUMap.emptyCollection.version3.obj b/data/test/LRUMap.emptyCollection.version3.obj new file mode 100644 index 000000000..59b17b460 Binary files /dev/null and b/data/test/LRUMap.emptyCollection.version3.obj differ diff --git a/data/test/LRUMap.fullCollection.version3.obj b/data/test/LRUMap.fullCollection.version3.obj new file mode 100644 index 000000000..f311f04e5 Binary files /dev/null and b/data/test/LRUMap.fullCollection.version3.obj differ diff --git a/data/test/LinkedMap.emptyCollection.version3.obj b/data/test/LinkedMap.emptyCollection.version3.obj index 8da49d7b4..cab0aeedc 100644 Binary files a/data/test/LinkedMap.emptyCollection.version3.obj and b/data/test/LinkedMap.emptyCollection.version3.obj differ diff --git a/data/test/LinkedMap.fullCollection.version3.obj b/data/test/LinkedMap.fullCollection.version3.obj index adc6a4517..939b729d6 100644 Binary files a/data/test/LinkedMap.fullCollection.version3.obj and b/data/test/LinkedMap.fullCollection.version3.obj differ diff --git a/src/java/org/apache/commons/collections/map/HashedMap.java b/src/java/org/apache/commons/collections/map/HashedMap.java index 56e150120..732db7134 100644 --- a/src/java/org/apache/commons/collections/map/HashedMap.java +++ b/src/java/org/apache/commons/collections/map/HashedMap.java @@ -1,5 +1,5 @@ /* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/map/HashedMap.java,v 1.7 2003/12/06 14:02:11 scolebourne Exp $ + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/map/HashedMap.java,v 1.8 2003/12/07 01:23:54 scolebourne Exp $ * ==================================================================== * * The Apache Software License, Version 1.1 @@ -87,7 +87,7 @@ import org.apache.commons.collections.MapIterator; * methods exposed. * * @since Commons Collections 3.0 - * @version $Revision: 1.7 $ $Date: 2003/12/06 14:02:11 $ + * @version $Revision: 1.8 $ $Date: 2003/12/07 01:23:54 $ * * @author java util HashMap * @author Stephen Colebourne @@ -113,7 +113,7 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { protected static final Object NULL = new Object(); /** Load factor, normally 0.75 */ - protected final float loadFactor; + protected transient float loadFactor; /** The size of the map */ protected transient int size; /** Map entries */ @@ -157,14 +157,14 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is less than one - * @throws IllegalArgumentException if the load factor is less than one + * @throws IllegalArgumentException if the load factor is less than zero */ public HashedMap(int initialCapacity, float loadFactor) { super(); if (initialCapacity < 1) { throw new IllegalArgumentException("Initial capacity must be greater than 0"); } - if (loadFactor <= 0 || Float.isNaN(loadFactor)) { + if (loadFactor <= 0.0f || Float.isNaN(loadFactor)) { throw new IllegalArgumentException("Load factor must be greater than 0"); } this.loadFactor = loadFactor; @@ -296,14 +296,13 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { while (entry != null) { if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) { Object oldValue = entry.getValue(); - entry.setValue(value); + updateEntry(entry, value); return oldValue; } entry = entry.next; } - modCount++; - add(hashCode, index, key, value); + addMapping(index, hashCode, key, value); return null; } @@ -335,18 +334,13 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { key = convertKey(key); int hashCode = hash(key); int index = hashIndex(hashCode, data.length); - HashEntry entry = data[index]; + HashEntry entry = data[index]; HashEntry previous = null; while (entry != null) { if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) { - modCount++; - if (previous == null) { - data[index] = entry.next; - } else { - previous.next = entry.next; - } - size--; - return destroyEntry(entry); + Object oldValue = entry.getValue(); + removeMapping(entry, index, previous); + return oldValue; } previous = entry; entry = entry.next; @@ -368,25 +362,6 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { } //----------------------------------------------------------------------- - /** - * Gets the entry mapped to the key specified. - * - * @param key the key - * @return the entry, null if no match - */ - protected HashEntry getEntry(Object key) { - key = convertKey(key); - int hashCode = hash(key); - HashEntry entry = data[hashIndex(hashCode, data.length)]; // no local for hash index - while (entry != null) { - if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) { - return entry; - } - entry = entry.next; - } - return null; - } - /** * Converts input keys to another object for storage in the map. * This implementation masks nulls. @@ -459,9 +434,91 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { return hashCode & (dataSize - 1); } + //----------------------------------------------------------------------- /** - * Creates an entry to store the data. - * This implementation creates a HashEntry instance. + * Gets the entry mapped to the key specified. + *

+ * This method exists for subclasses that may need to perform a multi-step + * process accessing the entry. The public methods in this class don't use this + * method to gain a small performance boost. + * + * @param key the key + * @return the entry, null if no match + */ + protected HashEntry getEntry(Object key) { + key = convertKey(key); + int hashCode = hash(key); + HashEntry entry = data[hashIndex(hashCode, data.length)]; // no local for hash index + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) { + return entry; + } + entry = entry.next; + } + return null; + } + + //----------------------------------------------------------------------- + /** + * Updates an existing key-value mapping to change the value. + *

+ * This implementation calls setValue() on the entry. + * Subclasses could override to handle changes to the map. + * + * @param entry the entry to update + * @param newValue the new value to store + * @return value the previous value + */ + protected void updateEntry(HashEntry entry, Object newValue) { + entry.setValue(newValue); + } + + /** + * Reuses an existing key-value mapping, storing completely new data. + *

+ * This implementation sets all the data fields on the entry. + * Subclasses could populate additional entry fields. + * + * @param entry the entry to update, not null + * @param hashIndex the index in the data array + * @param hashCode the hash code of the key to add + * @param key the key to add + * @param value the value to add + */ + protected void reuseEntry(HashEntry entry, int hashIndex, int hashCode, Object key, Object value) { + entry.next = data[hashIndex]; + entry.hashCode = hashCode; + entry.key = key; + entry.value = value; + } + + //----------------------------------------------------------------------- + /** + * Adds a new key-value mapping into this map. + *

+ * This implementation calls createEntry(), addEntry() + * and checkCapacity. + * It also handles changes to modCount and size. + * Subclasses could override to fully control adds to the map. + * + * @param hashIndex the index into the data array to store at + * @param hashCode the hash code of the key to add + * @param key the key to add + * @param value the value to add + * @return the value previously mapped to this key, null if none + */ + protected void addMapping(int hashIndex, int hashCode, Object key, Object value) { + modCount++; + HashEntry entry = createEntry(data[hashIndex], hashCode, key, value); + addEntry(entry, hashIndex); + size++; + checkCapacity(); + } + + /** + * Creates an entry to store the key-value data. + *

+ * This implementation creates a new HashEntry instance. * Subclasses can override this to return a different storage class, * or implement caching. * @@ -476,29 +533,81 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { } /** - * Kills an entry ready for the garbage collector. - * This implementation prepares the HashEntry for garbage collection. - * Subclasses can override this to implement caching (override clear as well). - * - * @param entry the entry to destroy - * @return the value from the entry - */ - protected Object destroyEntry(HashEntry entry) { - entry.next = null; - return entry.value; - } - - /** - * Adds a new key-value mapping into this map. - * Subclasses could override to fix the size of the map. + * Adds an entry into this map. + *

+ * This implementation adds the entry to the data storage table. + * Subclasses could override to handle changes to the map. * + * @param hashIndex the index into the data array to store at + * @param hashCode the hash code of the key to add * @param key the key to add * @param value the value to add * @return the value previously mapped to this key, null if none */ - protected void add(int hashCode, int hashIndex, Object key, Object value) { - data[hashIndex] = createEntry(data[hashIndex], hashCode, key, value); - if (size++ >= threshold) { + protected void addEntry(HashEntry entry, int hashIndex) { + data[hashIndex] = entry; + } + + //----------------------------------------------------------------------- + /** + * Removes a mapping from the map. + *

+ * This implementation calls removeEntry() and destroyEntry(). + * It also handles changes to modCount and size. + * Subclasses could override to fully control removals from the map. + * + * @param entry the entry to remove + * @param hashIndex the index into the data structure + * @param previous the previous entry in the chain + */ + protected void removeMapping(HashEntry entry, int hashIndex, HashEntry previous) { + modCount++; + removeEntry(entry, hashIndex, previous); + size--; + destroyEntry(entry); + } + + /** + * Removes an entry from the chain stored in a particular index. + *

+ * This implementation removes the entry from the data storage table. + * The size is not updated. + * Subclasses could override to handle changes to the map. + * + * @param entry the entry to remove + * @param hashIndex the index into the data structure + * @param previous the previous entry in the chain + */ + protected void removeEntry(HashEntry entry, int hashIndex, HashEntry previous) { + if (previous == null) { + data[hashIndex] = entry.next; + } else { + previous.next = entry.next; + } + } + + /** + * Kills an entry ready for the garbage collector. + *

+ * This implementation prepares the HashEntry for garbage collection. + * Subclasses can override this to implement caching (override clear as well). + * + * @param entry the entry to destroy + */ + protected void destroyEntry(HashEntry entry) { + entry.next = null; + entry.key = null; + entry.value = null; + } + + //----------------------------------------------------------------------- + /** + * Checks the capacity of the map and enlarges it if necessary. + *

+ * This implementation uses the threshold to check if the map needs enlarging + */ + protected void checkCapacity() { + if (size >= threshold) { ensureCapacity(data.length * 2); } } @@ -874,17 +983,21 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { this.key = key; this.value = value; } + public Object getKey() { return (key == NULL ? null : key); } + public Object getValue() { return value; } + public Object setValue(Object value) { Object old = this.value; this.value = value; return old; } + public boolean equals(Object obj) { if (obj == this) { return true; @@ -897,10 +1010,12 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue())); } + public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } + public String toString() { return new StringBuffer().append(getKey()).append('=').append(getValue()).toString(); } @@ -980,13 +1095,49 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { } //----------------------------------------------------------------------- + /** + * Write the data of subclasses. + *

+ * Serialization is not one of the JDK's nicest topics. Normal serialization will + * not load subclass fields until this object is setup. In other words the readObject + * on this class is performed before that on the subclass. Unfortunately, data setup in + * the subclass may affect the ability of methods such as put() to work properly. + *

+ * The solution adopted here is to have a method called by this class as part of the + * serialization and deserialization process. Override this method if the subclass + * stores additional data needed for put() to work. + * A sub-subclass must call super.doWriteObject(). + */ + protected void doWriteObject(ObjectOutputStream out) throws IOException { + // do nothing + } + + /** + * Read the data of subclasses. + *

+ * Serialization is not one of the JDK's nicest topics. Normal serialization will + * not load subclass fields until this object is setup. In other words the readObject + * on this class is performed before that on the subclass. Unfortunately, data setup in + * the subclass may affect the ability of methods such as put() to work properly. + *

+ * The solution adopted here is to have a method called by this class as part of the + * serialization and deserialization process. Override this method if the subclass + * stores additional data needed for put() to work. + * A sub-subclass must call super.doReadObject(). + */ + protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + // do nothing + } + /** * Write the map out using a custom routine. */ private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); + out.writeFloat(loadFactor); out.writeInt(data.length); out.writeInt(size); + doWriteObject(out); for (MapIterator it = mapIterator(); it.hasNext();) { out.writeObject(it.next()); out.writeObject(it.getValue()); @@ -998,16 +1149,20 @@ public class HashedMap implements IterableMap, Serializable, Cloneable { */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); + loadFactor = in.readFloat(); int capacity = in.readInt(); int size = in.readInt(); - data = new HashEntry[capacity]; + doReadObject(in); init(); + data = new HashEntry[capacity]; for (int i = 0; i < size; i++) { Object key = in.readObject(); Object value = in.readObject(); put(key, value); } + threshold = calculateThreshold(data.length, loadFactor); } + //----------------------------------------------------------------------- /** * Clones the map without cloning the keys or values. diff --git a/src/java/org/apache/commons/collections/map/LRUMap.java b/src/java/org/apache/commons/collections/map/LRUMap.java new file mode 100644 index 000000000..30cd56f82 --- /dev/null +++ b/src/java/org/apache/commons/collections/map/LRUMap.java @@ -0,0 +1,327 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/map/LRUMap.java,v 1.1 2003/12/07 01:23:54 scolebourne Exp $ + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2001-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowledgement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgement may appear in the software itself, + * if and wherever such third-party acknowledgements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.commons.collections.map; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Map; + +import org.apache.commons.collections.OrderedMap; + +/** + * A Map implementation with a fixed maximum size which removes + * the least recently used entry if an entry is added when full. + *

+ * The least recently used algorithm works on the get and put operations only. + * Iteration of any kind, including setting the value by iteration, does not + * change the order. Queries such as containsKey and containsValue or access + * via views also do not change the order. + *

+ * The map implements OrderedMap and entries may be queried using + * the bidirectional OrderedMapIterator. The order returned is + * most recently used to least recently used. Iterators from map views can + * also be cast to OrderedIterator if required. + *

+ * All the available iterators can be reset back to the start by casting to + * ResettableIterator and calling reset(). + *

+ * NOTE: The order of the map has changed from the previous version located + * in the main collections package. The map is now ordered most recently used + * to least recently used. + * + * @since Commons Collections 3.0 + * @version $Revision: 1.1 $ $Date: 2003/12/07 01:23:54 $ + * + * @author James Strachan + * @author Morgan Delagrange + * @author Stephen Colebourne + */ +public class LRUMap extends LinkedMap implements OrderedMap { + + /** Serialisation version */ + static final long serialVersionUID = -2848625157350244215L; + /** Default maximum size */ + protected static final int DEFAULT_MAX_SIZE = 100; + + /** Maximum size */ + private transient int maxSize; + + /** + * Constructs a new empty map with a maximum size of 100. + */ + public LRUMap() { + this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs a new, empty map with the specified maximum size. + * + * @param maxSize the maximum size of the map + * @throws IllegalArgumentException if the maximum size is less than one + */ + public LRUMap(int maxSize) { + this(maxSize, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs a new, empty map with the specified initial capacity and + * load factor. + * + * @param maxSize the maximum size of the map, -1 for no limit, + * @param loadFactor the load factor + * @throws IllegalArgumentException if the maximum size is less than one + * @throws IllegalArgumentException if the load factor is less than zero + */ + public LRUMap(int maxSize, float loadFactor) { + super((maxSize < 1 ? DEFAULT_CAPACITY : maxSize), loadFactor); + if (maxSize < 1) { + throw new IllegalArgumentException("LRUMap max size must be greater than 0"); + } + this.maxSize = maxSize; + } + + /** + * Constructor copying elements from another map. + *

+ * The maximum size is set from the map's size. + * + * @param map the map to copy + * @throws NullPointerException if the map is null + * @throws IllegalArgumentException if the map is empty + */ + public LRUMap(Map map) { + this(map.size(), DEFAULT_LOAD_FACTOR); + putAll(map); + } + + //----------------------------------------------------------------------- + /** + * Gets the value mapped to the key specified. + *

+ * This operation changes the position of the key in the map to the + * most recently used position (first). + * + * @param key the key + * @return the mapped value, null if no match + */ + public Object get(Object key) { + LinkEntry entry = (LinkEntry) getEntry(key); + if (entry == null) { + return null; + } + moveFirst(entry); + return entry.getValue(); + } + + //----------------------------------------------------------------------- + /** + * Updates an existing key-value mapping. + * This implementation moves the updated entry to the top of the list. + * + * @param entry the entry to update + * @param newValue the new value to store + * @return value the previous value + */ + protected void moveFirst(LinkEntry entry) { + if (entry.before != header) { + modCount++; + // remove + entry.after.before = entry.before; + entry.before.after = entry.after; + // add first + entry.before = header; + entry.after = header.after; + header.after.before = entry; + header.after = entry; + } + } + + /** + * Updates an existing key-value mapping. + * This implementation moves the updated entry to the top of the list. + * + * @param entry the entry to update + * @param newValue the new value to store + * @return value the previous value + */ + protected void updateEntry(HashEntry entry, Object newValue) { + moveFirst((LinkEntry) entry); // handles modCount + entry.setValue(newValue); + } + + /** + * Adds a new key-value mapping into this map. + * This implementation checks the LRU size and determines whether to + * discard an entry or not. + * + * @param hashIndex the index into the data array to store at + * @param hashCode the hash code of the key to add + * @param key the key to add + * @param value the value to add + * @return the value previously mapped to this key, null if none + */ + protected void addMapping(int hashIndex, int hashCode, Object key, Object value) { + if (size >= maxSize && removeLRU(header.before)) { + LinkEntry entry = header.before; + // remove from current location + int removeIndex = hashIndex(entry.hashCode, data.length); + HashEntry loop = data[removeIndex]; + HashEntry previous = null; + while (loop != entry) { + previous = loop; + loop = loop.next; + } + modCount++; + removeEntry(entry, removeIndex, previous); + reuseEntry(entry, hashIndex, hashCode, key, value); + addEntry(entry, hashIndex); + + } else { + super.addMapping(hashIndex, hashCode, key, value); + } + } + + /** + * Adds a new entry into this map using access order. + *

+ * This implementation adds the entry to the data storage table and + * to the start of the linked list. + * + * @param entry the entry to add + * @param hashIndex the index into the data array to store at + */ + protected void addEntry(HashEntry entry, int hashIndex) { + LinkEntry link = (LinkEntry) entry; + link.before = header; + link.after = header.after; + header.after.before = link; + header.after = link; + data[hashIndex] = entry; + } + + /** + * Subclass method to control removal of the least recently used entry from the map. + *

+ * This method exists for subclasses to override. A subclass may wish to + * provide cleanup of resources when an entry is removed. For example: + *

+     * protected boolean removeLRU(LinkEntry entry) {
+     *   releaseResources(entry.getValue());  // release resources held by entry
+     *   return true;  // actually delete entry
+     * }
+     * 
+ *

+ * Alternatively, a subclass may choose to not remove the entry or selectively + * keep certain LRU entries. For example: + *

+     * protected boolean removeLRU(LinkEntry entry) {
+     *   if (entry.getKey().toString().startsWith("System.")) {
+     *     return false;  // entry not removed from LRUMap
+     *   } else {
+     *     return true;  // actually delete entry
+     *   }
+     * }
+     * 
+ * Note that the effect of not removing an LRU is for the Map to exceed the maximum size. + * + * @param entry the entry to be removed + */ + protected boolean removeLRU(LinkEntry entry) { + return true; + } + + //----------------------------------------------------------------------- + /** + * Returns true if this map is full and no new mappings can be added. + * + * @return true if the map is full + */ + public boolean isFull() { + return (size >= maxSize); + } + + /** + * Gets the maximum size of the map (the bound). + * + * @return the maximum number of elements the map can hold + */ + public int maxSize() { + return maxSize; + } + + //----------------------------------------------------------------------- + /** + * Write the data of subclasses. + * A sub-subclass must call super.doWriteObject(). + */ + protected void doWriteObject(ObjectOutputStream out) throws IOException { + out.writeInt(maxSize); + super.doWriteObject(out); + } + + /** + * Read the data of subclasses. + * A sub-subclass must call super.doReadObject(). + */ + protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + maxSize = in.readInt(); + super.doReadObject(in); + } + +} diff --git a/src/java/org/apache/commons/collections/map/LinkedMap.java b/src/java/org/apache/commons/collections/map/LinkedMap.java index d7fc852c1..e7ac66d52 100644 --- a/src/java/org/apache/commons/collections/map/LinkedMap.java +++ b/src/java/org/apache/commons/collections/map/LinkedMap.java @@ -1,5 +1,5 @@ /* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/map/LinkedMap.java,v 1.2 2003/12/06 14:02:11 scolebourne Exp $ + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/map/LinkedMap.java,v 1.3 2003/12/07 01:23:54 scolebourne Exp $ * ==================================================================== * * The Apache Software License, Version 1.1 @@ -71,7 +71,8 @@ import org.apache.commons.collections.ResettableIterator; /** * A Map implementation that maintains the order of the entries. - * The order maintained is by insertion. + * In this implementation order is maintained is by original insertion, but + * subclasses may work differently. *

* This implementation improves on the JDK1.4 LinkedHashMap by adding the * {@link org.apache.commons.collections.iterators.MapIterator MapIterator} @@ -84,9 +85,12 @@ import org.apache.commons.collections.ResettableIterator; *

* All the available iterators can be reset back to the start by casting to * ResettableIterator and calling reset(). + *

+ * The implementation is also designed to be subclassed, with lots of useful + * methods exposed. * * @since Commons Collections 3.0 - * @version $Revision: 1.2 $ $Date: 2003/12/06 14:02:11 $ + * @version $Revision: 1.3 $ $Date: 2003/12/07 01:23:54 $ * * @author java util LinkedHashMap * @author Stephen Colebourne @@ -97,7 +101,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { static final long serialVersionUID = -1954063410665686469L; /** Header in the linked list */ - private transient LinkedEntry header; + protected transient LinkEntry header; /** * Constructs a new empty map with default size and load factor. @@ -123,7 +127,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is less than one - * @throws IllegalArgumentException if the load factor is less than one + * @throws IllegalArgumentException if the load factor is less than zero */ public LinkedMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); @@ -143,7 +147,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { * Initialise this subclass during construction. */ protected void init() { - header = new LinkedEntry(null, -1, null, null); + header = new LinkEntry(null, -1, null, null); header.before = header.after = header; } @@ -157,13 +161,13 @@ public class LinkedMap extends HashedMap implements OrderedMap { public boolean containsValue(Object value) { // override uses faster iterator if (value == null) { - for (LinkedEntry entry = header.after; entry != header; entry = entry.after) { + for (LinkEntry entry = header.after; entry != header; entry = entry.after) { if (entry.getValue() == null) { return true; } } } else { - for (LinkedEntry entry = header.after; entry != header; entry = entry.after) { + for (LinkEntry entry = header.after; entry != header; entry = entry.after) { if (isEqualValue(value, entry.getValue())) { return true; } @@ -214,7 +218,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { * @return the next key */ public Object nextKey(Object key) { - LinkedEntry entry = (LinkedEntry) getEntry(key); + LinkEntry entry = (LinkEntry) getEntry(key); return (entry == null || entry.after == header ? null : entry.after.getKey()); } @@ -225,14 +229,33 @@ public class LinkedMap extends HashedMap implements OrderedMap { * @return the previous key */ public Object previousKey(Object key) { - LinkedEntry entry = (LinkedEntry) getEntry(key); + LinkEntry entry = (LinkEntry) getEntry(key); return (entry == null || entry.before == header ? null : entry.before.getKey()); } //----------------------------------------------------------------------- + /** + * Adds an entry into this map, maintaining insertion order. + *

+ * This implementation adds the entry to the data storage table and + * to the end of the linked list. + * + * @param entry the entry to add + * @param hashIndex the index into the data array to store at + */ + protected void addEntry(HashEntry entry, int hashIndex) { + LinkEntry link = (LinkEntry) entry; + link.after = header; + link.before = header.before; + header.before.after = link; + header.before = link; + data[hashIndex] = entry; + } + /** * Creates an entry to store the data. - * This implementation creates a LinkEntry instance in the linked list. + *

+ * This implementation creates a new LinkEntry instance. * * @param next the next entry in sequence * @param hashCode the hash code to use @@ -241,30 +264,26 @@ public class LinkedMap extends HashedMap implements OrderedMap { * @return the newly created entry */ protected HashEntry createEntry(HashEntry next, int hashCode, Object key, Object value) { - LinkedEntry entry = new LinkedEntry(next, hashCode, key, value); - entry.after = header; - entry.before = header.before; - header.before.after = entry; - header.before = entry; - return entry; + return new LinkEntry(next, hashCode, key, value); } /** - * Kills an entry ready for the garbage collector. - * This implementation manages the linked list and prepares the - * LinkEntry for garbage collection. + * Removes an entry from the map and the linked list. + *

+ * This implementation removes the entry from the linked list chain, then + * calls the superclass implementation. * - * @param entry the entry to destroy - * @return the value from the entry + * @param entry the entry to remove + * @param hashIndex the index into the data structure + * @param previous the previous entry in the chain */ - protected Object destroyEntry(HashEntry entry) { - LinkedEntry link = (LinkedEntry) entry; + protected void removeEntry(HashEntry entry, int hashIndex, HashEntry previous) { + LinkEntry link = (LinkEntry) entry; link.before.after = link.after; link.after.before = link.before; - link.next = null; link.after = null; link.before = null; - return entry.value; + super.removeEntry(entry, hashIndex, previous); } //----------------------------------------------------------------------- @@ -283,7 +302,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { if (size == 0) { return IteratorUtils.EMPTY_ORDERED_MAP_ITERATOR; } - return new LinkedMapIterator(this); + return new LinkMapIterator(this); } /** @@ -301,15 +320,15 @@ public class LinkedMap extends HashedMap implements OrderedMap { if (size == 0) { return IteratorUtils.EMPTY_ORDERED_MAP_ITERATOR; } - return new LinkedMapIterator(this); + return new LinkMapIterator(this); } /** * MapIterator */ - static class LinkedMapIterator extends LinkedIterator implements OrderedMapIterator { + static class LinkMapIterator extends LinkIterator implements OrderedMapIterator { - LinkedMapIterator(LinkedMap map) { + LinkMapIterator(LinkedMap map) { super(map); } @@ -363,7 +382,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { /** * EntrySetIterator and MapEntry */ - static class EntrySetIterator extends LinkedIterator { + static class EntrySetIterator extends LinkIterator { EntrySetIterator(LinkedMap map) { super(map); @@ -427,7 +446,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { /** * ValuesIterator */ - static class ValuesIterator extends LinkedIterator { + static class ValuesIterator extends LinkIterator { ValuesIterator(LinkedMap map) { super(map); @@ -446,12 +465,12 @@ public class LinkedMap extends HashedMap implements OrderedMap { /** * LinkEntry */ - protected static class LinkedEntry extends HashEntry { + protected static class LinkEntry extends HashEntry { - LinkedEntry before; - LinkedEntry after; + protected LinkEntry before; + protected LinkEntry after; - protected LinkedEntry(HashEntry next, int hashCode, Object key, Object value) { + protected LinkEntry(HashEntry next, int hashCode, Object key, Object value) { super(next, hashCode, key, value); } } @@ -459,15 +478,15 @@ public class LinkedMap extends HashedMap implements OrderedMap { /** * Base Iterator */ - protected static abstract class LinkedIterator + protected static abstract class LinkIterator implements OrderedIterator, ResettableIterator { - private final LinkedMap map; - private LinkedEntry current; - private LinkedEntry next; - private int expectedModCount; + protected final LinkedMap map; + protected LinkEntry current; + protected LinkEntry next; + protected int expectedModCount; - protected LinkedIterator(LinkedMap map) { + protected LinkIterator(LinkedMap map) { super(); this.map = map; this.next = map.header.after; @@ -482,7 +501,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { return (next.before != map.header); } - protected LinkedEntry nextEntry() { + protected LinkEntry nextEntry() { if (map.modCount != expectedModCount) { throw new ConcurrentModificationException(); } @@ -494,11 +513,11 @@ public class LinkedMap extends HashedMap implements OrderedMap { return current; } - protected LinkedEntry previousEntry() { + protected LinkEntry previousEntry() { if (map.modCount != expectedModCount) { throw new ConcurrentModificationException(); } - LinkedEntry previous = next.before; + LinkEntry previous = next.before; if (previous == map.header) { throw new NoSuchElementException(HashedMap.NO_PREVIOUS_ENTRY); } @@ -507,7 +526,7 @@ public class LinkedMap extends HashedMap implements OrderedMap { return current; } - protected LinkedEntry currentEntry() { + protected LinkEntry currentEntry() { return current; } diff --git a/src/test/org/apache/commons/collections/map/TestAll.java b/src/test/org/apache/commons/collections/map/TestAll.java index 7f117fa01..670dd86aa 100644 --- a/src/test/org/apache/commons/collections/map/TestAll.java +++ b/src/test/org/apache/commons/collections/map/TestAll.java @@ -1,5 +1,5 @@ /* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/test/org/apache/commons/collections/map/TestAll.java,v 1.9 2003/12/03 19:04:41 scolebourne Exp $ + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/test/org/apache/commons/collections/map/TestAll.java,v 1.10 2003/12/07 01:23:54 scolebourne Exp $ * ==================================================================== * * The Apache Software License, Version 1.1 @@ -65,7 +65,7 @@ import junit.framework.TestSuite; * Entry point for tests. * * @since Commons Collections 3.0 - * @version $Revision: 1.9 $ $Date: 2003/12/03 19:04:41 $ + * @version $Revision: 1.10 $ $Date: 2003/12/07 01:23:54 $ * * @author Stephen Colebourne */ @@ -87,6 +87,7 @@ public class TestAll extends TestCase { suite.addTest(TestHashedMap.suite()); suite.addTest(TestIdentityMap.suite()); suite.addTest(TestLinkedMap.suite()); + suite.addTest(TestLRUMap.suite()); suite.addTest(TestReferenceMap.suite()); suite.addTest(TestStaticBucketMap.suite()); diff --git a/src/test/org/apache/commons/collections/map/TestLRUMap.java b/src/test/org/apache/commons/collections/map/TestLRUMap.java new file mode 100644 index 000000000..b88d6b019 --- /dev/null +++ b/src/test/org/apache/commons/collections/map/TestLRUMap.java @@ -0,0 +1,346 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/test/org/apache/commons/collections/map/TestLRUMap.java,v 1.1 2003/12/07 01:23:54 scolebourne Exp $ + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2001-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowledgement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgement may appear in the software itself, + * if and wherever such third-party acknowledgements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.commons.collections.map; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import junit.framework.Test; +import junit.textui.TestRunner; + +import org.apache.commons.collections.BulkTest; +import org.apache.commons.collections.OrderedMap; +import org.apache.commons.collections.ResettableIterator; + +/** + * JUnit tests. + * + * @version $Revision: 1.1 $ $Date: 2003/12/07 01:23:54 $ + * + * @author Stephen Colebourne + */ +public class TestLRUMap extends AbstractTestOrderedMap { + + public TestLRUMap(String testName) { + super(testName); + } + + public static void main(String[] args) { + TestRunner.run(suite()); + } + + public static Test suite() { + return BulkTest.makeSuite(TestLRUMap.class); + } + + public Map makeEmptyMap() { + return new LRUMap(); + } + + public boolean isGetStructuralModify() { + return true; + } + + public String getCompatibilityVersion() { + return "3"; + } + + //----------------------------------------------------------------------- + public void testLRU() { + if (isPutAddSupported() == false || isPutChangeSupported() == false) return; + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Iterator it = null; + + LRUMap map = new LRUMap(2); + assertEquals(0, map.size()); + assertEquals(false, map.isFull()); + assertEquals(2, map.maxSize()); + + map.put(keys[0], values[0]); + assertEquals(1, map.size()); + assertEquals(false, map.isFull()); + assertEquals(2, map.maxSize()); + + map.put(keys[1], values[1]); + assertEquals(2, map.size()); + assertEquals(true, map.isFull()); + assertEquals(2, map.maxSize()); + it = map.keySet().iterator(); + assertSame(keys[1], it.next()); + assertSame(keys[0], it.next()); + it = map.values().iterator(); + assertSame(values[1], it.next()); + assertSame(values[0], it.next()); + + map.put(keys[2], values[2]); + assertEquals(2, map.size()); + assertEquals(true, map.isFull()); + assertEquals(2, map.maxSize()); + it = map.keySet().iterator(); + assertSame(keys[2], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[2], it.next()); + assertSame(values[1], it.next()); + + map.put(keys[2], values[0]); + assertEquals(2, map.size()); + assertEquals(true, map.isFull()); + assertEquals(2, map.maxSize()); + it = map.keySet().iterator(); + assertSame(keys[2], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[0], it.next()); + assertSame(values[1], it.next()); + + map.put(keys[1], values[3]); + assertEquals(2, map.size()); + assertEquals(true, map.isFull()); + assertEquals(2, map.maxSize()); + it = map.keySet().iterator(); + assertSame(keys[1], it.next()); + assertSame(keys[2], it.next()); + it = map.values().iterator(); + assertSame(values[3], it.next()); + assertSame(values[0], it.next()); + } + + //----------------------------------------------------------------------- + public void testReset() { + resetEmpty(); + OrderedMap ordered = (OrderedMap) map; + ((ResettableIterator) ordered.mapIterator()).reset(); + + resetFull(); + ordered = (OrderedMap) map; + List list = new ArrayList(ordered.keySet()); + ResettableIterator it = (ResettableIterator) ordered.mapIterator(); + assertSame(list.get(0), it.next()); + assertSame(list.get(1), it.next()); + it.reset(); + assertSame(list.get(0), it.next()); + } + + //----------------------------------------------------------------------- + public void testAccessOrder() { + if (isPutAddSupported() == false || isPutChangeSupported() == false) return; + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Iterator it = null; + + resetEmpty(); + map.put(keys[1], values[1]); + map.put(keys[0], values[0]); + it = map.keySet().iterator(); + assertSame(keys[0], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[0], it.next()); + assertSame(values[1], it.next()); + + // change to order + map.put(keys[1], values[1]); + it = map.keySet().iterator(); + assertSame(keys[1], it.next()); + assertSame(keys[0], it.next()); + it = map.values().iterator(); + assertSame(values[1], it.next()); + assertSame(values[0], it.next()); + + // no change to order + map.put(keys[1], values[2]); + it = map.keySet().iterator(); + assertSame(keys[1], it.next()); + assertSame(keys[0], it.next()); + it = map.values().iterator(); + assertSame(values[2], it.next()); + assertSame(values[0], it.next()); + + // change to order + map.put(keys[0], values[3]); + it = map.keySet().iterator(); + assertSame(keys[0], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[3], it.next()); + assertSame(values[2], it.next()); + + // change to order + map.get(keys[1]); + it = map.keySet().iterator(); + assertSame(keys[1], it.next()); + assertSame(keys[0], it.next()); + it = map.values().iterator(); + assertSame(values[2], it.next()); + assertSame(values[3], it.next()); + + // change to order + map.get(keys[0]); + it = map.keySet().iterator(); + assertSame(keys[0], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[3], it.next()); + assertSame(values[2], it.next()); + + // no change to order + map.get(keys[0]); + it = map.keySet().iterator(); + assertSame(keys[0], it.next()); + assertSame(keys[1], it.next()); + it = map.values().iterator(); + assertSame(values[3], it.next()); + assertSame(values[2], it.next()); + } + + //----------------------------------------------------------------------- + public void testFirstKey() { // override + resetEmpty(); + OrderedMap ordered = (OrderedMap) map; + try { + ordered.firstKey(); + fail(); + } catch (NoSuchElementException ex) {} + + resetFull(); + ordered = (OrderedMap) map; + Object confirmedFirst = confirmed.keySet().iterator().next(); + ordered.get(confirmedFirst); + assertEquals(confirmedFirst, ordered.firstKey()); + } + + public void testLastKey() { // override + resetEmpty(); + OrderedMap ordered = (OrderedMap) map; + try { + ordered.lastKey(); + fail(); + } catch (NoSuchElementException ex) {} + + resetFull(); + ordered = (OrderedMap) map; + Object confirmedFirst = confirmed.keySet().iterator().next(); + // access order, thus first in is now in last place + assertEquals(confirmedFirst, ordered.lastKey()); + } + + //----------------------------------------------------------------------- + public void testNextKey() { // override + resetEmpty(); + OrderedMap ordered = (OrderedMap) map; + assertEquals(null, ordered.nextKey(getOtherKeys()[0])); + if (isAllowNullKey() == false) { + try { + assertEquals(null, ordered.nextKey(null)); // this is allowed too + } catch (NullPointerException ex) {} + } else { + assertEquals(null, ordered.nextKey(null)); + } + + resetFull(); + ordered = (OrderedMap) map; + List list = new ArrayList(confirmed.keySet()); + Collections.reverse(list); // first into map is eldest + Iterator it = list.iterator(); + Object confirmedLast = it.next(); + while (it.hasNext()) { + Object confirmedObject = it.next(); + assertEquals(confirmedObject, ordered.nextKey(confirmedLast)); + confirmedLast = confirmedObject; + } + assertEquals(null, ordered.nextKey(confirmedLast)); + } + + public void testPreviousKey() { // override + resetEmpty(); + OrderedMap ordered = (OrderedMap) map; + assertEquals(null, ordered.previousKey(getOtherKeys()[0])); + if (isAllowNullKey() == false) { + try { + assertEquals(null, ordered.previousKey(null)); // this is allowed too + } catch (NullPointerException ex) {} + } else { + assertEquals(null, ordered.previousKey(null)); + } + + resetFull(); + ordered = (OrderedMap) map; + List list = new ArrayList(confirmed.keySet()); + Iterator it = list.iterator(); + Object confirmedLast = it.next(); + while (it.hasNext()) { + Object confirmedObject = it.next(); + assertEquals(confirmedObject, ordered.previousKey(confirmedLast)); + confirmedLast = confirmedObject; + } + assertEquals(null, ordered.previousKey(confirmedLast)); + } + +// public void testCreate() throws Exception { +// resetEmpty(); +// writeExternalFormToDisk((Serializable) map, "D:/dev/collections/data/test/LRUMap.emptyCollection.version3.obj"); +// resetFull(); +// writeExternalFormToDisk((Serializable) map, "D:/dev/collections/data/test/LRUMap.fullCollection.version3.obj"); +// } +}