View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections.map;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.ObjectOutputStream;
22  import java.io.Serializable;
23  import java.util.Map;
24  
25  import org.apache.commons.collections.BoundedMap;
26  
27  /**
28   * A <code>Map</code> implementation with a fixed maximum size which removes
29   * the least recently used entry if an entry is added when full.
30   * <p>
31   * The least recently used algorithm works on the get and put operations only.
32   * Iteration of any kind, including setting the value by iteration, does not
33   * change the order. Queries such as containsKey and containsValue or access
34   * via views also do not change the order.
35   * <p>
36   * A somewhat subtle ramification of the least recently used
37   * algorithm is that calls to {@link #get(Object)} stand a very good chance
38   * of modifying the map's iteration order and thus invalidating any
39   * iterators currently in use.  It is therefore suggested that iterations
40   * over an {@link LRUMap} instance access entry values only through a
41   * {@link org.apache.commons.collections.MapIterator MapIterator} or {@link #entrySet()} iterator.
42   * <p>
43   * The map implements <code>OrderedMap</code> and entries may be queried using
44   * the bidirectional <code>OrderedMapIterator</code>. The order returned is
45   * least recently used to most recently used. Iterators from map views can
46   * also be cast to <code>OrderedIterator</code> if required.
47   * <p>
48   * All the available iterators can be reset back to the start by casting to
49   * <code>ResettableIterator</code> and calling <code>reset()</code>.
50   * <p>
51   * <strong>Note that LRUMap is not synchronized and is not thread-safe.</strong>
52   * If you wish to use this map from multiple threads concurrently, you must use
53   * appropriate synchronization. The simplest approach is to wrap this map
54   * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw
55   * <code>NullPointerException</code>'s when accessed by concurrent threads.
56   *
57   * @since 3.0 (previously in main package v1.0)
58   * @version $Id: LRUMap.java 1436463 2013-01-21 16:35:01Z tn $
59   */
60  public class LRUMap<K, V>
61          extends AbstractLinkedMap<K, V> implements BoundedMap<K, V>, Serializable, Cloneable {
62  
63      /** Serialisation version */
64      private static final long serialVersionUID = -612114643488955218L;
65      /** Default maximum size */
66      protected static final int DEFAULT_MAX_SIZE = 100;
67  
68      /** Maximum size */
69      private transient int maxSize;
70      /** Scan behaviour */
71      private boolean scanUntilRemovable;
72  
73      /**
74       * Constructs a new empty map with a maximum size of 100.
75       */
76      public LRUMap() {
77          this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR, false);
78      }
79  
80      /**
81       * Constructs a new, empty map with the specified maximum size.
82       *
83       * @param maxSize  the maximum size of the map
84       * @throws IllegalArgumentException if the maximum size is less than one
85       */
86      public LRUMap(final int maxSize) {
87          this(maxSize, DEFAULT_LOAD_FACTOR);
88      }
89  
90      /**
91       * Constructs a new, empty map with the specified maximum size.
92       *
93       * @param maxSize  the maximum size of the map
94       * @param scanUntilRemovable  scan until a removeable entry is found, default false
95       * @throws IllegalArgumentException if the maximum size is less than one
96       * @since 3.1
97       */
98      public LRUMap(final int maxSize, final boolean scanUntilRemovable) {
99          this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable);
100     }
101 
102     /**
103      * Constructs a new, empty map with the specified initial capacity and
104      * load factor.
105      *
106      * @param maxSize  the maximum size of the map
107      * @param loadFactor  the load factor
108      * @throws IllegalArgumentException if the maximum size is less than one
109      * @throws IllegalArgumentException if the load factor is less than zero
110      */
111     public LRUMap(final int maxSize, final float loadFactor) {
112         this(maxSize, loadFactor, false);
113     }
114 
115     /**
116      * Constructs a new, empty map with the specified initial capacity and
117      * load factor.
118      *
119      * @param maxSize  the maximum size of the ma
120      * @param loadFactor  the load factor
121      * @param scanUntilRemovable  scan until a removeable entry is found, default false
122      * @throws IllegalArgumentException if the maximum size is less than one
123      * @throws IllegalArgumentException if the load factor is less than zero
124      * @since 3.1
125      */
126     public LRUMap(final int maxSize, final float loadFactor, final boolean scanUntilRemovable) {
127         super(maxSize < 1 ? DEFAULT_CAPACITY : maxSize, loadFactor);
128         if (maxSize < 1) {
129             throw new IllegalArgumentException("LRUMap max size must be greater than 0");
130         }
131         this.maxSize = maxSize;
132         this.scanUntilRemovable = scanUntilRemovable;
133     }
134 
135     /**
136      * Constructor copying elements from another map.
137      * <p>
138      * The maximum size is set from the map's size.
139      *
140      * @param map  the map to copy
141      * @throws NullPointerException if the map is null
142      * @throws IllegalArgumentException if the map is empty
143      */
144     public LRUMap(final Map<K, V> map) {
145         this(map, false);
146     }
147 
148     /**
149      * Constructor copying elements from another map.
150      * <p/>
151      * The maximum size is set from the map's size.
152      *
153      * @param map  the map to copy
154      * @param scanUntilRemovable  scan until a removeable entry is found, default false
155      * @throws NullPointerException if the map is null
156      * @throws IllegalArgumentException if the map is empty
157      * @since 3.1
158      */
159     public LRUMap(final Map<K, V> map, final boolean scanUntilRemovable) {
160         this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable);
161         putAll(map);
162     }
163 
164     //-----------------------------------------------------------------------
165     /**
166      * Gets the value mapped to the key specified.
167      * <p>
168      * This operation changes the position of the key in the map to the
169      * most recently used position (first).
170      *
171      * @param key  the key
172      * @return the mapped value, null if no match
173      */
174     @Override
175     public V get(final Object key) {
176         final LinkEntry<K, V> entry = getEntry(key);
177         if (entry == null) {
178             return null;
179         }
180         moveToMRU(entry);
181         return entry.getValue();
182     }
183 
184     //-----------------------------------------------------------------------
185     /**
186      * Moves an entry to the MRU position at the end of the list.
187      * <p>
188      * This implementation moves the updated entry to the end of the list.
189      *
190      * @param entry  the entry to update
191      */
192     protected void moveToMRU(final LinkEntry<K, V> entry) {
193         if (entry.after != header) {
194             modCount++;
195             // remove
196             if(entry.before == null) {
197                 throw new IllegalStateException("Entry.before is null." + 
198                     " Please check that your keys are immutable, and that you have used synchronization properly." +
199                     " If so, then please report this to dev@commons.apache.org as a bug.");
200             }
201             entry.before.after = entry.after;
202             entry.after.before = entry.before;
203             // add first
204             entry.after = header;
205             entry.before = header.before;
206             header.before.after = entry;
207             header.before = entry;
208         } else if (entry == header) {
209             throw new IllegalStateException("Can't move header to MRU" +
210                 " (please report this to dev@commons.apache.org)");
211         }
212     }
213 
214     /**
215      * Updates an existing key-value mapping.
216      * <p>
217      * This implementation moves the updated entry to the top of the list
218      * using {@link #moveToMRU(AbstractLinkedMap.LinkEntry)}.
219      *
220      * @param entry  the entry to update
221      * @param newValue  the new value to store
222      */
223     @Override
224     protected void updateEntry(final HashEntry<K, V> entry, final V newValue) {
225         moveToMRU((LinkEntry<K, V>) entry);  // handles modCount
226         entry.setValue(newValue);
227     }
228 
229     /**
230      * Adds a new key-value mapping into this map.
231      * <p>
232      * This implementation checks the LRU size and determines whether to
233      * discard an entry or not using {@link #removeLRU(AbstractLinkedMap.LinkEntry)}.
234      * <p>
235      * From Commons Collections 3.1 this method uses {@link #isFull()} rather
236      * than accessing <code>size</code> and <code>maxSize</code> directly.
237      * It also handles the scanUntilRemovable functionality.
238      *
239      * @param hashIndex  the index into the data array to store at
240      * @param hashCode  the hash code of the key to add
241      * @param key  the key to add
242      * @param value  the value to add
243      */
244     @Override
245     protected void addMapping(final int hashIndex, final int hashCode, final K key, final V value) {
246         if (isFull()) {
247             LinkEntry<K, V> reuse = header.after;
248             boolean removeLRUEntry = false;
249             if (scanUntilRemovable) {
250                 while (reuse != header && reuse != null) {
251                     if (removeLRU(reuse)) {
252                         removeLRUEntry = true;
253                         break;
254                     }
255                     reuse = reuse.after;
256                 }
257                 if (reuse == null) {
258                     throw new IllegalStateException(
259                         "Entry.after=null, header.after" + header.after + " header.before" + header.before +
260                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
261                         " Please check that your keys are immutable, and that you have used synchronization properly." +
262                         " If so, then please report this to dev@commons.apache.org as a bug.");
263                 }
264             } else {
265                 removeLRUEntry = removeLRU(reuse);
266             }
267 
268             if (removeLRUEntry) {
269                 if (reuse == null) {
270                     throw new IllegalStateException(
271                         "reuse=null, header.after=" + header.after + " header.before" + header.before +
272                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
273                         " Please check that your keys are immutable, and that you have used synchronization properly." +
274                         " If so, then please report this to dev@commons.apache.org as a bug.");
275                 }
276                 reuseMapping(reuse, hashIndex, hashCode, key, value);
277             } else {
278                 super.addMapping(hashIndex, hashCode, key, value);
279             }
280         } else {
281             super.addMapping(hashIndex, hashCode, key, value);
282         }
283     }
284 
285     /**
286      * Reuses an entry by removing it and moving it to a new place in the map.
287      * <p>
288      * This method uses {@link #removeEntry}, {@link #reuseEntry} and {@link #addEntry}.
289      *
290      * @param entry  the entry to reuse
291      * @param hashIndex  the index into the data array to store at
292      * @param hashCode  the hash code of the key to add
293      * @param key  the key to add
294      * @param value  the value to add
295      */
296     protected void reuseMapping(final LinkEntry<K, V> entry, final int hashIndex, final int hashCode,
297                                 final K key, final V value) {
298         // find the entry before the entry specified in the hash table
299         // remember that the parameters (except the first) refer to the new entry,
300         // not the old one
301         try {
302             final int removeIndex = hashIndex(entry.hashCode, data.length);
303             final HashEntry<K, V>[] tmp = data;  // may protect against some sync issues
304             HashEntry<K, V> loop = tmp[removeIndex];
305             HashEntry<K, V> previous = null;
306             while (loop != entry && loop != null) {
307                 previous = loop;
308                 loop = loop.next;
309             }
310             if (loop == null) {
311                 throw new IllegalStateException(
312                     "Entry.next=null, data[removeIndex]=" + data[removeIndex] + " previous=" + previous +
313                     " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
314                     " Please check that your keys are immutable, and that you have used synchronization properly." +
315                     " If so, then please report this to dev@commons.apache.org as a bug.");
316             }
317 
318             // reuse the entry
319             modCount++;
320             removeEntry(entry, removeIndex, previous);
321             reuseEntry(entry, hashIndex, hashCode, key, value);
322             addEntry(entry, hashIndex);
323         } catch (final NullPointerException ex) {
324             throw new IllegalStateException(
325                     "NPE, entry=" + entry + " entryIsHeader=" + (entry==header) +
326                     " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
327                     " Please check that your keys are immutable, and that you have used synchronization properly." +
328                     " If so, then please report this to dev@commons.apache.org as a bug.");
329         }
330     }
331 
332     /**
333      * Subclass method to control removal of the least recently used entry from the map.
334      * <p>
335      * This method exists for subclasses to override. A subclass may wish to
336      * provide cleanup of resources when an entry is removed. For example:
337      * <pre>
338      * protected boolean removeLRU(LinkEntry entry) {
339      *   releaseResources(entry.getValue());  // release resources held by entry
340      *   return true;  // actually delete entry
341      * }
342      * </pre>
343      * <p>
344      * Alternatively, a subclass may choose to not remove the entry or selectively
345      * keep certain LRU entries. For example:
346      * <pre>
347      * protected boolean removeLRU(LinkEntry entry) {
348      *   if (entry.getKey().toString().startsWith("System.")) {
349      *     return false;  // entry not removed from LRUMap
350      *   } else {
351      *     return true;  // actually delete entry
352      *   }
353      * }
354      * </pre>
355      * The effect of returning false is dependent on the scanUntilRemovable flag.
356      * If the flag is true, the next LRU entry will be passed to this method and so on
357      * until one returns false and is removed, or every entry in the map has been passed.
358      * If the scanUntilRemovable flag is false, the map will exceed the maximum size.
359      * <p>
360      * NOTE: Commons Collections 3.0 passed the wrong entry to this method.
361      * This is fixed in version 3.1 onwards.
362      *
363      * @param entry  the entry to be removed
364      * @return {@code true}
365      */
366     protected boolean removeLRU(final LinkEntry<K, V> entry) {
367         return true;
368     }
369 
370     //-----------------------------------------------------------------------
371     /**
372      * Returns true if this map is full and no new mappings can be added.
373      *
374      * @return <code>true</code> if the map is full
375      */
376     public boolean isFull() {
377         return size >= maxSize;
378     }
379 
380     /**
381      * Gets the maximum size of the map (the bound).
382      *
383      * @return the maximum number of elements the map can hold
384      */
385     public int maxSize() {
386         return maxSize;
387     }
388 
389     /**
390      * Whether this LRUMap will scan until a removable entry is found when the
391      * map is full.
392      *
393      * @return true if this map scans
394      * @since 3.1
395      */
396     public boolean isScanUntilRemovable() {
397         return scanUntilRemovable;
398     }
399 
400     //-----------------------------------------------------------------------
401     /**
402      * Clones the map without cloning the keys or values.
403      *
404      * @return a shallow clone
405      */
406     @Override
407     public LRUMap<K, V> clone() {
408         return (LRUMap<K, V>) super.clone();
409     }
410 
411     /**
412      * Write the map out using a custom routine.
413      */
414     private void writeObject(final ObjectOutputStream out) throws IOException {
415         out.defaultWriteObject();
416         doWriteObject(out);
417     }
418 
419     /**
420      * Read the map in using a custom routine.
421      */
422     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
423         in.defaultReadObject();
424         doReadObject(in);
425     }
426 
427     /**
428      * Writes the data necessary for <code>put()</code> to work in deserialization.
429      * 
430      * @param out  the output stream
431      * @throws IOException if an error occurs while writing to the stream
432      */
433     @Override
434     protected void doWriteObject(final ObjectOutputStream out) throws IOException {
435         out.writeInt(maxSize);
436         super.doWriteObject(out);
437     }
438 
439     /**
440      * Reads the data necessary for <code>put()</code> to work in the superclass.
441      *
442      * @param in  the input stream
443      * @throws IOException if an error occurs while reading from the stream
444      * @throws ClassNotFoundException if an object read from the stream can not be loaded
445      */
446     @Override
447     protected void doReadObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
448         maxSize = in.readInt();
449         super.doReadObject(in);
450     }
451 
452 }