【手撕Java集合】Map容器概述以及常用子类HashMap、LinkedHashMap详解(附部分源码)

本文详细介绍了Map集合的基本概念、与Collection的区别,并深入讲解了HashMap和LinkedHashMap的特性和实现原理。HashMap是无序、非同步的,使用散列表+红黑树结构,允许null键值对,而LinkedHashMap则保持插入顺序或访问顺序。两者在put、resize、get和remove等方法上有不同的实现策略。

一、Map 集合概述

1. 什么是 Map?

Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复,并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。

如果需要存储几百万个学生,而且经常需要使用学号来搜索某个学生,那么这个需求有效的数据结构就是Map。

1629788940349

2. Map 和 Collection 的区别

  • Map 与 Collection 在集合框架中属并列存在。

  • Map 存储的是键值对,而 Collection 只存储值(也就是Collection是单列集合, Map 是双列集合)

  • Map 存储元素使用 put 方法,Collection 使用 add 方法。

  • Map 集合没有直接迭代元素的方法,而是先转成 Set 集合(entrySet),再通过迭代获取元素

  • Map 集合中键要保证唯一性,值是可以重复的。

3. Map 的结构体系

1630048100635

4. Map 的基础功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeF1Ukp6-1645777192905)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636783331643.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qjINltBA-1645777192909)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636783405837.png)]

5. Map 集合常用子类

  • HashMap

    • 无序。
    • 底层是 散列表(数组+链表)+红黑树 数据结构。
    • 线程是不同步的,可以存入 null 键,null 值。
    • 要保证键的唯一性,需要覆盖 hashCode 方法和 equals 方法。
  • LinkedHashMap

    • 底层是 散列表+双向链表。可以 Map 集合进行增删提高效率。
    • 插入顺序是有序的。
    • 允许为 null,不同步
  • HashTable

    • 底层是 散列表
    • 同步(线程安全),但效率较低。
    • key 和 value 都不允许为 null。
  • TreeMap

    • 底层是 红黑树。方法的复杂度大都是 log(n)。
    • 非同步
    • TreeMap 实现了 NavigableMap 接口,而 NavigableMap 接口继承了 SortedMap 接口,致使 TreeMap 是有序的。
    • 使用 Comparator 或者 Comparable 来比较 key 是否相等与排序问题。
  • ConcurrentHashMap

    • 底层是 散列表+红黑树
    • 同步(线程安全),支持高并发的访问和更新。
    • key 和 value 都不允许为 null。
    • 检索操作不用加锁,get 方法是非阻塞的。

二、HashMap

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znqwnf1m-1645777192910)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636784047067.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tyuVIcW-1645777192913)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636784278422.png)]

由类注释可以归纳出 HashMap 的特点:

  • 允许为 null, 但 null 作为键只能有一个,null 作为值可以有多个
  • 非同步,无序
  • 底层是 散列表+(红黑树)
  • 初始容量和装载因子都会影响迭代性能

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hAmZHGJC-1645777192915)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636889496356.png)]

1. HashMap 的属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PZeQTdnL-1645777192917)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636785081539.png)]

成员属性有如下几个:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LucGppY-1645777192919)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636785213649.png)]

属性详解:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;
    // table数组默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // table数组最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的填充因子
    /**
     *默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
     *无参构造方法创建的HashMap 对象,他初始长度默认是16  阈值 = 当前长度 * 0.75  就
     *能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    /**
    * threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。
    */
    int threshold;
    // 加载因子
    final float loadFactor;
}

HashMap 中还有一个内部类 Node,用于存放散列表下的链表结构中的节点元素:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6w66Zxl-1645777192921)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636785301797.png)]

2. HashMap 的构造器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCFUToDl-1645777192922)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636890733965.png)]

HashMap(int, float)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aa7r9EHv-1645777192923)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636785609555.png)]

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     * 用确定的初始化容量和装载因子构造一个HashMap
     *
     * @param  initialCapacity the initial capacity 初始化容量
     * @param  loadFactor      the load factor 装载因子
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) //初始容量不能为0
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) //容量大于最大允许容量(2^30)
            initialCapacity = MAXIMUM_CAPACITY; //赋为最大允许容量
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) //装载因子不能非法
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor; //赋为默认装载因子
        this.threshold = tableSizeFor(initialCapacity); 
    }

其中最后一行调用的 tableSizeFor() 方法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1zNoiopR-1645777192925)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636786567154.png)]

为啥是将2的整数幂的数赋给threshold

threshold 这个成员变量是阈值,决定了是否要将散列表再散列。它的值应该是: capacity * loadfactor

其实这里仅仅是一个初始化,当创建哈希表的时候,它会重新赋值的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dkrQDGIf-1645777192926)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636786763891.png)]

另外几个构造器:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     * 指定容量大小的构造函数,装载因子为默认值0.75
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     * 默认构造函数,容量为16,装载因子为0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     * 包含另一个“Map”的构造函数
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

3. put 方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
        //以key计算哈希值,传入key、value还有两个参数
    }

HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。

对 putVal 方法添加元素的分析如下:

  1. 如果定位到的数组位置没有元素就直接插入。
  2. 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uD2dVplJ-1645777192927)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/58e67eae921e4b431782c07444af824e_720w.png)]

说明:上图有两个小问题:

  • 直接覆盖之后应该就会 return,不会有后续操作。
  • 链表长度大于阈值(默认为 8)并且 HashMap 数组长度超过 64 的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。

源码解析:

public V put(K key, V value) {
    // 对key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // hash值不相等,即key不相等;如果是红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 如果是链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点(尾插法)
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

插入操作在 JDK1.7 和 JDK1.8 是有所不同的,JDK1.7 底层采用 数组+链表,插入时采用头插法

4. resize 方法

进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。

分为两步

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
  • ReHash:遍历原Entry数组,把所有的 Entry 重新 Hash 到新数组。

在初始化的时候要调用这个方法,当散列表元素大于 capacity * load factor 的时候也是调用 resize()

每次扩容之后容量都是翻倍。扩容后要将原数组中的所有元素找到在新数组中合适的位置。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好容忍碰撞
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;//直接返回旧的散列表
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {
        // signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

5. get 方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
    // 计算key的哈希值,调用getNode()获取对应的value
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {// 如果计算出来的哈希值存在该哈希表上
        // 在桶上首位可以找到就直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 否则在红黑树中找或者遍历链表来寻找
        if ((e = first.next) != null) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

6. remove 方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h65MoiNY-1645777192929)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636792953639.png)]

removeNode()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZ9S6s7w-1645777192930)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636793016587.png)]

三、LinkedHashMap

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xpzMz4Xr-1645777192932)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636793121789.png)]

类注释:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ThaozKXC-1645777192934)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636801168514.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bj43zVjm-1645777192937)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636801198882.png)]

从中可以归纳出 LinkedHashMap 的一些特点:

  • 底层是 散列表和双向链表
  • 允许为 null,不同步
  • 插入的顺序是有序的
  • 装载因子和初始容量对 LinkedHashMap 影响很大

通过上面的继承体系,我们知道它继承了 HashMap,所以它的内部也有(数组+链表+红黑树)这三种结构,但是它还额外添加了一种“双向链表”的结构存储所有元素的顺序。

添加删除元素的时候需要同时维护在 HashMap 中的存储,也要维护在 LinkedList 中的存储,所以性能上来说会比HashMap稍慢。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JAslUQoC-1645777192939)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/249993-20161215143120620-1544337380.png)]

1. LinkedHashMap 的属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7JtWDgnj-1645777192940)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636803993150.png)]

  • head

    双向链表的头节点,旧数据存在头节点。

  • tail

    双向链表的尾节点,新数据存在尾节点。

  • accessOrder

    是否需要按访问顺序排序,通过注释发现该变量为 true 时 access-order,即按访问顺序遍历,此时你任何一次的操作,包括 put、get 操作,都会改变map中已有的存储顺序,如果为 false,则表示按插入顺序遍历。默认为 false 也就是按照插入顺序。

内部类:

下面两个内部类都是用于存储节点,继承自HashMap 的 Node类,其 next 属性用于单链表存储于桶中,而 Entry 类的 before 和 after 用于双向链表存储所有元素。

// 位于LinkedHashMap中
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中
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;
    }
    ...
}

2. LinkedHashMap 的构造方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HF6MkWqc-1645777192943)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636804800896.png)]

/**
 * 构造方法1,构造一个指定初始容量和负载因子的、按照插入顺序的LinkedList
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * 构造方法2,构造一个指定初始容量的LinkedHashMap,取得键值对的顺序是插入顺序
 */
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

/**
 * 构造方法3,用默认的初始化容量和负载因子创建一个LinkedHashMap,取得键值对的顺序是插入顺序
 */
public LinkedHashMap() {
    super();  //其实就是调用的hashmap的默认构造方法,默认加载因子0.75
    accessOrder = false;
}

/**
 * 构造方法4,通过传入的map创建一个LinkedHashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,加
 *载因子为默认值
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}

/**
 * 构造方法5,根据指定容量、加载因子和键值对保持顺序创建一个LinkedHashMap
 */
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
  • 前四个构造方法 accessOrder 都等于 false,说明双向链表是按插入顺序存储元素。
  • 最后一个构造方法 accessOrder 从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现 LRU 缓存策略 的关键。

3. newNode 方法

LinkedHashMap 重写了 newNode() 方法,通过此方法保证了插入的顺序性,在此之前先看一下 HashMap 的 newNode() 方法 :

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

再看一下 LinkedHashMap 的 newNode() 方法 :

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);//其实还是用的hashmap中的方法
    linkNodeLast(p);
    return p;
}

在构建新节点时,构建的是 LinkedHashMap.Entry 不再是 Node。

这里调用了一个方法 linkNodeLast(),我们看一下这个方法,这个方法不止完成了串联后置,也完成了串联前置,所以插入的顺序性是通过这个方法保证的。

4. LinkNodeLast 方法

// 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结点与尾结点相连通
        p.before = last;
        last.after = p;
    }
}
  • 插入的顺序的保证是通过newNode()方法再调用linkNodeLast()
  • 将新插入的结点连接到尾结点

5. afterNodeAccess 方法

//也就是把p结点从中间拿出来放到尾部
void afterNodeAccess(Node<K,V> e) { // move node to last 将节点移动到最后一个
    LinkedHashMap.Entry<K,V> last;
    // accessOrder 确定是按照访问顺序的,如果当前节点不是最后节点,因为是的话就不用移了
    if (accessOrder && (last = tail) != e) {
        //强转一下e给到p。然后把e的前后结点都取出来
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //因为要插到最后所以先给null
        p.after = null;
        //如果p结点前结点为空,将p的后结点变头结点
        if (b == null)
            head = a;
        else
            //将p的前结点指向p的后结点 b>p>a   b>a
            b.after = a;
        //将a也指向b  a<>b
        if (a != null)
            a.before = b;
        else
            //a等于null,就将b作为尾结点
            last = b;
        if (last == null)
            head = p;
        else {
            // last<>p
            p.before = last;
            last.after = p;
        }
        //尾结点p
        tail = p;
        ++modCount;
    }
}
  • 在 HashMap 中没给具体实现,而在LinkedHashMap 重写了这个方法,目的是保证操作过的Node节点永远在最后,从而保证读取的顺序性,在调用 put 方法和 get 方法时都会用到。
  • 从双向链表中移除访问的节点,把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

newNode() 方法中调用的 linkNodeLast(Entry e) 方法和现在的 afterNodeAccess(Node e) 都是将传入的Node节点放到最后,那么它们的使用场景如何呢?

HashMap 的 put 流程中,如果在对应的 hash 位置上还没有元素,那么直接 new Node() 放到数组 table 中,这个时候对应到 LinkedHashMap 中,调用了 newNode() 方法,就会用到 linkNodeLast(),将新 node 放到双向链表的最后。

如果对应的 hash 位置上有元素,进行元素值的覆盖时,就会调用 afterNodeAccess(),将原本可能不是最后的 node 节点移动到了最后。

6. put 方法

LinkedHashMap 并没有重写 HashMap 中的 put 方法,而是直接继承来使用。所以 LinkedHashMap 和 HashMap 的 put 方法是一样的。

进行适当删减后的 put 方法逻辑:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    if ((p = tab[i = (n - 1) & hash]) == null)
    		// 调用linkNodeLast方法将新元素放到最后
        tab[i] = newNode(hash, key, value, null);
    else {
        if (e != null) {
        	// 如果key已经存在
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 将存在的节点放到最后(因为存在所以是Access,也就是访问存在的元素)
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();   
    afterNodeInsertion(evict);
    return null;
}

7. get 方法

public V get(Object key) {
    Node<K,V> e;
    // 调用HashMap定义的方法获取对应的节点
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e); // 如果是采用访问顺序遍历就会将该节点放到链表尾部
    return e.value;
}

8. remove 方法

对于 remove 方法,在 LinkedHashMap 中也没有重写,它调用的还是父类 HashMap 的 remove()方法,在 LinkedHashMap 中重写的是: afterNodeRemoval(Node<K,V> e) 这个方法。

9. afterNodeRemoval 方法

//就是将e从双链表中移除
void afterNodeRemoval(Node<K,V> e) { // unlink
    // b<>p<>a
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //将p脱离
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        //b>a
        b.after = a;
    if (a == null)
        tail = b;
    else
        //b<>a
        a.before = b;
}
  • 在节点被删除之后调用的方法
  • 因为 LinkedHashMap 的双向链表连接了 LinkedHashMap 中的所有元素,HashMap 中的删除方法可没有考虑这些,它只考虑了如何存红黑树、链表中删除节点,是不维护双向链表的,所以这里才有了这个方法的实现

在 remove 的时候会涉及到这个重写的 afterNodeRemoval 方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6qVHY31E-1645777192944)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636807340334.png)]

10. 遍历

Set<Map.Entry<K,V>> entrySet()是被重写的了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Duxde7t1-1645777192945)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636807540620.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AWcrXh5L-1645777192946)(https://gitee.com/coder-kaho/myNotes/raw/master/Java_Container/images/1636807592047.png)]

到这就可以解释为什么初始容量对遍历没有影响

因为它遍历的是 LinkedHashMap 内部维护的一个双向链表,而不是散列表。

11. 总结

四个维护双向链表的方法:

  • afterNodeAccess(Node<K,V> p)

    访问元素之后维护

  • afterNodeInsertion(boolean evict)

    插入元素之后维护

  • afterNodeRemoval(Node<K,V> p)

    删除元素之后维护

  • linkNodeLast(LinkedHashMap.Entry<K,V> p)

    也是插入元素之后维护,但是只用于桶上的第一个节点,后面的节点都是用afterNodeAccess或者afterNodeInsertion

LinkedHashMap

(1)LinkedHashMap 继承自 HashMap,具有 HashMap 的所有特性;

(2)LinkedHashMap 内部维护了一个双向链表存储所有的元素,LinkedHashMap 指的是遍历的时候的有序性,而有序性是通过双向链表实现的,真实的存储之间是没有顺序的。LinkedHashMap 实现了很多方法来维护这个有序性。

(3)如果 accessOrder 为 false,则可以按插入元素的顺序遍历元素;

(4)如果 accessOrder 为 true,则可以按访问元素的顺序遍历元素,也就是无论 put、get 都会将元素放到链表尾部;

(5)默认的 LinkedHashMap 并不会移除旧元素,如果需要移除旧元素,则需要重写 removeEldestEntry() 方法设定移除策略;

(6)LinkedHashMap 可以用来实现 LRU 缓存淘汰策略

(7)如何实现一个固定大小的 LinkedHashMap?继承 LinkedHashMap 实现 removeEldestEntry 方法,当插入成功后,判断是否要删除最老节点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kaho Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值