001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.collections4.map; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.ObjectOutputStream; 022import java.io.Serializable; 023import java.util.Map; 024 025import org.apache.commons.collections4.BoundedMap; 026 027/** 028 * A <code>Map</code> implementation with a fixed maximum size which removes 029 * the least recently used entry if an entry is added when full. 030 * <p> 031 * The least recently used algorithm works on the get and put operations only. 032 * Iteration of any kind, including setting the value by iteration, does not 033 * change the order. Queries such as containsKey and containsValue or access 034 * via views also do not change the order. 035 * <p> 036 * A somewhat subtle ramification of the least recently used 037 * algorithm is that calls to {@link #get(Object)} stand a very good chance 038 * of modifying the map's iteration order and thus invalidating any 039 * iterators currently in use. It is therefore suggested that iterations 040 * over an {@link LRUMap} instance access entry values only through a 041 * {@link org.apache.commons.collections4.MapIterator MapIterator} or {@link #entrySet()} iterator. 042 * <p> 043 * The map implements <code>OrderedMap</code> and entries may be queried using 044 * the bidirectional <code>OrderedMapIterator</code>. The order returned is 045 * least recently used to most recently used. Iterators from map views can 046 * also be cast to <code>OrderedIterator</code> if required. 047 * <p> 048 * All the available iterators can be reset back to the start by casting to 049 * <code>ResettableIterator</code> and calling <code>reset()</code>. 050 * <p> 051 * <strong>Note that LRUMap is not synchronized and is not thread-safe.</strong> 052 * If you wish to use this map from multiple threads concurrently, you must use 053 * appropriate synchronization. The simplest approach is to wrap this map 054 * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw 055 * <code>NullPointerException</code>'s when accessed by concurrent threads. 056 * 057 * @param <K> the type of the keys in this map 058 * @param <V> the type of the values in this map 059 * @since 3.0 (previously in main package v1.0) 060 */ 061public class LRUMap<K, V> 062 extends AbstractLinkedMap<K, V> implements BoundedMap<K, V>, Serializable, Cloneable { 063 064 /** Serialisation version */ 065 private static final long serialVersionUID = -612114643488955218L; 066 /** Default maximum size */ 067 protected static final int DEFAULT_MAX_SIZE = 100; 068 069 /** Maximum size */ 070 private transient int maxSize; 071 /** Scan behaviour */ 072 private boolean scanUntilRemovable; 073 074 /** 075 * Constructs a new empty map with a maximum size of 100. 076 */ 077 public LRUMap() { 078 this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR, false); 079 } 080 081 /** 082 * Constructs a new, empty map with the specified maximum size. 083 * 084 * @param maxSize the maximum size of the map 085 * @throws IllegalArgumentException if the maximum size is less than one 086 */ 087 public LRUMap(final int maxSize) { 088 this(maxSize, DEFAULT_LOAD_FACTOR); 089 } 090 091 /** 092 * Constructs a new, empty map with the specified maximum size. 093 * 094 * @param maxSize the maximum size of the map 095 * @param initialSize the initial size of the map 096 * @throws IllegalArgumentException if the maximum size is less than one 097 * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size 098 * @since 4.1 099 */ 100 public LRUMap(final int maxSize, final int initialSize) { 101 this(maxSize, initialSize, DEFAULT_LOAD_FACTOR); 102 } 103 104 /** 105 * Constructs a new, empty map with the specified maximum size. 106 * 107 * @param maxSize the maximum size of the map 108 * @param scanUntilRemovable scan until a removeable entry is found, default false 109 * @throws IllegalArgumentException if the maximum size is less than one 110 * @since 3.1 111 */ 112 public LRUMap(final int maxSize, final boolean scanUntilRemovable) { 113 this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable); 114 } 115 116 /** 117 * Constructs a new, empty map with the specified max capacity and 118 * load factor. 119 * 120 * @param maxSize the maximum size of the map 121 * @param loadFactor the load factor 122 * @throws IllegalArgumentException if the maximum size is less than one 123 * @throws IllegalArgumentException if the load factor is less than zero 124 */ 125 public LRUMap(final int maxSize, final float loadFactor) { 126 this(maxSize, loadFactor, false); 127 } 128 129 /** 130 * Constructs a new, empty map with the specified max / initial capacity and 131 * load factor. 132 * 133 * @param maxSize the maximum size of the map 134 * @param initialSize the initial size of the map 135 * @param loadFactor the load factor 136 * @throws IllegalArgumentException if the maximum size is less than one 137 * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size 138 * @throws IllegalArgumentException if the load factor is less than zero 139 * @since 4.1 140 */ 141 public LRUMap(final int maxSize, final int initialSize, final float loadFactor) { 142 this(maxSize, initialSize, loadFactor, false); 143 } 144 145 /** 146 * Constructs a new, empty map with the specified max capacity and load factor. 147 * 148 * @param maxSize the maximum size of the map 149 * @param loadFactor the load factor 150 * @param scanUntilRemovable scan until a removeable entry is found, default false 151 * @throws IllegalArgumentException if the maximum size is less than one 152 * @throws IllegalArgumentException if the load factor is less than zero 153 * @since 3.1 154 */ 155 public LRUMap(final int maxSize, final float loadFactor, final boolean scanUntilRemovable) { 156 this(maxSize, maxSize, loadFactor, scanUntilRemovable); 157 } 158 159 /** 160 * Constructs a new, empty map with the specified max / initial capacity and load factor. 161 * 162 * @param maxSize the maximum size of the map 163 * @param initialSize the initial size of the map 164 * @param loadFactor the load factor 165 * @param scanUntilRemovable scan until a removeable entry is found, default false 166 * @throws IllegalArgumentException if the maximum size is less than one 167 * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size 168 * @throws IllegalArgumentException if the load factor is less than zero 169 * @since 4.1 170 */ 171 public LRUMap(final int maxSize, 172 final int initialSize, 173 final float loadFactor, 174 final boolean scanUntilRemovable) { 175 176 super(initialSize, loadFactor); 177 if (maxSize < 1) { 178 throw new IllegalArgumentException("LRUMap max size must be greater than 0"); 179 } 180 if (initialSize > maxSize) { 181 throw new IllegalArgumentException("LRUMap initial size must not be greather than max size"); 182 } 183 this.maxSize = maxSize; 184 this.scanUntilRemovable = scanUntilRemovable; 185 } 186 187 /** 188 * Constructor copying elements from another map. 189 * <p> 190 * The maximum size is set from the map's size. 191 * 192 * @param map the map to copy 193 * @throws NullPointerException if the map is null 194 * @throws IllegalArgumentException if the map is empty 195 */ 196 public LRUMap(final Map<? extends K, ? extends V> map) { 197 this(map, false); 198 } 199 200 /** 201 * Constructor copying elements from another map. 202 * 203 * <p>The maximum size is set from the map's size.</p> 204 * 205 * @param map the map to copy 206 * @param scanUntilRemovable scan until a removeable entry is found, default false 207 * @throws NullPointerException if the map is null 208 * @throws IllegalArgumentException if the map is empty 209 * @since 3.1 210 */ 211 public LRUMap(final Map<? extends K, ? extends V> map, final boolean scanUntilRemovable) { 212 this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable); 213 putAll(map); 214 } 215 216 //----------------------------------------------------------------------- 217 /** 218 * Gets the value mapped to the key specified. 219 * <p> 220 * This operation changes the position of the key in the map to the 221 * most recently used position (last). 222 * 223 * @param key the key 224 * @return the mapped value, null if no match 225 */ 226 @Override 227 public V get(final Object key) { 228 return get(key, true); 229 } 230 231 /** 232 * Gets the value mapped to the key specified. 233 * <p> 234 * If {@code updateToMRU} is {@code true}, the position of the key in the map 235 * is changed to the most recently used position (last), otherwise the iteration 236 * order is not changed by this operation. 237 * 238 * @param key the key 239 * @param updateToMRU whether the key shall be updated to the 240 * most recently used position 241 * @return the mapped value, null if no match 242 * @since 4.1 243 */ 244 public V get(final Object key, final boolean updateToMRU) { 245 final LinkEntry<K, V> entry = getEntry(key); 246 if (entry == null) { 247 return null; 248 } 249 if (updateToMRU) { 250 moveToMRU(entry); 251 } 252 return entry.getValue(); 253 } 254 255 //----------------------------------------------------------------------- 256 /** 257 * Moves an entry to the MRU position at the end of the list. 258 * <p> 259 * This implementation moves the updated entry to the end of the list. 260 * 261 * @param entry the entry to update 262 */ 263 protected void moveToMRU(final LinkEntry<K, V> entry) { 264 if (entry.after != header) { 265 modCount++; 266 // remove 267 if(entry.before == null) { 268 throw new IllegalStateException("Entry.before is null." + 269 " Please check that your keys are immutable, and that you have used synchronization properly." + 270 " If so, then please report this to dev@commons.apache.org as a bug."); 271 } 272 entry.before.after = entry.after; 273 entry.after.before = entry.before; 274 // add first 275 entry.after = header; 276 entry.before = header.before; 277 header.before.after = entry; 278 header.before = entry; 279 } else if (entry == header) { 280 throw new IllegalStateException("Can't move header to MRU" + 281 " (please report this to dev@commons.apache.org)"); 282 } 283 } 284 285 /** 286 * Updates an existing key-value mapping. 287 * <p> 288 * This implementation moves the updated entry to the end of the list 289 * using {@link #moveToMRU(AbstractLinkedMap.LinkEntry)}. 290 * 291 * @param entry the entry to update 292 * @param newValue the new value to store 293 */ 294 @Override 295 protected void updateEntry(final HashEntry<K, V> entry, final V newValue) { 296 moveToMRU((LinkEntry<K, V>) entry); // handles modCount 297 entry.setValue(newValue); 298 } 299 300 /** 301 * Adds a new key-value mapping into this map. 302 * <p> 303 * This implementation checks the LRU size and determines whether to 304 * discard an entry or not using {@link #removeLRU(AbstractLinkedMap.LinkEntry)}. 305 * <p> 306 * From Commons Collections 3.1 this method uses {@link #isFull()} rather 307 * than accessing <code>size</code> and <code>maxSize</code> directly. 308 * It also handles the scanUntilRemovable functionality. 309 * 310 * @param hashIndex the index into the data array to store at 311 * @param hashCode the hash code of the key to add 312 * @param key the key to add 313 * @param value the value to add 314 */ 315 @Override 316 protected void addMapping(final int hashIndex, final int hashCode, final K key, final V value) { 317 if (isFull()) { 318 LinkEntry<K, V> reuse = header.after; 319 boolean removeLRUEntry = false; 320 if (scanUntilRemovable) { 321 while (reuse != header && reuse != null) { 322 if (removeLRU(reuse)) { 323 removeLRUEntry = true; 324 break; 325 } 326 reuse = reuse.after; 327 } 328 if (reuse == null) { 329 throw new IllegalStateException( 330 "Entry.after=null, header.after" + header.after + " header.before" + header.before + 331 " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize + 332 " Please check that your keys are immutable, and that you have used synchronization properly." + 333 " If so, then please report this to dev@commons.apache.org as a bug."); 334 } 335 } else { 336 removeLRUEntry = removeLRU(reuse); 337 } 338 339 if (removeLRUEntry) { 340 if (reuse == null) { 341 throw new IllegalStateException( 342 "reuse=null, header.after=" + header.after + " header.before" + header.before + 343 " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize + 344 " Please check that your keys are immutable, and that you have used synchronization properly." + 345 " If so, then please report this to dev@commons.apache.org as a bug."); 346 } 347 reuseMapping(reuse, hashIndex, hashCode, key, value); 348 } else { 349 super.addMapping(hashIndex, hashCode, key, value); 350 } 351 } else { 352 super.addMapping(hashIndex, hashCode, key, value); 353 } 354 } 355 356 /** 357 * Reuses an entry by removing it and moving it to a new place in the map. 358 * <p> 359 * This method uses {@link #removeEntry}, {@link #reuseEntry} and {@link #addEntry}. 360 * 361 * @param entry the entry to reuse 362 * @param hashIndex the index into the data array to store at 363 * @param hashCode the hash code of the key to add 364 * @param key the key to add 365 * @param value the value to add 366 */ 367 protected void reuseMapping(final LinkEntry<K, V> entry, final int hashIndex, final int hashCode, 368 final K key, final V value) { 369 // find the entry before the entry specified in the hash table 370 // remember that the parameters (except the first) refer to the new entry, 371 // not the old one 372 try { 373 final int removeIndex = hashIndex(entry.hashCode, data.length); 374 final HashEntry<K, V>[] tmp = data; // may protect against some sync issues 375 HashEntry<K, V> loop = tmp[removeIndex]; 376 HashEntry<K, V> previous = null; 377 while (loop != entry && loop != null) { 378 previous = loop; 379 loop = loop.next; 380 } 381 if (loop == null) { 382 throw new IllegalStateException( 383 "Entry.next=null, data[removeIndex]=" + data[removeIndex] + " previous=" + previous + 384 " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize + 385 " Please check that your keys are immutable, and that you have used synchronization properly." + 386 " If so, then please report this to dev@commons.apache.org as a bug."); 387 } 388 389 // reuse the entry 390 modCount++; 391 removeEntry(entry, removeIndex, previous); 392 reuseEntry(entry, hashIndex, hashCode, key, value); 393 addEntry(entry, hashIndex); 394 } catch (final NullPointerException ex) { 395 throw new IllegalStateException( 396 "NPE, entry=" + entry + " entryIsHeader=" + (entry==header) + 397 " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize + 398 " Please check that your keys are immutable, and that you have used synchronization properly." + 399 " If so, then please report this to dev@commons.apache.org as a bug."); 400 } 401 } 402 403 /** 404 * Subclass method to control removal of the least recently used entry from the map. 405 * <p> 406 * This method exists for subclasses to override. A subclass may wish to 407 * provide cleanup of resources when an entry is removed. For example: 408 * <pre> 409 * protected boolean removeLRU(LinkEntry entry) { 410 * releaseResources(entry.getValue()); // release resources held by entry 411 * return true; // actually delete entry 412 * } 413 * </pre> 414 * <p> 415 * Alternatively, a subclass may choose to not remove the entry or selectively 416 * keep certain LRU entries. For example: 417 * <pre> 418 * protected boolean removeLRU(LinkEntry entry) { 419 * if (entry.getKey().toString().startsWith("System.")) { 420 * return false; // entry not removed from LRUMap 421 * } else { 422 * return true; // actually delete entry 423 * } 424 * } 425 * </pre> 426 * The effect of returning false is dependent on the scanUntilRemovable flag. 427 * If the flag is true, the next LRU entry will be passed to this method and so on 428 * until one returns false and is removed, or every entry in the map has been passed. 429 * If the scanUntilRemovable flag is false, the map will exceed the maximum size. 430 * <p> 431 * NOTE: Commons Collections 3.0 passed the wrong entry to this method. 432 * This is fixed in version 3.1 onwards. 433 * 434 * @param entry the entry to be removed 435 * @return {@code true} 436 */ 437 protected boolean removeLRU(final LinkEntry<K, V> entry) { 438 return true; 439 } 440 441 //----------------------------------------------------------------------- 442 /** 443 * Returns true if this map is full and no new mappings can be added. 444 * 445 * @return <code>true</code> if the map is full 446 */ 447 @Override 448 public boolean isFull() { 449 return size >= maxSize; 450 } 451 452 /** 453 * Gets the maximum size of the map (the bound). 454 * 455 * @return the maximum number of elements the map can hold 456 */ 457 @Override 458 public int maxSize() { 459 return maxSize; 460 } 461 462 /** 463 * Whether this LRUMap will scan until a removable entry is found when the 464 * map is full. 465 * 466 * @return true if this map scans 467 * @since 3.1 468 */ 469 public boolean isScanUntilRemovable() { 470 return scanUntilRemovable; 471 } 472 473 //----------------------------------------------------------------------- 474 /** 475 * Clones the map without cloning the keys or values. 476 * 477 * @return a shallow clone 478 */ 479 @Override 480 public LRUMap<K, V> clone() { 481 return (LRUMap<K, V>) super.clone(); 482 } 483 484 /** 485 * Write the map out using a custom routine. 486 * 487 * @param out the output stream 488 * @throws IOException if an error occurs while writing to the stream 489 */ 490 private void writeObject(final ObjectOutputStream out) throws IOException { 491 out.defaultWriteObject(); 492 doWriteObject(out); 493 } 494 495 /** 496 * Read the map in using a custom routine. 497 * 498 * @param in the input stream 499 * @throws IOException if an error occurs while reading from the stream 500 * @throws ClassNotFoundException if an object read from the stream can not be loaded 501 */ 502 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 503 in.defaultReadObject(); 504 doReadObject(in); 505 } 506 507 /** 508 * Writes the data necessary for <code>put()</code> to work in deserialization. 509 * 510 * @param out the output stream 511 * @throws IOException if an error occurs while writing to the stream 512 */ 513 @Override 514 protected void doWriteObject(final ObjectOutputStream out) throws IOException { 515 out.writeInt(maxSize); 516 super.doWriteObject(out); 517 } 518 519 /** 520 * Reads the data necessary for <code>put()</code> to work in the superclass. 521 * 522 * @param in the input stream 523 * @throws IOException if an error occurs while reading from the stream 524 * @throws ClassNotFoundException if an object read from the stream can not be loaded 525 */ 526 @Override 527 protected void doReadObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 528 maxSize = in.readInt(); 529 super.doReadObject(in); 530 } 531 532}