From 60d03cea8f3bcb80e364099acc0b8d2598d51d11 Mon Sep 17 00:00:00 2001 From: Stephen Colebourne Date: Wed, 12 May 2004 19:55:56 +0000 Subject: [PATCH] Add behaviour to scan map to find a removable element when full bug 28887, from Mario Ivankovits git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/collections/trunk@131712 13f79535-47bb-0310-9956-ffa450edef68 --- RELEASE-NOTES.html | 1 + .../commons/collections/map/LRUMap.java | 89 +++++++++++++++++-- .../commons/collections/map/TestLRUMap.java | 55 +++++++++++- 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/RELEASE-NOTES.html b/RELEASE-NOTES.html index ababb70c6..44c2e06a6 100644 --- a/RELEASE-NOTES.html +++ b/RELEASE-NOTES.html @@ -56,6 +56,7 @@ No deprecations have occurred.
  • SingletonIterator - make remove() functionality optional
  • AbstractLinkedList/NodeCachingLinkedList - added getValue() and setValue() to Node, and made everything use them
  • LRUMap - The addMapping() method now uses isFull() to determine whether it is full
  • +
  • LRUMap - Add boolean flag, scanUntilRemovable, giving the removeLRU() method more power [28887]
  • Made Serializable

    diff --git a/src/java/org/apache/commons/collections/map/LRUMap.java b/src/java/org/apache/commons/collections/map/LRUMap.java index 9dfffa403..1ae92eafb 100644 --- a/src/java/org/apache/commons/collections/map/LRUMap.java +++ b/src/java/org/apache/commons/collections/map/LRUMap.java @@ -41,12 +41,13 @@ import org.apache.commons.collections.BoundedMap; * ResettableIterator and calling reset(). * * @since Commons Collections 3.0 (previously in main package v1.0) - * @version $Revision: 1.12 $ $Date: 2004/04/25 23:30:07 $ + * @version $Revision: 1.13 $ $Date: 2004/05/12 19:51:28 $ * * @author James Strachan * @author Morgan Delagrange * @author Stephen Colebourne * @author Mike Pettypiece + * @author Mario Ivankovits */ public class LRUMap extends AbstractLinkedMap implements BoundedMap, Serializable, Cloneable { @@ -55,15 +56,19 @@ public class LRUMap static final long serialVersionUID = -612114643488955218L; /** Default maximum size */ protected static final int DEFAULT_MAX_SIZE = 100; + /** Default scan behaviour */ + protected static final boolean DEFAULT_SCAN_UNTIL_REMOVABLE = false; /** Maximum size */ private transient int maxSize; + /** Scan behaviour */ + private boolean scanUntilRemovable; /** * Constructs a new empty map with a maximum size of 100. */ public LRUMap() { - this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR); + this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR, DEFAULT_SCAN_UNTIL_REMOVABLE); } /** @@ -76,6 +81,17 @@ public class LRUMap this(maxSize, DEFAULT_LOAD_FACTOR); } + /** + * Constructs a new, empty map with the specified maximum size. + * + * @param maxSize the maximum size of the map + * @param scanUntilRemovable scan until a removeable entry is found, default false + * @throws IllegalArgumentException if the maximum size is less than one + */ + public LRUMap(int maxSize, boolean scanUntilRemovable) { + this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable); + } + /** * Constructs a new, empty map with the specified initial capacity and * load factor. @@ -86,11 +102,26 @@ public class LRUMap * @throws IllegalArgumentException if the load factor is less than zero */ public LRUMap(int maxSize, float loadFactor) { + this(maxSize, loadFactor, DEFAULT_SCAN_UNTIL_REMOVABLE); + } + + /** + * 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 + * @param scanUntilRemovable scan until a removeable entry is found, default false + * @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, boolean scanUntilRemovable) { super((maxSize < 1 ? DEFAULT_CAPACITY : maxSize), loadFactor); if (maxSize < 1) { throw new IllegalArgumentException("LRUMap max size must be greater than 0"); } this.maxSize = maxSize; + this.scanUntilRemovable = scanUntilRemovable; } /** @@ -103,7 +134,21 @@ public class LRUMap * @throws IllegalArgumentException if the map is empty */ public LRUMap(Map map) { - this(map.size(), DEFAULT_LOAD_FACTOR); + this(map, DEFAULT_SCAN_UNTIL_REMOVABLE); + } + + /** + * Constructor copying elements from another map. + *

    + * The maximum size is set from the map's size. + * + * @param map the map to copy + * @param scanUntilRemovable scan until a removeable entry is found, default false + * @throws NullPointerException if the map is null + * @throws IllegalArgumentException if the map is empty + */ + public LRUMap(Map map, boolean scanUntilRemovable) { + this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable); putAll(map); } @@ -170,6 +215,7 @@ public class LRUMap *

    * From Commons Collections 3.1 this method uses {@link #isFull()} rather * than accessing size and maxSize directly. + * It also handles the scanUntilRemovable functionality. * * @param hashIndex the index into the data array to store at * @param hashCode the hash code of the key to add @@ -177,8 +223,26 @@ public class LRUMap * @param value the value to add */ protected void addMapping(int hashIndex, int hashCode, Object key, Object value) { - if (isFull() && removeLRU(header.after)) { - reuseMapping(header.after, hashIndex, hashCode, key, value); + if (isFull()) { + LinkEntry reuse = header.after; + boolean removeLRUEntry = false; + if (scanUntilRemovable) { + while (reuse != header) { + if (removeLRU(reuse)) { + removeLRUEntry = true; + break; + } + reuse = reuse.after; + } + } else { + removeLRUEntry = removeLRU(reuse); + } + + if (removeLRUEntry) { + reuseMapping(reuse, hashIndex, hashCode, key, value); + } else { + super.addMapping(hashIndex, hashCode, key, value); + } } else { super.addMapping(hashIndex, hashCode, key, value); } @@ -237,7 +301,10 @@ public class LRUMap * } * } * - * Note that the effect of not removing an LRU is for the Map to exceed the maximum size. + * The effect of returning false is dependent on the scanUntilRemovable flag. + * If the flag is true, the next LRU entry will be passed to this method and so on + * until one returns false and is removed, or every entry in the map has been passed. + * If the scanUntilRemovable flag is false, the map will exceed the maximum size. *

    * NOTE: Commons Collections 3.0 passed the wrong entry to this method. * This is fixed in version 3.1 onwards. @@ -267,6 +334,16 @@ public class LRUMap return maxSize; } + /** + * Whether this LRUMap will scan until a removable entry is found when the + * map is full. + * + * @return true if this map scans + */ + public boolean scanUntilRemovable() { + return scanUntilRemovable; + } + //----------------------------------------------------------------------- /** * Clones the map without cloning the keys or values. diff --git a/src/test/org/apache/commons/collections/map/TestLRUMap.java b/src/test/org/apache/commons/collections/map/TestLRUMap.java index 421afde5b..e6d8b3ec6 100644 --- a/src/test/org/apache/commons/collections/map/TestLRUMap.java +++ b/src/test/org/apache/commons/collections/map/TestLRUMap.java @@ -30,7 +30,7 @@ import org.apache.commons.collections.ResettableIterator; /** * JUnit tests. * - * @version $Revision: 1.7 $ $Date: 2004/04/16 23:53:59 $ + * @version $Revision: 1.8 $ $Date: 2004/05/12 19:51:28 $ * * @author Stephen Colebourne */ @@ -240,9 +240,11 @@ public class TestLRUMap extends AbstractTestOrderedMap { LinkEntry entry; Object key; Object value; + MockLRUMapSubclass(int size) { super(size); } + protected boolean removeLRU(LinkEntry entry) { this.entry = entry; this.key = entry.getKey(); @@ -252,7 +254,22 @@ public class TestLRUMap extends AbstractTestOrderedMap { } public void testRemoveLRUBlocksRemove() { - MockLRUMapSubclassBlocksRemove map = new MockLRUMapSubclassBlocksRemove(2); + MockLRUMapSubclassBlocksRemove map = new MockLRUMapSubclassBlocksRemove(2, false); + assertEquals(0, map.size()); + map.put("A", "a"); + assertEquals(1, map.size()); + map.put("B", "b"); + assertEquals(2, map.size()); + map.put("C", "c"); // should remove oldest, which is A=a, but this is blocked + assertEquals(3, map.size()); + assertEquals(2, map.maxSize()); + assertEquals(true, map.containsKey("A")); + assertEquals(true, map.containsKey("B")); + assertEquals(true, map.containsKey("C")); + } + + public void testRemoveLRUBlocksRemoveScan() { + MockLRUMapSubclassBlocksRemove map = new MockLRUMapSubclassBlocksRemove(2, true); assertEquals(0, map.size()); map.put("A", "a"); assertEquals(1, map.size()); @@ -267,14 +284,44 @@ public class TestLRUMap extends AbstractTestOrderedMap { } static class MockLRUMapSubclassBlocksRemove extends LRUMap { - MockLRUMapSubclassBlocksRemove(int size) { - super(size); + MockLRUMapSubclassBlocksRemove(int size, boolean scanUntilRemove) { + super(size, scanUntilRemove); } + protected boolean removeLRU(LinkEntry entry) { return false; } } + public void testRemoveLRUFirstBlocksRemove() { + MockLRUMapSubclassFirstBlocksRemove map = new MockLRUMapSubclassFirstBlocksRemove(2); + assertEquals(0, map.size()); + map.put("A", "a"); + assertEquals(1, map.size()); + map.put("B", "b"); + assertEquals(2, map.size()); + map.put("C", "c"); // should remove oldest, which is A=a but this is blocked - so advance to B=b + assertEquals(2, map.size()); + assertEquals(2, map.maxSize()); + assertEquals(true, map.containsKey("A")); + assertEquals(false, map.containsKey("B")); + assertEquals(true, map.containsKey("C")); + } + + static class MockLRUMapSubclassFirstBlocksRemove extends LRUMap { + MockLRUMapSubclassFirstBlocksRemove(int size) { + super(size, true); + } + + protected boolean removeLRU(LinkEntry entry) { + if ("a".equals(entry.getValue())) { + return false; + } else { + return true; + } + } + } + // public void testCreate() throws Exception { // resetEmpty(); // writeExternalFormToDisk((java.io.Serializable) map, "D:/dev/collections/data/test/LRUMap.emptyCollection.version3.obj");