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