LinkedHashMap 学习笔记

LinkedHashMap 在 Map 集合框架的位置

LinkedHashMap 学习笔记_第1张图片

LinkedHashMap简介

LinkedHashMap是Map 接口的 哈希表(hash table) 和 链表(linked list) 的实现,具有可预测的迭代顺序。此实现与 HashMap 的不同之处在于,它维护一个双链接列表,该列表贯穿其所有条目。这个链表定义了迭代的顺序,通常是键值插入到 map 中的插入顺序(insertion-order). 请注意,如果键重新插入到 map 中,则插入顺序不受影响。

在遍历LinkedHashMap时,不会像遍历 HashMap 和 Hashtable 所得到的遍历顺序是不确定的。而且LinkedHashMap在遍历有顺序保证的同时,不会像TreeMap那样招致额外的开销。它可以用来拷贝已有的map, 不用考虑这个被拷贝的map对象原来的实现方式。拷贝出来的map对象和原始的map对象的顺序是一致的。

     void foo(Map m) {
         Map copy = new LinkedHashMap(m);
         ...
     }

写一个测试方法跑一下看下结果:

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapTest {
  public static void main(String args[]) {
      Map<Integer, Integer> map = new HashMap();
      for(int i=1; i<5; i++) {
          map.put(i, i*i);
      }
      for(int i=1; i>-5; i--) {
          map.put(i, i*i);
      }
      System.out.println(map);
      System.out.println(map);
      System.out.println(map);
      System.out.println("-----------------copy from map -----------------------");
      LinkedHashMap<Integer,Integer> copy = new LinkedHashMap<Integer, Integer>(map);
      System.out.println(copy);
      Map<Integer, Integer> linkedHashMap = new LinkedHashMap();
      for(int i=1; i<5; i++) {
          linkedHashMap.put(i, i*i);
      }
      for(int i=1; i>-5; i--) {
          linkedHashMap.put(i, i*i);
      }
      System.out.println("-----------------------------------------");
      System.out.println(linkedHashMap);
      System.out.println("---------- put exist key again ---------------------");
      for(int i=1; i<5; i++) {
          linkedHashMap.put(i, i*i);
      }
      System.out.println(linkedHashMap);
      System.out.println("--- remove exist key and put again, keep insert order ----");
      linkedHashMap.remove(3);
      linkedHashMap.put(3, 9);
      System.out.println(linkedHashMap);
  }
}

输出内容:

{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------copy from map -----------------------
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------------------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
---------- put exist key again ---------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
--- remove exist key and put again, keep insert order ----
{1=1, 2=4, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16, 3=9}

如果模块在输入上获取映射,将其复制并随后返回结果(其顺序由副本的顺序确定),则此技术特别有用。(客户通常喜欢按插入的顺序返回。)

构造函数的 accessOrder 参数

LinkedHashMap 有一个特殊的构造函数:

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

这个构造函数的参数除了有初始化容量 initialCapacity 和加载因子 loadFactor 之外,还有一个布尔参数:accessOrder
accessOrder指定了维护顺序的模式。

  • accessOrder = false. 按照插入顺序进行排序。
  • accessOrder = true, 按照访问顺序排序。也就是根据上一次访问截止时。按照最近最少到最近最多的访问顺序排列。
    写一个方法测试一下:
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapTest {
    public static void main(String args[]) {
        Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(10, 0.75f, true);
        for(int i=0; i<10; i++) {
            map.put(i, i*i);
        }
        System.out.println("------------origin order---------");
        System.out.println(map);
        System.out.println(map.values());

        for(int i=1; i<10; i++) {
            map.get(3);
        }
        for(int i=1; i<30; i++) {
            map.get(6);
        }
        for(int i=1; i<10; i++) {
            map.get(4);
        }
        for(int i=1; i<9; i++) {
            map.get(2);
        }
        System.out.println("------------ after access ---------");
        System.out.println(map);
        System.out.println(map.values());
    }
}

输出内容:

------------origin order---------
{0=0, 1=1, 2=4, 3=9, 4=16, 5=25, 6=36, 7=49, 8=64, 9=81}
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
------------ after access ---------
{0=0, 1=1, 5=25, 7=49, 8=64, 9=81, 3=9, 6=36, 4=16, 2=4}
[0, 1, 25, 49, 64, 81, 9, 36, 16, 4]

它的移动算法如下:

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

重写 removeEldestEntry(Map.Entry eldest) 方法

如果此 Map 应删除其老的条目,则返回 true。 此方法在将新条目插入映射后,通过 put 和 putAll 调用。 它为实现者提供了每次添加新条目时删除最老条目的机会。 如果映射表示缓存,这非常有用:它允许Map通过删除陈旧条目来减少内存消耗。
写一个代码测试一下:

import java.util.LinkedHashMap;
import java.util.Map;

public class CacheMap<K, V> extends LinkedHashMap<K, V> {
    public static final int MAX_SIZE = 10;

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> entry) {
        return size() > MAX_SIZE;
    }

    public CacheMap(int i, float v, boolean b) {
        super(i, v, b);
    }

    public static void main(String[] args) {
        CacheMap<Integer, Integer> cacheMap = new CacheMap<Integer, Integer>(40, 0.75f, true);
        for (int i=0; i<40; i++) {
            cacheMap.put(i, i*i);
        }
        for (int i=1; i<20; i++) {
            cacheMap.get(3);
        }
        System.out.println(cacheMap);
        System.out.println("cacheMap.size() = "+cacheMap.size());
        cacheMap.put(20, 400);
        System.out.println("-----------after insert new element---------------");
        System.out.println(cacheMap);
        System.out.println("cacheMap.size() = "+cacheMap.size());
    }
}

输出内容:

{30=900, 31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521}
cacheMap.size() = 10
-----------after insert new element---------------
{31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521, 20=400}
cacheMap.size() = 10

看到没有,在CacheMap里面,我设置最大容量是10.通过重写removeEldestEntry方法,在里面判断size()和MAX_SIZE的大小决定要不要移除最老的元素。以达到插入新元素时移除老元素的目的。

此方法通常不会以任何方式修改map,而是允许map按照其返回值的指示进行自身修改。此方法允许直接修改map,但如果这样做,则必须返回 false(指示map不应尝试任何进一步修改)。未指定在此方法内修改映map返回 true 的效果。

在LinkedHashMap中,此实现仅返回 false(因此此映射的行为类似于普通地图 - 最长的元素永远不会被删除)。

LinkedHashMap 的数据结构

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
 //-------------------------------------------------------------------------------------------------
  //------------------------------------------- HashMap.Node ----------------------------------------
   //-------------------------------------------------------------------------------------------------
  /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

可以看到,Entry 和Map.Node相比,多了两个属性:before 和 after. 用于维护双向队列。

如何在队尾插入元素

  • 先把 tail 备份为 last
  • 把tail赋值为新entry p
  • 如果last为null.表示空map.直接把新 entry p 赋值给 head
  • 如果last不为null,表示非空map. 把p的前驱节点赋值为之前的last.把之前的last的后去赋值为新 entry p.
    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    ```

### 如何替换Entry
```java
    // apply src's links to dst
    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                               LinkedHashMap.Entry<K,V> dst) {
        LinkedHashMap.Entry<K,V> b = dst.before = src.before;
        LinkedHashMap.Entry<K,V> a = dst.after = src.after;
        if (b == null)
            head = dst;
        else
            b.after = dst;
        if (a == null)
            tail = dst;
        else
            a.before = dst;
    }

你可能感兴趣的:(map,LinkedHashMap,java,LinkedHashMap,Map)