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 }