LRUMap.java

  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.collections4.map;

  18. import java.io.IOException;
  19. import java.io.ObjectInputStream;
  20. import java.io.ObjectOutputStream;
  21. import java.io.Serializable;
  22. import java.util.Map;

  23. import org.apache.commons.collections4.BoundedMap;

  24. /**
  25.  * A {@code Map} implementation with a fixed maximum size which removes
  26.  * the least recently used entry if an entry is added when full.
  27.  * <p>
  28.  * The least recently used algorithm works on the get and put operations only.
  29.  * Iteration of any kind, including setting the value by iteration, does not
  30.  * change the order. Queries such as containsKey and containsValue or access
  31.  * via views also do not change the order.
  32.  * </p>
  33.  * <p>
  34.  * A somewhat subtle ramification of the least recently used
  35.  * algorithm is that calls to {@link #get(Object)} stand a very good chance
  36.  * of modifying the map's iteration order and thus invalidating any
  37.  * iterators currently in use.  It is therefore suggested that iterations
  38.  * over an {@link LRUMap} instance access entry values only through a
  39.  * {@link org.apache.commons.collections4.MapIterator MapIterator} or {@link #entrySet()} iterator.
  40.  * </p>
  41.  * <p>
  42.  * The map implements {@code OrderedMap} and entries may be queried using
  43.  * the bidirectional {@code OrderedMapIterator}. The order returned is
  44.  * least recently used to most recently used. Iterators from map views can
  45.  * also be cast to {@code OrderedIterator} if required.
  46.  * </p>
  47.  * <p>
  48.  * All the available iterators can be reset back to the start by casting to
  49.  * {@code ResettableIterator} and calling {@code reset()}.
  50.  * </p>
  51.  * <p>
  52.  * <strong>Note that LRUMap is not synchronized and is not thread-safe.</strong>
  53.  * If you wish to use this map from multiple threads concurrently, you must use
  54.  * appropriate synchronization. The simplest approach is to wrap this map
  55.  * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw
  56.  * {@code NullPointerException}'s when accessed by concurrent threads.
  57.  * </p>
  58.  *
  59.  * @param <K> the type of the keys in this map
  60.  * @param <V> the type of the values in this map
  61.  * @since 3.0 (previously in main package v1.0)
  62.  */
  63. public class LRUMap<K, V>
  64.         extends AbstractLinkedMap<K, V> implements BoundedMap<K, V>, Serializable, Cloneable {

  65.     /** Serialization version */
  66.     private static final long serialVersionUID = -612114643488955218L;
  67.     /** Default maximum size */
  68.     protected static final int DEFAULT_MAX_SIZE = 100;

  69.     /** Maximum size */
  70.     private transient int maxSize;
  71.     /** Scan behavior */
  72.     private final boolean scanUntilRemovable;

  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.      * Constructs a new, empty map with the specified maximum size.
  81.      *
  82.      * @param maxSize  the maximum size of the map
  83.      * @throws IllegalArgumentException if the maximum size is less than one
  84.      */
  85.     public LRUMap(final int maxSize) {
  86.         this(maxSize, DEFAULT_LOAD_FACTOR);
  87.     }

  88.     /**
  89.      * Constructs a new, empty map with the specified maximum size.
  90.      *
  91.      * @param maxSize  the maximum size of the map
  92.      * @param scanUntilRemovable  scan until a removable entry is found, default false
  93.      * @throws IllegalArgumentException if the maximum size is less than one
  94.      * @since 3.1
  95.      */
  96.     public LRUMap(final int maxSize, final boolean scanUntilRemovable) {
  97.         this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable);
  98.     }

  99.     /**
  100.      * Constructs a new, empty map with the specified max capacity and
  101.      * load factor.
  102.      *
  103.      * @param maxSize  the maximum size of the map
  104.      * @param loadFactor  the load factor
  105.      * @throws IllegalArgumentException if the maximum size is less than one
  106.      * @throws IllegalArgumentException if the load factor is less than zero
  107.      */
  108.     public LRUMap(final int maxSize, final float loadFactor) {
  109.         this(maxSize, loadFactor, false);
  110.     }

  111.     /**
  112.      * Constructs a new, empty map with the specified max capacity and load factor.
  113.      *
  114.      * @param maxSize  the maximum size of the map
  115.      * @param loadFactor  the load factor
  116.      * @param scanUntilRemovable  scan until a removable entry is found, default false
  117.      * @throws IllegalArgumentException if the maximum size is less than one
  118.      * @throws IllegalArgumentException if the load factor is less than zero
  119.      * @since 3.1
  120.      */
  121.     public LRUMap(final int maxSize, final float loadFactor, final boolean scanUntilRemovable) {
  122.         this(maxSize, maxSize, loadFactor, scanUntilRemovable);
  123.     }

  124.     /**
  125.      * Constructs a new, empty map with the specified maximum size.
  126.      *
  127.      * @param maxSize  the maximum size of the map
  128.      * @param initialSize  the initial size of the map
  129.      * @throws IllegalArgumentException if the maximum size is less than one
  130.      * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size
  131.      * @since 4.1
  132.      */
  133.     public LRUMap(final int maxSize, final int initialSize) {
  134.         this(maxSize, initialSize, DEFAULT_LOAD_FACTOR);
  135.     }

  136.     /**
  137.      * Constructs a new, empty map with the specified max / initial capacity and
  138.      * load factor.
  139.      *
  140.      * @param maxSize  the maximum size of the map
  141.      * @param initialSize  the initial size of the map
  142.      * @param loadFactor  the load factor
  143.      * @throws IllegalArgumentException if the maximum size is less than one
  144.      * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size
  145.      * @throws IllegalArgumentException if the load factor is less than zero
  146.      * @since 4.1
  147.      */
  148.     public LRUMap(final int maxSize, final int initialSize, final float loadFactor) {
  149.         this(maxSize, initialSize, loadFactor, false);
  150.     }

  151.     /**
  152.      * Constructs a new, empty map with the specified max / initial capacity and load factor.
  153.      *
  154.      * @param maxSize  the maximum size of the map
  155.      * @param initialSize  the initial size of the map
  156.      * @param loadFactor  the load factor
  157.      * @param scanUntilRemovable  scan until a removable entry is found, default false
  158.      * @throws IllegalArgumentException if the maximum size is less than one
  159.      * @throws IllegalArgumentException if the initial size is negative or larger than the maximum size
  160.      * @throws IllegalArgumentException if the load factor is less than zero
  161.      * @since 4.1
  162.      */
  163.     public LRUMap(final int maxSize,
  164.                   final int initialSize,
  165.                   final float loadFactor,
  166.                   final boolean scanUntilRemovable) {

  167.         super(initialSize, loadFactor);
  168.         if (maxSize < 1) {
  169.             throw new IllegalArgumentException("LRUMap max size must be greater than 0");
  170.         }
  171.         if (initialSize > maxSize) {
  172.             throw new IllegalArgumentException("LRUMap initial size must not be greater than max size");
  173.         }
  174.         this.maxSize = maxSize;
  175.         this.scanUntilRemovable = scanUntilRemovable;
  176.     }

  177.     /**
  178.      * Constructor copying elements from another map.
  179.      * <p>
  180.      * The maximum size is set from the map's size.
  181.      * </p>
  182.      *
  183.      * @param map  the map to copy
  184.      * @throws NullPointerException if the map is null
  185.      * @throws IllegalArgumentException if the map is empty
  186.      */
  187.     public LRUMap(final Map<? extends K, ? extends V> map) {
  188.         this(map, false);
  189.     }

  190.     /**
  191.      * Constructor copying elements from another map.
  192.      *
  193.      * <p>The maximum size is set from the map's size.</p>
  194.      *
  195.      * @param map  the map to copy
  196.      * @param scanUntilRemovable  scan until a removable entry is found, default false
  197.      * @throws NullPointerException if the map is null
  198.      * @throws IllegalArgumentException if the map is empty
  199.      * @since 3.1
  200.      */
  201.     public LRUMap(final Map<? extends K, ? extends V> map, final boolean scanUntilRemovable) {
  202.         this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable);
  203.         putAll(map);
  204.     }

  205.     /**
  206.      * Adds a new key-value mapping into this map.
  207.      * <p>
  208.      * This implementation checks the LRU size and determines whether to
  209.      * discard an entry or not using {@link #removeLRU(AbstractLinkedMap.LinkEntry)}.
  210.      * </p>
  211.      * <p>
  212.      * From Commons Collections 3.1 this method uses {@link #isFull()} rather
  213.      * than accessing {@code size} and {@code maxSize} directly.
  214.      * It also handles the scanUntilRemovable functionality.
  215.      * </p>
  216.      *
  217.      * @param hashIndex  the index into the data array to store at
  218.      * @param hashCode  the hash code of the key to add
  219.      * @param key  the key to add
  220.      * @param value  the value to add
  221.      */
  222.     @Override
  223.     protected void addMapping(final int hashIndex, final int hashCode, final K key, final V value) {
  224.         if (isFull()) {
  225.             LinkEntry<K, V> reuse = header.after;
  226.             boolean removeLRUEntry = false;
  227.             if (scanUntilRemovable) {
  228.                 while (reuse != header && reuse != null) {
  229.                     if (removeLRU(reuse)) {
  230.                         removeLRUEntry = true;
  231.                         break;
  232.                     }
  233.                     reuse = reuse.after;
  234.                 }
  235.                 if (reuse == null) {
  236.                     throw new IllegalStateException(
  237.                         "Entry.after=null, header.after=" + header.after + " header.before=" + header.before +
  238.                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
  239.                         " This should not occur if your keys are immutable and you used synchronization properly.");
  240.                 }
  241.             } else {
  242.                 removeLRUEntry = removeLRU(reuse);
  243.             }

  244.             if (removeLRUEntry) {
  245.                 if (reuse == null) {
  246.                     throw new IllegalStateException(
  247.                         "reuse=null, header.after=" + header.after + " header.before=" + header.before +
  248.                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
  249.                         " This should not occur if your keys are immutable and you used synchronization properly.");
  250.                 }
  251.                 reuseMapping(reuse, hashIndex, hashCode, key, value);
  252.             } else {
  253.                 super.addMapping(hashIndex, hashCode, key, value);
  254.             }
  255.         } else {
  256.             super.addMapping(hashIndex, hashCode, key, value);
  257.         }
  258.     }

  259.     /**
  260.      * Clones the map without cloning the keys or values.
  261.      *
  262.      * @return a shallow clone
  263.      */
  264.     @Override
  265.     public LRUMap<K, V> clone() {
  266.         return (LRUMap<K, V>) super.clone();
  267.     }

  268.     /**
  269.      * Reads the data necessary for {@code put()} to work in the superclass.
  270.      *
  271.      * @param in  the input stream
  272.      * @throws IOException if an error occurs while reading from the stream
  273.      * @throws ClassNotFoundException if an object read from the stream cannot be loaded
  274.      */
  275.     @Override
  276.     protected void doReadObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
  277.         maxSize = in.readInt();
  278.         super.doReadObject(in);
  279.     }

  280.     /**
  281.      * Writes the data necessary for {@code put()} to work in deserialization.
  282.      *
  283.      * @param out  the output stream
  284.      * @throws IOException if an error occurs while writing to the stream
  285.      */
  286.     @Override
  287.     protected void doWriteObject(final ObjectOutputStream out) throws IOException {
  288.         out.writeInt(maxSize);
  289.         super.doWriteObject(out);
  290.     }

  291.     /**
  292.      * Gets the value mapped to the key specified.
  293.      * <p>
  294.      * This operation changes the position of the key in the map to the
  295.      * most recently used position (last).
  296.      *
  297.      * @param key  the key
  298.      * @return the mapped value, null if no match
  299.      */
  300.     @Override
  301.     public V get(final Object key) {
  302.         return get(key, true);
  303.     }

  304.     /**
  305.      * Gets the value mapped to the key specified.
  306.      * <p>
  307.      * If {@code updateToMRU} is {@code true}, the position of the key in the map
  308.      * is changed to the most recently used position (last), otherwise the iteration
  309.      * order is not changed by this operation.
  310.      * </p>
  311.      *
  312.      * @param key  the key
  313.      * @param updateToMRU  whether the key shall be updated to the
  314.      *   most recently used position
  315.      * @return the mapped value, null if no match
  316.      * @since 4.1
  317.      */
  318.     public V get(final Object key, final boolean updateToMRU) {
  319.         final LinkEntry<K, V> entry = getEntry(key);
  320.         if (entry == null) {
  321.             return null;
  322.         }
  323.         if (updateToMRU) {
  324.             moveToMRU(entry);
  325.         }
  326.         return entry.getValue();
  327.     }

  328.     /**
  329.      * Returns true if this map is full and no new mappings can be added.
  330.      *
  331.      * @return {@code true} if the map is full
  332.      */
  333.     @Override
  334.     public boolean isFull() {
  335.         return size >= maxSize;
  336.     }

  337.     /**
  338.      * Tests whether this LRUMap will scan until a removable entry is found when the
  339.      * map is full.
  340.      *
  341.      * @return true if this map scans
  342.      * @since 3.1
  343.      */
  344.     public boolean isScanUntilRemovable() {
  345.         return scanUntilRemovable;
  346.     }

  347.     /**
  348.      * Gets the maximum size of the map (the bound).
  349.      *
  350.      * @return the maximum number of elements the map can hold
  351.      */
  352.     @Override
  353.     public int maxSize() {
  354.         return maxSize;
  355.     }

  356.     /**
  357.      * Moves an entry to the MRU position at the end of the list.
  358.      * <p>
  359.      * This implementation moves the updated entry to the end of the list.
  360.      * </p>
  361.      *
  362.      * @param entry  the entry to update
  363.      */
  364.     protected void moveToMRU(final LinkEntry<K, V> entry) {
  365.         if (entry.after != header) {
  366.             modCount++;
  367.             // remove
  368.             if (entry.before == null) {
  369.                 throw new IllegalStateException("Entry.before is null." +
  370.                     " This should not occur if your keys are immutable, and you have used synchronization properly.");
  371.             }
  372.             entry.before.after = entry.after;
  373.             entry.after.before = entry.before;
  374.             // add first
  375.             entry.after = header;
  376.             entry.before = header.before;
  377.             header.before.after = entry;
  378.             header.before = entry;
  379.         } else if (entry == header) {
  380.             throw new IllegalStateException("Can't move header to MRU" +
  381.                     " This should not occur if your keys are immutable, and you have used synchronization properly.");
  382.         }
  383.     }

  384.     /**
  385.      * Deserializes the map in using a custom routine.
  386.      *
  387.      * @param in the input stream
  388.      * @throws IOException if an error occurs while reading from the stream
  389.      * @throws ClassNotFoundException if an object read from the stream cannot be loaded
  390.      */
  391.     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
  392.         in.defaultReadObject();
  393.         doReadObject(in);
  394.     }

  395.     /**
  396.      * Subclass method to control removal of the least recently used entry from the map.
  397.      * <p>
  398.      * This method exists for subclasses to override. A subclass may wish to
  399.      * provide cleanup of resources when an entry is removed. For example:
  400.      * </p>
  401.      * <pre>
  402.      * protected boolean removeLRU(LinkEntry entry) {
  403.      *   releaseResources(entry.getValue());  // release resources held by entry
  404.      *   return true;  // actually delete entry
  405.      * }
  406.      * </pre>
  407.      * <p>
  408.      * Alternatively, a subclass may choose to not remove the entry or selectively
  409.      * keep certain LRU entries. For example:
  410.      * </p>
  411.      * <pre>
  412.      * protected boolean removeLRU(LinkEntry entry) {
  413.      *   if (entry.getKey().toString().startsWith("System.")) {
  414.      *     return false;  // entry not removed from LRUMap
  415.      *   } else {
  416.      *     return true;  // actually delete entry
  417.      *   }
  418.      * }
  419.      * </pre>
  420.      * <p>
  421.      * The effect of returning false is dependent on the scanUntilRemovable flag.
  422.      * If the flag is true, the next LRU entry will be passed to this method and so on
  423.      * until one returns false and is removed, or every entry in the map has been passed.
  424.      * If the scanUntilRemovable flag is false, the map will exceed the maximum size.
  425.      * </p>
  426.      * <p>
  427.      * Note: Commons Collections 3.0 passed the wrong entry to this method.
  428.      * This is fixed in version 3.1 onwards.
  429.      * </p>
  430.      *
  431.      * @param entry  the entry to be removed
  432.      * @return {@code true}
  433.      */
  434.     protected boolean removeLRU(final LinkEntry<K, V> entry) {
  435.         return true;
  436.     }

  437.     /**
  438.      * Reuses an entry by removing it and moving it to a new place in the map.
  439.      * <p>
  440.      * This method uses {@link #removeEntry}, {@link #reuseEntry} and {@link #addEntry}.
  441.      *
  442.      * @param entry  the entry to reuse
  443.      * @param hashIndex  the index into the data array to store at
  444.      * @param hashCode  the hash code of the key to add
  445.      * @param key  the key to add
  446.      * @param value  the value to add
  447.      */
  448.     protected void reuseMapping(final LinkEntry<K, V> entry, final int hashIndex, final int hashCode,
  449.                                 final K key, final V value) {
  450.         // find the entry before the entry specified in the hash table
  451.         // remember that the parameters (except the first) refer to the new entry,
  452.         // not the old one
  453.         try {
  454.             final int removeIndex = hashIndex(entry.hashCode, data.length);
  455.             final HashEntry<K, V>[] tmp = data;  // may protect against some sync issues
  456.             HashEntry<K, V> loop = tmp[removeIndex];
  457.             HashEntry<K, V> previous = null;
  458.             while (loop != entry && loop != null) {
  459.                 previous = loop;
  460.                 loop = loop.next;
  461.             }
  462.             if (loop == null) {
  463.                 throw new IllegalStateException(
  464.                     "Entry.next=null, data[removeIndex]=" + data[removeIndex] + " previous=" + previous +
  465.                     " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
  466.                     " This should not occur if your keys are immutable, and you have used synchronization properly.");
  467.             }

  468.             // reuse the entry
  469.             modCount++;
  470.             removeEntry(entry, removeIndex, previous);
  471.             reuseEntry(entry, hashIndex, hashCode, key, value);
  472.             addEntry(entry, hashIndex);
  473.         } catch (final NullPointerException ex) {
  474.             throw new IllegalStateException("NPE, entry=" + entry + " entryIsHeader=" + (entry == header) + " key=" + key + " value=" + value + " size=" + size
  475.                     + " maxSize=" + maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
  476.         }
  477.     }

  478.     /**
  479.      * Updates an existing key-value mapping.
  480.      * <p>
  481.      * This implementation moves the updated entry to the end of the list
  482.      * using {@link #moveToMRU(AbstractLinkedMap.LinkEntry)}.
  483.      * </p>
  484.      *
  485.      * @param entry  the entry to update
  486.      * @param newValue  the new value to store
  487.      */
  488.     @Override
  489.     protected void updateEntry(final HashEntry<K, V> entry, final V newValue) {
  490.         moveToMRU((LinkEntry<K, V>) entry);  // handles modCount
  491.         entry.setValue(newValue);
  492.     }

  493.     /**
  494.      * Serializes this object to an ObjectOutputStream.
  495.      *
  496.      * @param out the target ObjectOutputStream.
  497.      * @throws IOException thrown when an I/O errors occur writing to the target stream.
  498.      */
  499.     private void writeObject(final ObjectOutputStream out) throws IOException {
  500.         out.defaultWriteObject();
  501.         doWriteObject(out);
  502.     }

  503. }